openintel/settings.py
"""
Django settings for openintel project.
Generated by 'django-admin startproject' using Django 1.10.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
# http://bruno.im/2013/may/18/django-stop-writing-settings-files/
import logging
import os
import sys
import dj_database_url
import environ
from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
def _detect_command(cmd):
"""Return True iff the user is running the specified Django admin command."""
# https://stackoverflow.com/questions/4088253/django-how-to-detect-test-environment
return len(sys.argv) > 1 and sys.argv[1] == cmd
TESTING = _detect_command("test") or "pytest" in sys.modules
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
# Set defaults and read .env file from root directory
# Defaults should be "safe" from a security perspective
# NOTE: 'env' follows naming convention from the environ documentation
env = environ.Env( # pylint: disable=invalid-name
DEBUG=(bool, False),
ENABLE_CACHE=(bool, True),
ENVIRONMENT_NAME=(str, None),
DJANGO_LOG_LEVEL=(str, "ERROR"),
APP_LOG_LEVEL=(str, "ERROR"),
CERTBOT_PUBLIC_KEY=(str, None),
CERTBOT_SECRET_KEY=(str, None),
SESSION_COOKIE_SECURE=(bool, True),
CSRF_COOKIE_SECURE=(bool, True),
CSRF_COOKIE_HTTPONLY=(bool, True),
X_FRAME_OPTIONS=(str, "DENY"),
ALLOWED_HOSTS=(str, "*"),
SECURE_SSL_REDIRECT=(bool, True),
SECURE_BROWSER_XSS_FILTER=(bool, True),
SECURE_CONTENT_TYPE_NOSNIFF=(bool, True),
SECURE_HSTS_INCLUDE_SUBDOMAINS=(bool, True),
SECURE_HSTS_SECONDS=(int, 31536000), # default to maximum age in seconds
ROLLBAR_ACCESS_TOKEN=(str, None),
ROLLBAR_ENABLED=(bool, False),
ACCOUNT_REQUIRED=(bool, False),
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_EMAIL_REQUIRED=(bool, True),
ACCOUNT_EMAIL_VERIFICATION=(str, "mandatory"), # 'mandatory', 'optional', or 'none'
EVIDENCE_REQUIRE_SOURCE=(bool, True),
EDIT_REMOVE_ENABLED=(bool, True),
INVITE_REQUIRED=(bool, False),
SENDGRID_USERNAME=(str, None),
SENDGRID_API_KEY=(str, None),
SLUG_MAX_LENGTH=(int, 72),
TWITTER_ACCOUNT=(str, None),
DONATE_BITCOIN_ADDRESS=(str, None),
INVITE_REQUEST_URL=(str, None),
BANNER_MESSAGE=(str, None),
PRIVACY_URL=(str, None),
DIGEST_WEEKLY_DAY=(int, 0), # default to Monday
CELERY_ALWAYS_EAGER=(bool, False),
RECAPTCHA_PUBLIC_KEY=(str, None),
RECAPTCHA_PRIVATE_KEY=(str, None),
)
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env("DJANGO_SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DEBUG")
# https://docs.djangoproject.com/en/1.10/ref/settings/#std:setting-ALLOWED_HOSTS
# NOTE: 'env' will return a value because a default is defined for 'ALLOWED_HOSTS'
ALLOWED_HOSTS = env("ALLOWED_HOSTS").split(",") # pylint: disable=no-member
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sites",
"django.contrib.sitemaps",
"django_comments",
"webpack_loader",
"field_history",
"bootstrapform",
"openach",
"allauth",
"allauth.account",
"allauth.socialaccount",
# notifications must appear after applications generating notifications
"notifications",
# invitations must appear after allauth: https://github.com/bee-keeper/django-invitations#allauth-integration
"invitations",
]
# See https://docs.djangoproject.com/en/2.1/topics/http/middleware/
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
# whitenoise should be placed directly after the security middleware
# http://whitenoise.evans.io/en/stable/django.html#enable-whitenoise
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
# LocaleMiddleware must come after SessionMiddleware and before CommonMiddleware
# see: https://docs.djangoproject.com/en/1.10/topics/i18n/translation/#how-django-discovers-language-preference
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"field_history.middleware.FieldHistoryMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.gzip.GZipMiddleware",
"csp.middleware.CSPMiddleware",
# Rollbar middleware needs to be last so it can wrap the exceptions
"rollbar.contrib.django.middleware.RollbarNotifierMiddleware",
]
INTERNAL_IPS = [
"127.0.0.1",
]
if DEBUG and not TESTING:
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
# Configure N+1 detection during DEBUG and TESTING; see https://github.com/jmcarp/nplusone
if DEBUG or TESTING:
INSTALLED_APPS.insert(0, "nplusone.ext.django")
MIDDLEWARE.insert(
0,
"nplusone.ext.django.NPlusOneMiddleware",
)
if TESTING:
# http://whitenoise.evans.io/en/latest/django.html#whitenoise-makes-my-tests-run-slow
WHITENOISE_AUTOREFRESH = True
NPLUSONE_RAISE = TESTING
ROOT_URLCONF = "openintel.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "templates")],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"openach.context_processors.site",
"openach.context_processors.meta",
"openach.context_processors.invite",
"openach.context_processors.banner",
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.request",
],
# Template debugging is required for coverage testing
"debug": DEBUG,
},
},
]
WSGI_APPLICATION = "openintel.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
)
# Password validation
# https://docs.djangoproject.com/en/1.10/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",
},
]
# https://docs.djangoproject.com/en/1.10/topics/security/#ssl-https
SESSION_COOKIE_SECURE = env("SESSION_COOKIE_SECURE")
CSRF_COOKIE_SECURE = env("CSRF_COOKIE_SECURE")
CSRF_COOKIE_HTTPONLY = env("CSRF_COOKIE_SECURE")
X_FRAME_OPTIONS = env("X_FRAME_OPTIONS")
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = env("SECURE_SSL_REDIRECT")
SECURE_BROWSER_XSS_FILTER = env("SECURE_BROWSER_XSS_FILTER")
SECURE_CONTENT_TYPE_NOSNIFF = env("SECURE_CONTENT_TYPE_NOSNIFF")
SECURE_HSTS_INCLUDE_SUBDOMAINS = env("SECURE_HSTS_INCLUDE_SUBDOMAINS")
SECURE_HSTS_SECONDS = env("SECURE_HSTS_SECONDS")
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGES = (
# Add new locales in LANGUAGES variable, e.g., ('az', _('Azerbaijani'))
("en", _("English (United States)")),
)
LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# https://docs.djangoproject.com/en/1.10/ref/contrib/sites/
SITE_ID = 1
# Update database configuration with $DATABASE_URL.
# XXX: migrate db config to use environ library?
# NOTE: 'db_from_env' follows naming convention from the Heroku documentation
db_from_env = dj_database_url.config(conn_max_age=500) # pylint: disable=invalid-name
DATABASES["default"].update(db_from_env)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_ROOT = os.path.join(PROJECT_ROOT, "staticfiles")
STATIC_URL = "/static/"
# Extra places for collectstatic to find static files.
STATICFILES_DIRS = (
os.path.join(PROJECT_ROOT, "static"),
os.path.join(BASE_DIR, "openach-frontend", "bundles"),
)
# Simplified static file serving.
# https://warehouse.python.org/project/whitenoise/
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# https://github.com/owais/django-webpack-loader#default-configuration
WEBPACK_LOADER = {
"DEFAULT": {
"CACHE": not DEBUG,
"BUNDLE_DIR_NAME": "/",
"STATS_FILE": os.path.join(BASE_DIR, "webpack-stats.json"),
}
}
# Logging configuration
# cf. https://chrxr.com/django-error-logging-configuration-heroku/
# cf. https://stackoverflow.com/questions/18920428/django-logging-on-heroku
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": (
"%(asctime)s [%(process)d] [%(levelname)s] "
+ "pathname=%(pathname)s lineno=%(lineno)s "
+ "funcname=%(funcName)s %(message)s"
),
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"simple": {"format": "%(levelname)s %(message)s"},
},
"handlers": {
"null": {
"level": "DEBUG",
"class": "logging.NullHandler",
},
"console": {"class": "logging.StreamHandler", "formatter": "verbose"},
},
"loggers": {
"django": {
"handlers": ["console"],
"level": env("DJANGO_LOG_LEVEL"),
},
"openach": {
"handlers": ["console"],
"level": env("APP_LOG_LEVEL"),
},
},
}
# Email Options using sendgrid-django or smtp
# https://sendgrid.com/docs/for-developers/sending-email/django/
if env("SENDGRID_API_KEY"): # pragma: no cover
EMAIL_BACKEND = "sendgrid_backend.SendgridBackend"
SENDGRID_API_KEY = env("SENDGRID_API_KEY")
elif env("SENDGRID_USERNAME"):
EMAIL_HOST = "smtp.sendgrid.net"
EMAIL_HOST_USER = env("SENDGRID_USERNAME")
EMAIL_HOST_PASSWORD = env("SENDGRID_PASSWORD")
EMAIL_PORT = 587
EMAIL_USE_TLS = True
else:
logger.warning("SendGrid not configured: SENDGRID_API_KEY or SENDGRID_USERNAME")
# Instance configuration
SITE_NAME = env("SITE_NAME")
SITE_DOMAIN = env("SITE_DOMAIN")
ACCOUNT_REQUIRED = env("ACCOUNT_REQUIRED")
if _detect_command("createadmin"): # pragma: no cover
# Load the admin credentials if the admin is creating a default account (e.g., when deploying with Heroku button)
ADMIN_USERNAME = env("ADMIN_USERNAME")
ADMIN_PASSWORD = env("ADMIN_PASSWORD")
ADMIN_EMAIL_ADDRESS = env("ADMIN_EMAIL_ADDRESS")
PRIVACY_URL = env("PRIVACY_URL")
INVITE_REQUIRED = env("INVITE_REQUIRED")
INVITE_REQUEST_URL = env("INVITE_REQUEST_URL")
EVIDENCE_REQUIRE_SOURCE = env("EVIDENCE_REQUIRE_SOURCE")
EDIT_REMOVE_ENABLED = env("EDIT_REMOVE_ENABLED")
TWITTER_ACCOUNT = env("TWITTER_ACCOUNT")
DONATE_BITCOIN_ADDRESS = env("DONATE_BITCOIN_ADDRESS")
BANNER_MESSAGE = env("BANNER_MESSAGE")
DIGEST_WEEKLY_DAY = env("DIGEST_WEEKLY_DAY")
if env("ENVIRONMENT_NAME"):
ENVIRONMENT_NAME = env("ENVIRONMENT_NAME")
elif DEBUG:
ENVIRONMENT_NAME = "development"
else:
ENVIRONMENT_NAME = "production"
# Authentication Options:
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_EMAIL_REQUIRED = env("ACCOUNT_EMAIL_REQUIRED")
ACCOUNT_EMAIL_VERIFICATION = env("ACCOUNT_EMAIL_VERIFICATION")
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
# https://stackoverflow.com/questions/22700041/django-allauth-sends-verification-emails-from-webmasterservername
DEFAULT_FROM_EMAIL = env.get_value("DEFAULT_FROM_EMAIL", default=ADMIN_EMAIL_ADDRESS)
ACCOUNT_ADAPTER = "openach.account_adapters.AccountAdapter"
ACCOUNT_SIGNUP_FORM_CLASS = "openach.forms.SignupForm"
# Invitations Options:
# https://github.com/bee-keeper/django-invitations#additional-configuration
INVITATIONS_INVITATION_ONLY = INVITE_REQUIRED
INVITATIONS_ADAPTER = ACCOUNT_ADAPTER
# Challenge/Response for Let's Encrypt. In the future, we may want to support challenge/response for multiple domains.
CERTBOT_PUBLIC_KEY = env("CERTBOT_PUBLIC_KEY")
CERTBOT_SECRET_KEY = env("CERTBOT_SECRET_KEY")
# Rollbar Error tracking: https://rollbar.com/docs/notifier/pyrollbar/#django
# Rollbar endpoint via 'endpoint' configuration is not working. For now just use the default.
ROLLBAR = {
"enabled": env("ROLLBAR_ENABLED") and not TESTING,
"access_token": env("ROLLBAR_ACCESS_TOKEN"),
"environment": ENVIRONMENT_NAME,
"root": PROJECT_ROOT,
"branch": "master",
}
# Content Security Policy (CSP) Header configuration
# https://django-csp.readthedocs.io/en/latest/configuration.html
# http://www.html5rocks.com/en/tutorials/security/content-security-policy/
CSP_DEFAULT_SRC = ["'self'"]
CSP_OBJECT_SRC = ["'none'"]
# SEO Configuration
SLUG_MAX_LENGTH = env("SLUG_MAX_LENGTH")
def _get_cache():
if env("ENABLE_CACHE") and not TESTING:
# https://devcenter.heroku.com/articles/django-memcache#configure-django-with-memcachier
try:
logger.info("Using MEMCACHIER servers: %s", env("MEMCACHIER_SERVERS"))
return {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",
"LOCATION": env("MEMCACHIER_SERVERS"),
# TIMEOUT is not the connection timeout! It's the default expiration
# timeout that should be applied to keys! Setting it to `None`
# disables expiration.
"TIMEOUT": 500,
"OPTIONS": {
"binary": True,
"username": env("MEMCACHIER_USERNAME"),
"password": env("MEMCACHIER_PASSWORD"),
},
}
}
except: # pylint: disable=bare-except
# NOTE: bare except clause is OK here because all exceptions would be caused by a bad/missing configuration
logger.warning("MEMCACHIER not configured; using local memory cache")
return {
"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}
}
else:
# https://docs.djangoproject.com/en/1.10/topics/cache/#dummy-caching-for-development
logger.info("ENABLE_CACHE not set; using DummyCache")
return {
"default": {
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
}
}
# https://docs.djangoproject.com/en/1.10/topics/cache/
CACHES = _get_cache()
# http://docs.celeryproject.org/en/latest/configuration.html
# XXX: not sure why these have to be declared globally in addition to the celery app setup
CELERY_TASK_SERIALIZER = "json"
CELERY_ACCEPT_CONTENT = ["json"] # set globally for safety
CELERY_RESULT_SERIALIZER = "json"
if TESTING:
logger.info("Enabling CELERY_ALWAYS_EAGER for testing")
CELERY_ALWAYS_EAGER = True
else:
CELERY_ALWAYS_EAGER = env("CELERY_ALWAYS_EAGER")
if env.get_value("CELERY_BROKER_URL", cast=str, default=None):
CELERY_BROKER_URL = env("CELERY_BROKER_URL")
CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND")
elif env.get_value("REDIS_URL", cast=str, default=None):
logger.info(
"No CELERY_BROKER_URL specified, using REDIS_URL for Celery broker and result backend"
)
CELERY_BROKER_URL = env("REDIS_URL")
CELERY_RESULT_BACKEND = env("REDIS_URL")
PAGE_CACHE_TIMEOUT_SECONDS = 60
BOARD_SEARCH_RESULTS_MAX = 5
# Google Recaptcha support: https://pypi.org/project/django-recaptcha/
RECAPTCHA_PUBLIC_KEY = env("RECAPTCHA_PUBLIC_KEY")
RECAPTCHA_PRIVATE_KEY = env("RECAPTCHA_PRIVATE_KEY")
if RECAPTCHA_PUBLIC_KEY:
logger.info("ReCAPTCHA is enabled")
INSTALLED_APPS += ["captcha"]
else:
logger.info("ReCAPTCHA NOT enabled")