"""Cobalt Settings.py
This is the single settings.py for all Cobalt environments.
We manage all configuration differences through environment variables.
This provides security for confidential information in the online
environments (Test, UAT and Production) which is managed by Elastic
Beanstalk through settings which become environment at run-time.
For development, you also need to set environment variables, or it
won't work.
"""
import os
import ast
from django.contrib.messages import constants as messages
from firebase_admin import initialize_app
# Hide development server warning
# https://docs.djangoproject.com/en/stable/ref/django-admin/#envvar-DJANGO_RUNSERVER_HIDE_WARNING
os.environ["DJANGO_RUNSERVER_HIDE_WARNING"] = "true"
###########################################
# function to set values from environment #
# variables. #
###########################################
[docs]
def set_value(val_name, default="not-set"):
return os.environ[val_name] if val_name in os.environ else default
###########################################
# base settings that need to come first. #
###########################################
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
###########################################
# Specific settings per environment. #
# Override through environment variables. #
# Dummy values are required for read the #
# docs to work. #
###########################################
# basics
SECRET_KEY = set_value("SECRET_KEY")
DEBUG = os.environ.get("DEBUG") == "ON"
API_KEY_PREFIX = set_value("API_KEY_PREFIX", "API_")
# Set up ADMINS list from string
ADMINS = [
("Mark Guthrie", "m@rkguthrie.au"),
# ("Julian Foster", "Julian.Foster@abf.com.au"),
]
# COB-488 - require 2FA for Admin site access (Y or N)
REQUIRE_2FA = set_value("REQUIRE_2FA", "N") == "Y"
SERVER_EMAIL = set_value("SERVER_EMAIL", "notset@myabf.com.au")
# masterpoints server
GLOBAL_MPSERVER = set_value("GLOBAL_MPSERVER")
# email
EMAIL_HOST = set_value("EMAIL_HOST")
EMAIL_HOST_USER = set_value("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = set_value("EMAIL_HOST_PASSWORD")
DEFAULT_FROM_EMAIL = set_value("DEFAULT_FROM_EMAIL", "notset@fake.com")
# TODO: SUPPORT_EMAIL is only used to send client side errors - replace with ADMINS
SUPPORT_EMAIL = set_value("SUPPORT_EMAIL", ["success@simulator.amazonses.com"])
# playpen - don't send emails from non-prod systems
DISABLE_PLAYPEN = set_value("DISABLE_PLAYPEN", "OFF")
# stripe
STRIPE_SECRET_KEY = set_value("STRIPE_SECRET_KEY")
STRIPE_PUBLISHABLE_KEY = set_value("STRIPE_PUBLISHABLE_KEY")
STRIPE_WEBHOOK_SECRET = set_value("STRIPE_WEBHOOK_SECRET", "")
# aws
AWS_ACCESS_KEY_ID = set_value("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = set_value("AWS_SECRET_ACCESS_KEY")
AWS_REGION_NAME = set_value("AWS_REGION_NAME")
AWS_SES_REGION_NAME = AWS_REGION_NAME
AWS_SES_REGION_ENDPOINT = set_value("AWS_SES_REGION_ENDPOINT")
AWS_SES_CONFIGURATION_SET = set_value("AWS_SES_CONFIGURATION_SET")
# Set this to false so we don't need to install m2crypto which needs OS installs to work
# Not verifying the certificate is lower risk than having us rely on an OS install
AWS_SES_VERIFY_EVENT_SIGNATURES = False
# our logical hostname (dev, test, uat, prod)
COBALT_HOSTNAME = set_value("COBALT_HOSTNAME", "127.0.0.1:8000")
# Hostname set by AWS
HOSTNAME = set_value("HOSTNAME", "Unknown")
# New Relic App ID - Test, UAT and Prod all have their own IDs. Default to Dev
NEW_RELIC_APP_ID = set_value("NEW_RELIC_APP_ID", "601323710")
# Masterpoint source
MP_USE_FILE = set_value("MP_USE_FILE", None)
MP_USE_DJANGO = set_value("MP_USE_DJANGO", None)
# database
RDS_DB_NAME = set_value("RDS_DB_NAME")
RDS_USERNAME = set_value("RDS_USERNAME")
RDS_PASSWORD = set_value("RDS_PASSWORD")
RDS_HOSTNAME = set_value("RDS_HOSTNAME")
RDS_PORT = set_value("RDS_PORT")
USE_SQLITE = set_value("USE_SQLITE", 0)
# xero
XERO_CLIENT_ID = set_value("XERO_CLIENT_ID")
XERO_CLIENT_SECRET = set_value("XERO_CLIENT_SECRET")
XERO_WEBHOOK_KEY = set_value("XERO_WEBHOOK_KEY")
XERO_BANK_ACCOUNT_CODE = set_value("XERO_BANK_ACCOUNT_CODE", "not-set")
# XERO_PAYABLE_ACCOUNT_CODE. When creating an ACCPAY invoice line item, Xero needs to know
# which GL account to classify the expense/payable against in the chart of
# accounts (e.g. a "Club Settlement Payouts" Current Liability account). This is
# separate from XERO_BANK_ACCOUNT_CODE, which identifies the physical bank account
# that money flows through. You create/identify the account in Xero's chart of accounts.
XERO_PAYABLE_ACCOUNT_CODE = set_value("XERO_PAYABLE_ACCOUNT_CODE", "not-set")
# XERO_PAYABLE_TAX_TYPE. The Xero tax type applied to ACCPAY settlement invoice
# line items. Xero requires a TaxType on ACCPAY line items. Use the tax type code
# from your Xero chart of accounts — typically "NOTAX" (BAS Excluded) for
# settlement disbursements. Check Xero Settings > Tax Rates for valid codes.
XERO_PAYABLE_TAX_TYPE = set_value("XERO_PAYABLE_TAX_TYPE", "NOTAX")
# XERO_FEE_ACCOUNT_CODE. GL account code for the processing-fee recovery line on the
# ACCREC fee invoice. This must be a Revenue/Income-type account in Xero so that it
# accepts OUTPUT (GST on Income) as its tax type. Liability accounts such as the
# XERO_PAYABLE_ACCOUNT_CODE cannot accept OUTPUT tax, so a separate income account
# is required here.
XERO_FEE_ACCOUNT_CODE = set_value("XERO_FEE_ACCOUNT_CODE", "not-set")
# XERO_FEE_TAX_TYPE. Tax type applied to the processing-fee recovery line on the
# ACCREC fee invoice. Typically "OUTPUT" (GST on Income, 10%) for Australian entities.
# Check Xero Settings > Tax Rates for valid codes.
XERO_FEE_TAX_TYPE = set_value("XERO_FEE_TAX_TYPE", "OUTPUT")
# XERO_ALERT_EMAILS. Comma-separated list of email addresses to notify when a
# Xero API call fails completely (after retries). Leave unset or empty to
# disable alerts. Example: "admin@example.com,finance@example.com"
XERO_BRANDING_THEME_ID = set_value("XERO_BRANDING_THEME_ID", "")
_xero_alert_emails_raw = set_value("XERO_ALERT_EMAILS", "")
XERO_ALERT_EMAILS = (
[e.strip() for e in _xero_alert_emails_raw.split(",") if e.strip()]
if _xero_alert_emails_raw
else []
)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": RDS_DB_NAME,
"USER": RDS_USERNAME,
"PASSWORD": RDS_PASSWORD,
"HOST": RDS_HOSTNAME,
"PORT": RDS_PORT,
}
}
# Test Only - Dummy data count
DUMMY_DATA_COUNT = int(set_value("DUMMY_DATA_COUNT", 20))
# Maintenance mode setting used by cobalt.middleware
# Set this to the string "ON" to put site into maintenance mode - only admins can login
MAINTENANCE_MODE = set_value("MAINTENANCE_MODE", "OFF")
# Recaptcha keys
RECAPTCHA_SITE_KEY = set_value("RECAPTCHA_SITE_KEY")
RECAPTCHA_SECRET_KEY = set_value("RECAPTCHA_SECRET_KEY")
# Unregistered users don't have emails, it is a close call between making email optional
# or using an email address to identify that we haven't set it.
# It is more likely to cause problems by making email optional.
UNREGISTERED_USER_NOT_SET_EMAIL = "noemail@notset.com"
#########################################
# Dynamic settings. #
#########################################
ALLOWED_HOSTS = [
"myabf.com.au",
".myabf.com.au",
"127.0.0.1",
"bs-local.com",
"localhost",
"testserver",
".eba-4ngvp62w.ap-southeast-2.elasticbeanstalk.com",
]
# In development, allow any connections
if COBALT_HOSTNAME == "127.0.0.1:8000":
ALLOWED_HOSTS = ["*"]
# For AWS we also need to add the local IP address as this is used by the health checks
# We do this dynamically
# Windows doesn't support this and isn't used on AWS so skip unless Unix
if os.name == "posix":
local_ip = os.popen("hostname -I 2>/dev/null").read()
ALLOWED_HOSTS.append(local_ip.strip())
#########################################
# Common settings for all environments #
#########################################
INSTALLED_APPS = [
# Our apps
"calendar_app",
"api",
"notifications",
"events",
"forums",
"masterpoints",
"payments",
"support",
"accounts",
"dashboard",
"results",
"organisations",
"logs",
"rbac",
"cobalt",
"club_sessions",
"utils",
"tests",
"xero",
# Django contrib
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.humanize",
"django.contrib.postgres",
"django.contrib.admindocs",
# 3rd party apps
"django_summernote",
"crispy_forms",
"crispy_bootstrap4",
"widget_tweaks",
"django_extensions",
"post_office",
"django_ses",
"django_otp",
"django_otp.plugins.otp_totp",
"loginas",
"fcm_django",
"pglock",
"pgactivity",
]
MIDDLEWARE = [
"utils.middleware.CobaltMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"cobalt.middleware.MaintenanceModeMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_otp.middleware.OTPMiddleware",
]
# Use middleware on non-prod systems to show errors
if DEBUG:
MIDDLEWARE.append("cobalt.middleware.CobaltLog500ErrorsMiddleware")
ROOT_URLCONF = "cobalt.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": ["cobalt/templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"cobalt.context_processors.global_settings",
"loginas.context_processors.impersonated_session_status",
],
},
},
{
"BACKEND": "post_office.template.backends.post_office.PostOfficeTemplates",
"APP_DIRS": True,
"DIRS": [],
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.template.context_processors.request",
]
},
},
]
# We use django-loginas to allow admins to take over sessions
CAN_LOGIN_AS = "utils.can_login_as.check"
CRISPY_TEMPLATE_PACK = "bootstrap4"
WSGI_APPLICATION = "cobalt.wsgi.application"
AUTH_USER_MODEL = "accounts.User"
AUTHENTICATION_BACKENDS = ["accounts.backend.CobaltBackend"]
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"},
]
EMAIL_BACKEND = "post_office.EmailBackend"
POST_OFFICE = {
"BACKENDS": {
"default": "django_ses.SESBackend",
},
"TEMPLATE_ENGINE": "post_office",
"DEFAULT_PRIORITY": "medium",
"MESSAGE_ID_ENABLED": True,
}
EMAIL_USE_TLS = True
EMAIL_PORT = 587
LANGUAGE_CODE = "en-au"
TIME_ZONE = "Australia/Sydney"
USE_I18N = True
USE_L10N = True
USE_TZ = True
DATE_FORMAT = "j M Y"
TIME_FORMAT = "g:I A"
DATE_INPUT_FORMATS = ["%d %b %Y", "%d/%m/%Y", "%d %b %Y"]
TIME_INPUT_FORMATS = [
"%I:%M %p",
]
# This is where collectstatic will put the static files it finds
STATIC_ROOT = os.path.join(BASE_DIR, "static")
# External reference point to find static
STATIC_URL = "/static/"
# append MD5 hash to filenames to prevent caching on version change
STATICFILES_STORAGE = "utils.storage.ForgivingManifestStaticFilesStorage"
# TODO: update environment variables in EB and delete the EFS code here
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
if "FILE_SYSTEM_ID" in os.environ: # AWS EFS for media
MEDIA_ROOT = "/cobalt-media"
MEDIA_URL = "/media/"
# Allow override of media root
MEDIA_ROOT = set_value("MEDIA_ROOT_OVERRIDE", MEDIA_ROOT)
LOGIN_REDIRECT_URL = "/dashboard"
LOGOUT_REDIRECT_URL = "/"
MESSAGE_TAGS = {
messages.DEBUG: "alert-info",
messages.INFO: "alert-info",
messages.SUCCESS: "alert-success",
messages.WARNING: "alert-warning",
messages.ERROR: "alert-danger",
}
EMAIL_SUBJECT_PREFIX = f"[{COBALT_HOSTNAME}] "
GLOBAL_ORG = "ABF"
GLOBAL_TITLE = "My ABF"
GLOBAL_CONTACT = "/support/contact-logged-in"
GLOBAL_ABOUT = "https://abf.com.au"
GLOBAL_COOKIES = "/support/cookies"
GLOBAL_PRODUCTION = "www.myabf.com.au"
GLOBAL_TEST = "test.myabf.com.au"
GLOBAL_PRIVACY = "https://abf.com.au/privacy"
GLOBAL_CURRENCY_SYMBOL = "$"
GLOBAL_CURRENCY_NAME = "Dollar"
BRIDGE_CREDITS = "Bridge Credits"
# Payments auto amounts
AUTO_TOP_UP_LOW_LIMIT = 20
AUTO_TOP_UP_DEFAULT_AMT = 100
AUTO_TOP_UP_MIN_AMT = 50
AUTO_TOP_UP_MAX_AMT = 2000
# django-summernote provides the rich text entry fields
# SUMMERNOTE_THEME = 'bs4'
SUMMERNOTE_CONFIG = {
"iframe": False,
"summernote": {
"fontSizes": ["8", "9", "10", "11", "12", "14", "16", "18", "24", "36"],
"lineHeights": ["1", "0.5", "0"],
"airMode": False,
"width": "100%",
"height": "600",
"lang": None,
"spellCheck": True,
"toolbar": [
["style", ["style"]],
["fontsize", ["fontsize"]],
["font", ["bold", "italic", "underline"]],
["fontname", ["fontname"]],
["height", ["height"]],
["color", ["color"]],
["para", ["ul", "ol", "paragraph"]],
["table", ["table"]],
["insert", ["link", "picture", "hr"]],
[
"cards",
[
"specialcharsspades",
"specialcharshearts",
"specialcharsdiamonds",
"specialcharsclubs",
"specialcharshand",
],
],
["view", ["fullscreen", "codeview"]],
["help", ["help"]],
],
},
"attachment_require_authentication": True,
"disable_attachment": False,
"attachment_absolute_uri": True,
"attachment_filesize_limit": 20000000,
}
# COB-947: Removing hard coding of system account ids
# Default user to be the everyone user for RBAC
RBAC_EVERYONE = int(set_value("RBAC_EVERYONE_ID", 1))
# TBA User for Event entries
TBA_PLAYER = int(set_value("TBA_PLAYER_ID", 2))
# ABF User for Announcements
ABF_USER = int(set_value("ABF_USER_ID", 3))
# System accounts
ALL_SYSTEM_ACCOUNTS = [RBAC_EVERYONE, TBA_PLAYER, ABF_USER]
ALL_SYSTEM_ACCOUNT_SYSTEM_NUMBERS = [0, 1, 2]
# ABF Organisation - used for Settlement transactions and other things. Assumed to be the first thing created.
ABF_ORG = 1
# Org id for the system account
GLOBAL_ORG_ID = 1
# Logout users every 100 years or so
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_COOKIE_AGE = 5256000000
# Upgrade to Django 3.2 requires this setting. Not clear if BigAutoField would be better
# This is the current default so using that
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# The bleach module is used to clean user supplied HTML, mainly for textfields from Summernote
BLEACH_ALLOWED_TAGS = [
"a",
"abbr",
"acronym",
"b",
"u",
"blockquote",
"code",
"em",
"i",
"li",
"ol",
"strong",
"ul",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"br",
"p",
"span",
"font",
"pre",
"table",
"tbody",
"tr",
"td",
"img",
"hr",
]
BLEACH_ALLOWED_ATTRIBUTES = [
"title",
"href",
"style",
"color",
"class",
"target",
"src",
"data-filename",
]
BLEACH_ALLOWED_STYLES = [
"text-decoration",
"text-align",
"line-height",
"font-size",
"font-family",
"background-color",
"width",
"float",
]
# Group used to manage the helpdesk staff
RBAC_HELPDESK_GROUP = "rbac.orgs.abf.abf_roles.helpdesk_staff"
# ABF States - org_id, name, state
ABF_STATES = {
1801: ("BFACT", "ACT"),
2001: ("NSWBA", "NSW"),
8901: ("NTBA", "NT"),
4501: ("QBA", "QLD"),
5700: ("SABF", "SA"),
7801: ("TBA", "TAS"),
3301: ("VBA", "VIC"),
6751: ("BAWA", "WA"),
}
# On Elastic Beanstalk the userid that we run under seems to change. Set all permissions to 777 as there is
# no sensitive information stored here, and only Django can access the files directly.
FILE_UPLOAD_PERMISSIONS = 0o777
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o777
# Check for default value of COBALT_HOSTNAME. If this is not set to something else then we could be on Read The Docs
# Read the Docs will fail writing to the log file
if COBALT_HOSTNAME == "127.0.0.1:8000":
LOGFILE = "/tmp/cobalt.log"
else:
LOGFILE = "/var/log/cobalt.log"
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"cobalt": {
"format": "[%(levelname)-8s] %(asctime)s [%(module)s %(funcName)s %(lineno)d] %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "cobalt",
},
"file": {
"class": "logging.FileHandler",
"filename": LOGFILE,
"formatter": "cobalt",
},
},
"loggers": {
"cobalt": {
"handlers": ["console", "file"],
"level": "DEBUG",
},
"post_office": {
"handlers": ["console", "file"],
"level": "DEBUG",
},
},
}
GOOGLE_APPLICATION_CREDENTIALS = set_value("GOOGLE_APPLICATION_CREDENTIALS", "NOTSET")
FIREBASE_APP = initialize_app()
# Check if we want to enable the debug toolbar
DEBUG_TOOLBAR_ENABLED = set_value("DEBUG_TOOLBAR_ENABLED", False)
DEBUG_TOOLBAR_ENABLED = DEBUG_TOOLBAR_ENABLED == "ON"
if DEBUG and DEBUG_TOOLBAR_ENABLED:
INSTALLED_APPS.append("debug_toolbar")
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
INTERNAL_IPS = [
"127.0.0.1",
]