676 lines
23 KiB
Python
676 lines
23 KiB
Python
"""Django settings module.
|
|
|
|
All configuration is supplied via environment variables.
|
|
"""
|
|
from datetime import timedelta
|
|
from dateutil.relativedelta import relativedelta
|
|
from typing import List
|
|
import os
|
|
import pathlib
|
|
import sys
|
|
|
|
from dotenv import load_dotenv
|
|
import braintree
|
|
import dj_database_url
|
|
import meilisearch
|
|
|
|
import common.upload_paths
|
|
|
|
|
|
# Load variables from .env, if available
|
|
path = os.path.dirname(os.path.abspath(__file__)) + '/../.env'
|
|
print(path)
|
|
if os.path.isfile(path):
|
|
load_dotenv(path)
|
|
|
|
|
|
def _get(name: str, default=None, coerse_to=None):
|
|
val = os.environ.get(name, default)
|
|
return coerse_to(val) if coerse_to is not None else val
|
|
|
|
|
|
BASE_DIR = pathlib.Path(__file__).absolute().parent.parent
|
|
TESTING = sys.argv[1:2] == ['test']
|
|
|
|
ADMIN_SITE_HEADER = 'Blender Studio Admin'
|
|
ADMIN_SITE_TITLE = 'Blender Studio'
|
|
|
|
# Application definition
|
|
|
|
INSTALLED_APPS = [
|
|
'django.contrib.redirects',
|
|
'django.contrib.flatpages',
|
|
'emails',
|
|
'blog',
|
|
'comments',
|
|
'common',
|
|
'films',
|
|
'search',
|
|
'static_assets',
|
|
'subscriptions',
|
|
'training',
|
|
'cloud_import',
|
|
'stats',
|
|
'django.contrib.admin',
|
|
'django.contrib.admindocs',
|
|
'django.contrib.auth',
|
|
'django.contrib.contenttypes',
|
|
'django.contrib.humanize',
|
|
'django.contrib.messages',
|
|
'django.contrib.sessions',
|
|
'django.contrib.sites',
|
|
'django.contrib.staticfiles',
|
|
'blender_id_oauth_client',
|
|
'profiles',
|
|
'looper',
|
|
'pipeline',
|
|
'sorl.thumbnail',
|
|
'taggit',
|
|
'actstream',
|
|
'background_task',
|
|
'users',
|
|
'loginas',
|
|
'nested_admin',
|
|
'characters',
|
|
'logentry_admin',
|
|
'rest_framework',
|
|
'rest_framework.authtoken',
|
|
's3direct',
|
|
]
|
|
|
|
AUTH_USER_MODEL = 'users.User'
|
|
|
|
MIDDLEWARE = [
|
|
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
|
|
'django.middleware.security.SecurityMiddleware',
|
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
'django.middleware.common.CommonMiddleware',
|
|
'django.middleware.csrf.CsrfViewMiddleware',
|
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
'django.contrib.messages.middleware.MessageMiddleware',
|
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
'django.contrib.redirects.middleware.RedirectFallbackMiddleware',
|
|
'subscriptions.middleware.SetCurrencyMiddleware',
|
|
]
|
|
|
|
ROOT_URLCONF = 'studio.urls'
|
|
|
|
STATIC_URL = _get('STATIC_URL', '/static-studio/')
|
|
STATIC_ROOT = _get('STATIC_ROOT', BASE_DIR / 'public/static', str)
|
|
|
|
STATICFILES_DIRS = [
|
|
str(BASE_DIR / 'assets_shared'),
|
|
str(BASE_DIR / 'assets_shared/src/scripts/'),
|
|
]
|
|
|
|
MEDIA_URL = '/media/'
|
|
MEDIA_ROOT = BASE_DIR / 'public/media'
|
|
|
|
TEMPLATES = [
|
|
{
|
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
'DIRS': [
|
|
str(BASE_DIR / 'comments/templates'),
|
|
str(BASE_DIR / 'common/templates'),
|
|
str(BASE_DIR / 'films/templates'),
|
|
str(BASE_DIR / 'search/templates'),
|
|
str(BASE_DIR / 'training/templates'),
|
|
],
|
|
'APP_DIRS': True,
|
|
'OPTIONS': {
|
|
'context_processors': [
|
|
'django.template.context_processors.debug',
|
|
'django.template.context_processors.request',
|
|
'django.contrib.auth.context_processors.auth',
|
|
'django.contrib.messages.context_processors.messages',
|
|
'common.context_processors.search_client_config',
|
|
'common.context_processors.settings_analytics_id',
|
|
'common.context_processors.extra_context',
|
|
'training.context_processors.enums',
|
|
# TODO(anna) when Profile model is added, this should become a prop on it instead.
|
|
'training.context_processors.favorited',
|
|
'users.context_processors.user_dict',
|
|
'looper.context_processors.preferred_currency',
|
|
'loginas.context_processors.impersonated_session_status',
|
|
]
|
|
},
|
|
},
|
|
]
|
|
|
|
WSGI_APPLICATION = 'studio.wsgi.application'
|
|
|
|
# Password validation
|
|
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
|
|
|
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'},
|
|
]
|
|
|
|
# Blender ID login with Blender ID OAuth client
|
|
|
|
LOGIN_URL = '/oauth/login'
|
|
LOGOUT_URL = '/oauth/logout'
|
|
LOGIN_REDIRECT_URL = '/'
|
|
LOGOUT_REDIRECT_URL = '/'
|
|
SESSION_COOKIE_AGE = 604_800 * 8 # 8 weeks (in seconds)
|
|
|
|
# Internationalization
|
|
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
|
|
|
LANGUAGE_CODE = 'en-us'
|
|
|
|
TIME_ZONE = 'Europe/Amsterdam'
|
|
|
|
USE_I18N = True
|
|
|
|
USE_L10N = False
|
|
DATE_FORMAT = 'N j, Y'
|
|
TIME_FORMAT = 'H:i'
|
|
DATETIME_FORMAT = f'{DATE_FORMAT} {TIME_FORMAT}'
|
|
|
|
USE_THOUSAND_SEPARATOR = True
|
|
DECIMAL_SEPARATOR = '.'
|
|
THOUSAND_SEPARATOR = ','
|
|
NUMBER_GROUPING = 3
|
|
|
|
USE_TZ = True
|
|
|
|
PIPELINE = {
|
|
'JS_COMPRESSOR': 'pipeline.compressors.jsmin.JSMinCompressor',
|
|
'CSS_COMPRESSOR': 'pipeline.compressors.NoopCompressor',
|
|
'JAVASCRIPT': {
|
|
'studio': {
|
|
'source_filenames': [
|
|
'comments/scripts/*.js',
|
|
'comments/scripts/components/*.js',
|
|
'common/scripts/*.js',
|
|
],
|
|
'output_filename': 'js/studio.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'training': {
|
|
'source_filenames': [
|
|
'training/scripts/section.js',
|
|
'training/scripts/training.js',
|
|
'training/scripts/components/card_training.js',
|
|
],
|
|
'output_filename': 'js/training.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'search': {
|
|
'source_filenames': ['search/scripts/*.js'],
|
|
'output_filename': 'js/search.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'training_search': {
|
|
'source_filenames': ['training/scripts/training_search.js'],
|
|
'output_filename': 'js/training_search.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'film_search': {
|
|
'source_filenames': ['films/scripts/film_search.js'],
|
|
'output_filename': 'js/film_search.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'vendor': {
|
|
'source_filenames': [
|
|
'common/scripts/vendor/bootstrap.bundle.js',
|
|
'common/scripts/vendor/plyr.polyfilled.js',
|
|
'common/scripts/vendor/js.cookie.js',
|
|
'common/scripts/vendor/imagesloaded.pkgd.js',
|
|
'common/scripts/vendor/confetti.browser.min.js',
|
|
],
|
|
'output_filename': 'js/vendor.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'vendor_instantsearch': {
|
|
'source_filenames': [
|
|
'common/scripts/vendor/instant-meilisearch.umd.min.js',
|
|
'common/scripts/vendor/instantsearch.production.min.js',
|
|
],
|
|
'output_filename': 'js/vendor_instantsearch.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'vendor_chartjs': {
|
|
'source_filenames': ['common/scripts/vendor/chart.bundle.min.js'],
|
|
'output_filename': 'js/vendor_chartjs.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'vendor_masonry': {
|
|
'source_filenames': ['common/scripts/vendor/masonry.pkgd.js'],
|
|
'output_filename': 'js/vendor_masonry.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'looper': {
|
|
'source_filenames': [
|
|
'looper/scripts/*.js',
|
|
],
|
|
'output_filename': 'js/looper.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'subscriptions': {
|
|
'source_filenames': [
|
|
'common/scripts/ajax.js',
|
|
'subscriptions/scripts/*.js',
|
|
],
|
|
'output_filename': 'js/subscriptions.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'vendor_highlight': {
|
|
'source_filenames': ['common/scripts/vendor/highlight.min.js'],
|
|
'output_filename': 'js/vendor_highlight.js',
|
|
'extra_context': {'async': False, 'defer': False},
|
|
},
|
|
'ajax': {
|
|
'source_filenames': ['common/scripts/ajax.js'],
|
|
'output_filename': 'js/ajax.js',
|
|
},
|
|
'web-assets': {
|
|
'source_filenames': [
|
|
'tutti/20_theme.js',
|
|
],
|
|
'output_filename': 'js/web-assets.js',
|
|
},
|
|
},
|
|
'STYLESHEETS': {
|
|
'studio': {
|
|
'source_filenames': ('common/styles/studio/studio.sass',),
|
|
'output_filename': 'css/studio.css',
|
|
'extra_context': {'media': 'screen'},
|
|
},
|
|
'vendor_highlight': {
|
|
'source_filenames': ('common/styles/vendor/highlight/monokai-sublime.min.css',),
|
|
'output_filename': 'css/highlight-monokai-sublime.css',
|
|
'extra_context': {'media': 'screen'},
|
|
},
|
|
'looper_admin': {
|
|
'source_filenames': ('looper/styles/*.sass',),
|
|
'output_filename': 'css/looper_admin.css',
|
|
'extra_context': {'media': 'screen,projection'},
|
|
},
|
|
},
|
|
'COMPILERS': ('libsasscompiler.LibSassCompiler',),
|
|
'DISABLE_WRAPPER': True,
|
|
}
|
|
|
|
STATICFILES_STORAGE = 'pipeline.storage.PipelineManifestStorage'
|
|
|
|
STATICFILES_FINDERS = (
|
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
|
'pipeline.finders.PipelineFinder',
|
|
)
|
|
|
|
LOGGING = {
|
|
'version': 1,
|
|
'disable_existing_loggers': False,
|
|
'formatters': {
|
|
'default': {'format': '%(asctime)-15s %(levelname)8s %(name)s %(message)s'},
|
|
'verbose': {
|
|
'format': '%(asctime)-15s %(levelname)8s %(name)s %(process)d %(thread)d %(message)s'
|
|
},
|
|
},
|
|
'handlers': {
|
|
'console': {
|
|
'class': 'logging.StreamHandler',
|
|
'formatter': 'default', # Set to 'verbose' in production
|
|
'stream': 'ext://sys.stderr',
|
|
},
|
|
},
|
|
'loggers': {
|
|
'asyncio': {'level': 'WARNING'},
|
|
'django': {'level': 'WARNING'},
|
|
'urllib3': {'level': 'WARNING'},
|
|
'search': {'level': 'DEBUG'},
|
|
'static_assets': {'level': 'DEBUG'},
|
|
},
|
|
'root': {'level': 'WARNING', 'handlers': ['console']},
|
|
}
|
|
|
|
SITE_ID = 1
|
|
|
|
# Required by Django Debug Toolbar
|
|
INTERNAL_IPS = ['127.0.0.1']
|
|
|
|
|
|
TAGGIT_CASE_INSENSITIVE = True
|
|
|
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
|
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
|
|
PUBLIC_FILE_STORAGE = 'common.storage.S3PublicStorage'
|
|
# Do not set "public-read" ACL on bucket items
|
|
AWS_DEFAULT_ACL = None
|
|
AWS_S3_FILE_OVERWRITE = False
|
|
AWS_S3_REGION_NAME = _get('AWS_S3_REGION_NAME')
|
|
AWS_STORAGE_BUCKET_NAME = _get('AWS_STORAGE_BUCKET_NAME')
|
|
AWS_S3_CUSTOM_DOMAIN = _get('AWS_S3_CUSTOM_DOMAIN')
|
|
# Used for temporary storage when processing videos (and in the future
|
|
# when performing direct-to-s3 uploads). Once the upload is completed
|
|
# we take care of moving the file to AWS_STORAGE_BUCKET_NAME through a
|
|
# background task.
|
|
AWS_UPLOADS_BUCKET_NAME = _get('AWS_UPLOADS_BUCKET_NAME')
|
|
AWS_S3_OBJECT_PARAMETERS = {
|
|
# Set max-age to 10 days
|
|
'CacheControl': str('private,max-age=1728000'),
|
|
}
|
|
# In order to set the same headers for already existing S3 keys,
|
|
# --metadata-directive must be used, e.g.:
|
|
# aws s3 cp s3://blender-studio/ s3://blender-studio/ --exclude "*" --include "*.jpg" \
|
|
# --recursive --metadata-directive REPLACE --cache-control public,max-age=864000
|
|
|
|
THUMBNAIL_STORAGE = PUBLIC_FILE_STORAGE
|
|
THUMBNAIL_CROP_MODE = 'center'
|
|
THUMBNAIL_SIZE_S = '400x225'
|
|
THUMBNAIL_SIZE_M = '1280x720'
|
|
|
|
CSRF_COOKIE_NAME = 'bstudiocsrftoken'
|
|
|
|
ACTSTREAM_SETTINGS = {
|
|
'MANAGER': 'users.managers.CustomStreamManager',
|
|
'FETCH_RELATIONS': True,
|
|
}
|
|
|
|
ADMIN_MAIL = _get('ADMIN_MAIL', 'admin@studio')
|
|
STORE_PRODUCT_URL = _get('STORE_PRODUCT_URL')
|
|
STORE_MANAGE_URL = _get('STORE_MANAGE_URL')
|
|
|
|
SUPPORTED_CURRENCIES = {'EUR', 'USD'}
|
|
|
|
# Collection of automatically renewing subscriptions will be attempted this
|
|
# many times before giving up and setting the subscription status to 'on-hold'.
|
|
#
|
|
# This value is only used when automatic renewal fails, so setting it < 1 will
|
|
# be treated the same as 1 (one attempt is made, and failure is immediate, no
|
|
# retries).
|
|
LOOPER_CLOCK_MAX_AUTO_ATTEMPTS = 3
|
|
|
|
# Only retry collection of automatic renewals this long after the last failure.
|
|
# This separates the frequency of retrials from the frequency of the clock.
|
|
LOOPER_ORDER_RETRY_AFTER = relativedelta(days=2)
|
|
|
|
# The system user from looper/fixtures/systemuser.json. This user is required
|
|
# for logging things in the admin history (those log entries always need to
|
|
# have a non-NULL user ID).
|
|
LOOPER_SYSTEM_USER_ID = _get('LOOPER_SYSTEM_USER_ID', 1, int)
|
|
|
|
LOOPER_MONEY_LOCALE = 'en_US.UTF-8'
|
|
|
|
LOOPER_SUBSCRIPTION_CREATION_WARNING_THRESHOLD = relativedelta(days=1)
|
|
|
|
# Expire on-hold subscriptions after they haven't been paid for half a year
|
|
LOOPER_SUBSCRIPTION_EXPIRE_AFTER = timedelta(weeks=4 * 6)
|
|
LOOPER_ORDER_RECEIPT_PDF_URL = 'subscriptions:receipt-pdf'
|
|
LOOPER_PAY_EXISTING_ORDER_URL = 'subscriptions:pay-existing-order'
|
|
LOOPER_MANAGER_MAIL = ADMIN_MAIL
|
|
LOOPER_USER_SEARCH_FIELDS = ('user__full_name',)
|
|
|
|
|
|
REST_FRAMEWORK = {
|
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
|
'DEFAULT_PERMISSION_CLASSES': [
|
|
'rest_framework.permissions.IsAdminUser',
|
|
'rest_framework.permissions.DjangoModelPermissions',
|
|
],
|
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
'rest_framework.authentication.TokenAuthentication',
|
|
'rest_framework.authentication.SessionAuthentication',
|
|
],
|
|
'PAGE_SIZE': 10,
|
|
}
|
|
|
|
if TESTING:
|
|
STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'
|
|
AWS_STORAGE_BUCKET_NAME = 'blender-studio-test'
|
|
MEILISEARCH_INDEX_UID = 'test_studio'
|
|
TRAINING_INDEX_UID = 'test_training'
|
|
LOGGING = {
|
|
'version': 1,
|
|
'loggers': {
|
|
'': {'level': 'CRITICAL'},
|
|
},
|
|
}
|
|
|
|
SECRET_KEY = _get('SECRET_KEY')
|
|
DEBUG = _get('DEBUG', False, bool)
|
|
TEMPLATE_DEBUG = DEBUG
|
|
# Enable to use OAuth without https during local development
|
|
|
|
MIDDLEWARE += [
|
|
'pyinstrument.middleware.ProfilerMiddleware',
|
|
]
|
|
|
|
|
|
def custom_show_pyinstrument(request):
|
|
return request.user.is_superuser
|
|
|
|
|
|
PYINSTRUMENT_SHOW_CALLBACK = "%s.custom_show_pyinstrument" % __name__
|
|
|
|
if DEBUG:
|
|
MIDDLEWARE = [
|
|
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
|
] + MIDDLEWARE
|
|
INSTALLED_APPS += [
|
|
'debug_toolbar',
|
|
]
|
|
TEMPLATE_STRING_IF_INVALID = 'DEBUG WARNING: undefined template variable [%s] not found'
|
|
DEBUG_TOOLBAR_CONFIG = {
|
|
'PROFILER_MAX_DEPTH': 20,
|
|
'SQL_WARNING_THRESHOLD': 100, # milliseconds
|
|
}
|
|
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
|
|
|
ALLOWED_HOSTS: List[str] = _get('ALLOWED_HOSTS', '', str).split(',')
|
|
|
|
BLENDER_ID = {
|
|
# MUST end in a slash:
|
|
"BASE_URL": _get('BID_BASE_URL', "https://id.blender.org/"),
|
|
"OAUTH_CLIENT": _get('BID_OAUTH_CLIENT'),
|
|
"OAUTH_SECRET": _get('BID_OAUTH_SECRET'),
|
|
"WEBHOOK_USER_MODIFIED_SECRET": (_get('BID_WEBHOOK_USER_MODIFIED_SECRET', '') or '').encode(),
|
|
# Credentials linked to a Blender ID system cloud_badger user, for updating subscriber badges
|
|
"BADGER_API_OAUTH_CLIENT": _get('BADGER_API_OAUTH_CLIENT'),
|
|
"BADGER_API_OAUTH_SECRET": _get('BADGER_API_OAUTH_SECRET'),
|
|
"BADGER_API_ACCESS_TOKEN": _get('BADGER_API_ACCESS_TOKEN'),
|
|
}
|
|
|
|
DEFAULT_DATABASE_URL = 'postgres://studio:studio@localhost:5432/studio'
|
|
DATABASES = {
|
|
'default': dj_database_url.config(
|
|
default=DEFAULT_DATABASE_URL,
|
|
conn_max_age=600,
|
|
),
|
|
}
|
|
|
|
# Braintree configuration
|
|
# Provide merchant accounts in the following format:
|
|
# `CURRENCY_CODE:ACCOUNT_ID,CURRENCY_CODE:ACCOUNT_ID`
|
|
# where comma separates multiple merchant accounts
|
|
_BT_MERCHANT_ACCOUNTS = _get('BT_MERCHANT_ACCOUNTS', '', str)
|
|
BT_MERCHANT_ACCOUNTS = _BT_MERCHANT_ACCOUNTS.split(',') if _BT_MERCHANT_ACCOUNTS else []
|
|
BT_ENVIRONMENT = _get('BT_ENVIRONMENT', 'Sandbox') # Sandbox or Production
|
|
GATEWAYS = {
|
|
'braintree': {
|
|
'environment': getattr(braintree.Environment, BT_ENVIRONMENT),
|
|
'merchant_id': _get('BT_MERCHANT_ID'),
|
|
'public_key': _get('BT_PUBLIC_KEY'),
|
|
'private_key': _get('BT_PRIVATE_KEY'),
|
|
'merchant_account_ids': dict(acc.split(':') for acc in BT_MERCHANT_ACCOUNTS),
|
|
'supported_collection_methods': {'automatic', 'manual'},
|
|
},
|
|
'bank': {'supported_collection_methods': {'manual'}},
|
|
}
|
|
|
|
# Optional Sentry configuration
|
|
|
|
SENTRY_DSN = _get('SENTRY_DSN')
|
|
if SENTRY_DSN:
|
|
import sentry_sdk
|
|
from sentry_sdk.integrations.django import DjangoIntegration
|
|
|
|
sentry_sdk.init(
|
|
dsn=SENTRY_DSN,
|
|
integrations=[DjangoIntegration()],
|
|
traces_sample_rate=1.0,
|
|
# If you wish to associate users to errors (assuming you are using
|
|
# django.contrib.auth) you may enable sending PII data.
|
|
send_default_pii=False,
|
|
# Looks like IP address is also not sent when this is False.
|
|
)
|
|
|
|
# Meilisearch configuration
|
|
MEILISEARCH_INDEX_UID = 'studio'
|
|
TRAINING_INDEX_UID = 'training'
|
|
MEILI_MASTER_KEY = _get('MEILI_MASTER_KEY')
|
|
MEILISEARCH_PUBLIC_KEY = _get('MEILISEARCH_PUBLIC_KEY')
|
|
MEILISEARCH_API_ADDRESS = _get('MEILISEARCH_API_ADDRESS')
|
|
SEARCH_CLIENT = meilisearch.Client(MEILISEARCH_API_ADDRESS, MEILI_MASTER_KEY)
|
|
|
|
DEFAULT_RANKING_RULES = [
|
|
'typo',
|
|
'words',
|
|
'proximity',
|
|
'attribute',
|
|
'exactness',
|
|
]
|
|
DATE_DESC_RANKING_RULES = ['sort', 'timestamp:desc', *DEFAULT_RANKING_RULES]
|
|
DATE_ASC_RANKING_RULES = ['sort', 'timestamp:asc', *DEFAULT_RANKING_RULES]
|
|
MAIN_SEARCH = {
|
|
'SEARCHABLE_ATTRIBUTES': [
|
|
'model',
|
|
'name',
|
|
'film_title',
|
|
'tags',
|
|
'secondary_tags',
|
|
'topic',
|
|
'collection_name',
|
|
'chapter_name',
|
|
'description',
|
|
'summary',
|
|
'content',
|
|
'author_name',
|
|
],
|
|
'SORTABLE_ATTRIBUTES': [
|
|
'timestamp',
|
|
'date_created',
|
|
'date_updated',
|
|
'date_published',
|
|
],
|
|
'FACETING_ATTRIBUTES': ['model', 'film_title', 'license', 'media_type', 'free'],
|
|
'RANKING_RULES': {
|
|
MEILISEARCH_INDEX_UID: DEFAULT_RANKING_RULES,
|
|
f'{MEILISEARCH_INDEX_UID}_date_desc': DATE_DESC_RANKING_RULES,
|
|
f'{MEILISEARCH_INDEX_UID}_date_asc': DATE_ASC_RANKING_RULES,
|
|
},
|
|
}
|
|
TRAINING_SEARCH = {
|
|
'SEARCHABLE_ATTRIBUTES': [
|
|
'model',
|
|
'name',
|
|
'training_name',
|
|
'tags',
|
|
'secondary_tags',
|
|
'chapter_name',
|
|
'description',
|
|
'summary',
|
|
'author_name',
|
|
],
|
|
'SORTABLE_ATTRIBUTES': [
|
|
'timestamp',
|
|
'date_created',
|
|
'date_updated',
|
|
'date_published',
|
|
],
|
|
'FACETING_ATTRIBUTES': ['type', 'difficulty'],
|
|
'RANKING_RULES': {
|
|
TRAINING_INDEX_UID: DEFAULT_RANKING_RULES,
|
|
f'{TRAINING_INDEX_UID}_date_desc': DATE_DESC_RANKING_RULES,
|
|
f'{TRAINING_INDEX_UID}_date_asc': DATE_ASC_RANKING_RULES,
|
|
},
|
|
}
|
|
|
|
# AWS configuration
|
|
|
|
AWS_ACCESS_KEY_ID = _get('AWS_ACCESS_KEY_ID')
|
|
AWS_SECRET_ACCESS_KEY = _get('AWS_SECRET_ACCESS_KEY')
|
|
AWS_CLOUDFRONT_KEY_ID = _get('AWS_CLOUDFRONT_KEY_ID')
|
|
if AWS_CLOUDFRONT_KEY_ID:
|
|
with open(BASE_DIR / f'pk-{AWS_CLOUDFRONT_KEY_ID}.pem', 'rb') as f:
|
|
AWS_CLOUDFRONT_KEY = f.read()
|
|
|
|
BLENDER_CLOUD_SECRET_KEY = _get('BLENDER_CLOUD_SECRET_KEY')
|
|
BLENDER_CLOUD_AUTH_ENABLED = _get('BLENDER_CLOUD_AUTH_ENABLED', False, bool)
|
|
BLENDER_CLOUD_DOMAIN = _get('BLENDER_CLOUD_DOMAIN')
|
|
|
|
COCONUT_API_KEY = _get('COCONUT_API_KEY')
|
|
COCONUT_DECLARED_HOSTNAME = _get('COCONUT_DECLARED_HOSTNAME')
|
|
|
|
if _get('DEFAULT_FROM_EMAIL'):
|
|
DEFAULT_FROM_EMAIL = _get('DEFAULT_FROM_EMAIL')
|
|
MAILGUN_API_KEY = _get('MAILGUN_API_KEY')
|
|
MAILGUN_SENDER_DOMAIN = _get('MAILGUN_SENDER_DOMAIN')
|
|
NEWSLETTER_LIST = _get('NEWSLETTER_LIST')
|
|
NEWSLETTER_NONSUBSCRIBER_LIST = _get('NEWSLETTER_NONSUBSCRIBER_LIST')
|
|
NEWSLETTER_SUBSCRIBER_LIST = _get('NEWSLETTER_SUBSCRIBER_LIST')
|
|
# By default, dump emails to the console instead of trying to actually send them.
|
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
|
if MAILGUN_SENDER_DOMAIN:
|
|
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"
|
|
ANYMAIL = {
|
|
"MAILGUN_SENDER_DOMAIN": MAILGUN_SENDER_DOMAIN,
|
|
"MAILGUN_WEBHOOK_SIGNING_KEY": _get('MAILGUN_WEBHOOK_SIGNING_KEY'),
|
|
'WEBHOOK_SECRET': _get('MAILGUN_WEBHOOK_SECRET'),
|
|
}
|
|
|
|
GEOIP2_DB = _get('GEOIP2_DB')
|
|
|
|
GOOGLE_ANALYTICS_TRACKING_ID = _get('GOOGLE_ANALYTICS_TRACKING_ID')
|
|
GOOGLE_RECAPTCHA_SECRET_KEY = _get('GOOGLE_RECAPTCHA_SECRET_KEY')
|
|
GOOGLE_RECAPTCHA_SITE_KEY = _get('GOOGLE_RECAPTCHA_SITE_KEY')
|
|
|
|
S3DIRECT_DESTINATIONS = {
|
|
'default': {
|
|
# "key" [required] The location to upload file
|
|
# 1. String: folder path to upload to
|
|
# 2. Function: generate folder path + filename using a function
|
|
'key': lambda filename: str(common.upload_paths.get_upload_to_hashed_path(None, filename)),
|
|
|
|
# "auth" [optional] Limit to specfic Django users
|
|
# Function: ACL function
|
|
'auth': lambda user: user.is_staff,
|
|
|
|
# "allowed" [optional] Limit to specific mime types
|
|
# List: list of mime types
|
|
# 'allowed': ['image/jpeg', 'image/png', 'video/mp4'],
|
|
|
|
# "bucket" [optional] Bucket if different from AWS_STORAGE_BUCKET_NAME
|
|
# String: bucket name
|
|
# 'bucket': AWS_STORAGE_BUCKET_NAME,
|
|
|
|
# "endpoint" [optional] Endpoint if different from AWS_S3_ENDPOINT_URL
|
|
# String: endpoint URL
|
|
# More info at http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
|
|
'endpoint': f'https://s3.{AWS_S3_REGION_NAME}.amazonaws.com',
|
|
|
|
# "acl" [optional] Custom ACL for object, default is 'public-read'
|
|
# String: ACL
|
|
'acl': 'private',
|
|
|
|
# "cache_control" [optional] Custom cache control header
|
|
# String: header
|
|
'cache_control': 'max-age=2592000',
|
|
|
|
# "content_disposition" [optional] Custom content disposition header
|
|
# String: header
|
|
'content_disposition': lambda x: 'attachment; filename="{}"'.format(x),
|
|
|
|
# "allow_existence_optimization" [optional] Checks to see if file already exists,
|
|
# returns the URL to the object if so (no upload)
|
|
# Boolean: True, False
|
|
'allow_existence_optimization': True,
|
|
},
|
|
}
|