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_BUCKET_NAME # 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_BUCKET_NAME = env("MINIO_BUCKET_NAME", default="winofy-media") # replacement for MEDIA_ROOT MINIO_PRIVATE_BUCKETS = [ 'default', ] MINIO_PUBLIC_BUCKETS = [ 'default-public', MINIO_BUCKET_NAME, ] 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_BUCKET_NAME, "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": "Media", "description": "Presigned MinIO upload URLs for product/store images."}, {"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