Swaps default media storage from local FileSystemStorage to MinIO via django-minio-backend, matching the rest of the Winsoo ecosystem (advertising, campaign, wallet). Points at a local MinIO container (added to docker-compose.yml) by default since no shared bucket is provisioned yet. Existing ImageFields pick up the new storage backend automatically — no model changes needed. Verified against a live local MinIO container: bucket auto-creates on first save, object lands under the expected key, URL resolves.
248 lines
9.8 KiB
Python
248 lines
9.8 KiB
Python
from datetime import timedelta
|
|
from pathlib import Path
|
|
|
|
import environ
|
|
from decouple import config
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
|
|
env = environ.Env()
|
|
environ.Env.read_env(BASE_DIR / ".env")
|
|
|
|
SECRET_KEY = env("SECRET_KEY")
|
|
DEBUG = env.bool("DEBUG", default=False)
|
|
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[])
|
|
|
|
INSTALLED_APPS = [
|
|
"django.contrib.admin",
|
|
"django.contrib.auth",
|
|
"django.contrib.contenttypes",
|
|
"django.contrib.sessions",
|
|
"django.contrib.messages",
|
|
"django.contrib.staticfiles",
|
|
"django.contrib.gis",
|
|
|
|
# pip installed apps
|
|
'drf_spectacular',
|
|
'oauth2_provider',
|
|
'rest_framework',
|
|
'rest_framework_gis',
|
|
'django_filters',
|
|
'corsheaders',
|
|
'django_minio_backend.apps.DjangoMinioBackendConfig',
|
|
|
|
# local apps
|
|
'apps.core',
|
|
'apps.users',
|
|
'apps.gooyal_oauth2',
|
|
'apps.locations',
|
|
'apps.stores',
|
|
'apps.catalog',
|
|
'apps.orders',
|
|
'apps.reviews',
|
|
]
|
|
|
|
MIDDLEWARE = [
|
|
"django.middleware.security.SecurityMiddleware",
|
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
'corsheaders.middleware.CorsMiddleware',
|
|
"django.middleware.common.CommonMiddleware",
|
|
'django.middleware.locale.LocaleMiddleware',
|
|
"django.middleware.csrf.CsrfViewMiddleware",
|
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
'oauth2_provider.middleware.OAuth2TokenMiddleware',
|
|
"django.contrib.messages.middleware.MessageMiddleware",
|
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
'utils.exceptions.ErrorMiddleware',
|
|
]
|
|
|
|
OAUTH2_PROVIDER_APPLICATION_MODEL = "gooyal_oauth2.Application"
|
|
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "gooyal_oauth2.AccessToken"
|
|
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "gooyal_oauth2.IDToken"
|
|
OAUTH2_PROVIDER_GRANT_MODEL = "gooyal_oauth2.Grant"
|
|
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "gooyal_oauth2.RefreshToken"
|
|
|
|
OAUTH2_PROVIDER_BASE_PUBLIC_URL = config("OAUTH2_PROVIDER_BASE_PUBLIC_URL", default="")
|
|
OAUTH2_PROVIDER_BASE_PRIVATE_URL = config("OAUTH2_PROVIDER_BASE_PRIVATE_URL", default="")
|
|
|
|
OAUTH2_PROVIDER_CLIENT_ID = config('OAUTH2_PROVIDER_CLIENT_ID', default="")
|
|
OAUTH2_PROVIDER_CLIENT_SECRET = config('OAUTH2_PROVIDER_CLIENT_SECRET', default="")
|
|
OAUTH2_PROVIDER_SCOPES = config('OAUTH2_PROVIDER_SCOPES', default='')
|
|
OAUTH2_PROVIDER = {
|
|
'OAUTH2_VALIDATOR_CLASS': 'apps.gooyal_oauth2.validators.OAuth2Validator',
|
|
'RESOURCE_SERVER_INTROSPECTION_URL': (OAUTH2_PROVIDER_BASE_PRIVATE_URL or OAUTH2_PROVIDER_BASE_PUBLIC_URL) + '/introspect/',
|
|
'RESOURCE_SERVER_INTROSPECTION_CREDENTIALS': (OAUTH2_PROVIDER_CLIENT_ID, OAUTH2_PROVIDER_CLIENT_SECRET),
|
|
'RESOURCE_SERVER_TOKEN_CACHING_SECONDS': 600,
|
|
}
|
|
|
|
AUTHENTICATION_BACKENDS = (
|
|
'oauth2_provider.backends.OAuth2Backend',
|
|
'django.contrib.auth.backends.ModelBackend',
|
|
)
|
|
|
|
ROOT_URLCONF = "config.urls"
|
|
|
|
TEMPLATES = [
|
|
{
|
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
"DIRS": [],
|
|
"APP_DIRS": True,
|
|
"OPTIONS": {
|
|
"context_processors": [
|
|
"django.template.context_processors.request",
|
|
"django.contrib.auth.context_processors.auth",
|
|
"django.contrib.messages.context_processors.messages",
|
|
],
|
|
},
|
|
},
|
|
]
|
|
|
|
WSGI_APPLICATION = "config.wsgi.application"
|
|
|
|
DATABASES = {
|
|
"default": {
|
|
"ENGINE": "django.contrib.gis.db.backends.postgis",
|
|
"NAME": env("DB_NAME", default="winofy_dev"),
|
|
"USER": env("DB_USER", default=""),
|
|
"PASSWORD": env("DB_PASSWORD", default=""),
|
|
"HOST": env("DB_HOST", default="localhost"),
|
|
"PORT": env("DB_PORT", default="5432"),
|
|
}
|
|
}
|
|
|
|
CACHES = {
|
|
"default": {
|
|
"BACKEND": "django_redis.cache.RedisCache",
|
|
"LOCATION": env("REDIS_URL", default="redis://localhost:6379/1"),
|
|
"OPTIONS": {
|
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
},
|
|
}
|
|
}
|
|
|
|
AUTH_PASSWORD_VALIDATORS = [
|
|
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
|
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
|
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
|
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
|
]
|
|
AUTH_USER_MODEL = 'users.User'
|
|
LANGUAGE_CODE = "fa"
|
|
TIME_ZONE = "Asia/Tehran"
|
|
USE_I18N = True
|
|
USE_TZ = True
|
|
LOCALE_PATHS = [BASE_DIR / 'locale']
|
|
|
|
API_BASE_PATH = config('API_BASE_PATH', default='')
|
|
STATIC_URL = "{}static/".format(API_BASE_PATH)
|
|
STATIC_ROOT = BASE_DIR / "static"
|
|
MEDIA_URL = "{}media/".format(API_BASE_PATH)
|
|
MEDIA_ROOT = BASE_DIR / "media"
|
|
|
|
# MinIO — same object-storage pattern as the rest of the Winsoo ecosystem
|
|
# (campaign, wallet, advertising, ...). Points at a local MinIO instance by
|
|
# default; no bucket has been provisioned centrally yet, so MINIO_MEDIA_FILES_BUCKET
|
|
# just needs to be set to whatever this service's bucket ends up being called.
|
|
MINIO_ENDPOINT = env("MINIO_ENDPOINT", default="localhost:9000")
|
|
MINIO_USE_HTTPS = env.bool("MINIO_USE_HTTPS", default=False)
|
|
MINIO_EXTERNAL_ENDPOINT = env("MINIO_EXTERNAL_ENDPOINT", default=MINIO_ENDPOINT)
|
|
MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = env.bool("MINIO_EXTERNAL_ENDPOINT_USE_HTTPS", default=MINIO_USE_HTTPS)
|
|
MINIO_REGION = None
|
|
MINIO_ACCESS_KEY = env("MINIO_ACCESS_KEY", default="minioadmin")
|
|
MINIO_SECRET_KEY = env("MINIO_SECRET_KEY", default="minioadmin")
|
|
MINIO_URL_EXPIRY_HOURS = timedelta(days=1)
|
|
MINIO_CONSISTENCY_CHECK_ON_START = False
|
|
MINIO_MEDIA_FILES_BUCKET = env("MINIO_MEDIA_FILES_BUCKET", default="winofy-media") # replacement for MEDIA_ROOT
|
|
MINIO_PRIVATE_BUCKETS = [
|
|
'default',
|
|
]
|
|
MINIO_PUBLIC_BUCKETS = [
|
|
'default-public',
|
|
MINIO_MEDIA_FILES_BUCKET,
|
|
]
|
|
MINIO_POLICY_HOOKS = []
|
|
MINIO_BUCKET_CHECK_ON_SAVE = True # creates the bucket if missing, then saves
|
|
|
|
STORAGES = {
|
|
"default": {
|
|
"BACKEND": "django_minio_backend.models.MinioBackend",
|
|
"OPTIONS": {
|
|
"MINIO_ENDPOINT": MINIO_ENDPOINT,
|
|
"MINIO_USE_HTTPS": MINIO_USE_HTTPS,
|
|
"MINIO_EXTERNAL_ENDPOINT": MINIO_EXTERNAL_ENDPOINT,
|
|
"MINIO_EXTERNAL_ENDPOINT_USE_HTTPS": MINIO_EXTERNAL_ENDPOINT_USE_HTTPS,
|
|
"MINIO_REGION": MINIO_REGION,
|
|
"MINIO_ACCESS_KEY": MINIO_ACCESS_KEY,
|
|
"MINIO_SECRET_KEY": MINIO_SECRET_KEY,
|
|
"MINIO_URL_EXPIRY_HOURS": MINIO_URL_EXPIRY_HOURS,
|
|
"MINIO_CONSISTENCY_CHECK_ON_START": MINIO_CONSISTENCY_CHECK_ON_START,
|
|
"MINIO_DEFAULT_BUCKET": MINIO_MEDIA_FILES_BUCKET,
|
|
"MINIO_PRIVATE_BUCKETS": MINIO_PRIVATE_BUCKETS,
|
|
"MINIO_PUBLIC_BUCKETS": MINIO_PUBLIC_BUCKETS,
|
|
"MINIO_POLICY_HOOKS": MINIO_POLICY_HOOKS,
|
|
"MINIO_BUCKET_CHECK_ON_SAVE": MINIO_BUCKET_CHECK_ON_SAVE,
|
|
},
|
|
},
|
|
"staticfiles": {
|
|
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
|
},
|
|
}
|
|
|
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
|
|
CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS", default=[])
|
|
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[])
|
|
|
|
if _gdal := env("GDAL_LIBRARY_PATH", default=""):
|
|
GDAL_LIBRARY_PATH = _gdal
|
|
if _geos := env("GEOS_LIBRARY_PATH", default=""):
|
|
GEOS_LIBRARY_PATH = _geos
|
|
|
|
# Platform commission taken from each delivered order's items subtotal (S10 mock: 4%).
|
|
# Overridable per-store via Store.commission_percent.
|
|
DEFAULT_COMMISSION_PERCENT = env.float("DEFAULT_COMMISSION_PERCENT", default=4.0)
|
|
|
|
# Fallback delivery-coverage radius (meters) used when a store has no explicit
|
|
# service_neighborhoods assigned yet.
|
|
NEARBY_RADIUS_METERS = env.int("NEARBY_RADIUS_METERS", default=5000)
|
|
|
|
REST_FRAMEWORK = {
|
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
|
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
|
'rest_framework.authentication.SessionAuthentication',
|
|
),
|
|
'DEFAULT_PERMISSION_CLASSES': (
|
|
'rest_framework.permissions.IsAuthenticated',
|
|
),
|
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
|
'PAGE_SIZE': 50,
|
|
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
|
|
"DEFAULT_SCHEMA_CLASS": "apps.core.schema.TaggedAutoSchema",
|
|
'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler',
|
|
}
|
|
|
|
SPECTACULAR_SETTINGS = {
|
|
"TITLE": "Winofy API",
|
|
"DESCRIPTION": "Winofy — neighborhood multi-vendor marketplace (customer app + seller panel).",
|
|
"VERSION": "1.0.0",
|
|
"SERVE_INCLUDE_SCHEMA": False,
|
|
"TAGS": [
|
|
{"name": "Locations", "description": "Cities and neighborhoods (public reference data)."},
|
|
{"name": "Addresses", "description": "The authenticated customer's saved addresses."},
|
|
{"name": "Stores", "description": "Customer-facing store browsing (home feed, store page)."},
|
|
{"name": "Catalog", "description": "Customer-facing product browsing and search."},
|
|
{"name": "Cart", "description": "The authenticated customer's multi-store cart."},
|
|
{"name": "Checkout", "description": "Splits the cart into per-store orders and starts payment."},
|
|
{"name": "Orders", "description": "Customer order history and tracking."},
|
|
{"name": "Reviews", "description": "Customer reviews of stores/products."},
|
|
{"name": "Notifications", "description": "Shared notification feed (customer app + seller panel)."},
|
|
{"name": "Seller · Store", "description": "The seller's own store profile, settings, working hours."},
|
|
{"name": "Seller · Products", "description": "The seller's product catalog and inventory."},
|
|
{"name": "Seller · Orders", "description": "Seller order management and status stepper."},
|
|
{"name": "Seller · Analytics", "description": "Seller sales dashboard."},
|
|
{"name": "Seller · Reviews", "description": "Reviews left for the seller's store, with reply support."},
|
|
],
|
|
}
|
|
|
|
from config.other_settings.logging import LOGGING # noqa: E402
|