Source code for intranet.settings

import datetime
import logging
import os
import re
import sys
from typing import Any, Dict, List, Tuple  # noqa

import celery.schedules
import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import LoggingIntegration

from ..utils import helpers  # pylint: disable=wrong-import-position # noqa

if sys.version_info < (3, 5):
    # Require Python 3.5+
    raise Exception("Python 3.5 or higher is required.")


""" !! In production, add a file called secret.py to the settings package that
defines SECRET_KEY, SECRET_DATABASE_URL. !!


SECRET_DATABASE_URL should be of the following form:
    postgres://<user>:<password>@<host>/<database>
"""

# Dummy values for development and testing.
# Overridden by the import from secret.py below.
SECRET_DATABASE_URL = None  # type: str
MAINTENANCE_MODE = None  # type: bool
TJSTAR_MAP = None  # type: bool
TWITTER_KEYS = None  # type: Dict[str,str]
SENTRY_PUBLIC_DSN = None  # type: str
USE_SASL = True
NO_CACHE = False
PARKING_ENABLED = True
PARKING_MAX_ABSENCES = 5
NOMINATIONS_ACTIVE = False
NOMINATION_POSITION = ""
ENABLE_WAITLIST = False  # WARNING: Enabling the waitlist causes severe performance issues
ENABLE_BUS_APP = True
ENABLE_BUS_DRIVER = True
ENABLE_PRE_EIGHTH_REDIRECT = False
NOTIFY_ADMIN_EMAILS = None

IOS_APP_CLIENT_IDS = []  # Attempting to OAuth to an application with one of these client IDs will result in a *special* error message
# See templates/oauth2_provider/authorize.html

ALLOWED_METRIC_SCRAPE_IPS = []

EMERGENCY_MESSAGE = None  # type: str

# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
#
# In production, Nginx filters requests that are not in this list. If this is
# not done, a notification gets sent whenever someone messes with
# the HTTP Host header.
ALLOWED_HOSTS = ["ion.tjhsst.edu", "198.38.18.250", "localhost", "127.0.0.1"]

# When school is scheduled to start
SCHOOL_START_DATE = datetime.date(2017, 8, 28)

# Dates when hoco starts and ends
HOCO_START_DATE = datetime.date(2017, 10, 2)
HOCO_END_DATE = datetime.date(2017, 10, 14)

PRODUCTION = os.getenv("PRODUCTION", "").upper() == "TRUE"
TRAVIS = os.getenv("TRAVIS", "").upper() == "TRUE"
# FIXME: figure out a less-hacky way to do this.
TESTING = "test" in sys.argv
LOGGING_VERBOSE = PRODUCTION

# Whether to report master password attempts
MASTER_NOTIFY = False

# DEBUG defaults to off in PRODUCTION, on otherwise.
DEBUG = os.getenv("DEBUG", str(not PRODUCTION).upper()) == "TRUE"

# Don't send emails unless we're in production.
EMAIL_ANNOUNCEMENTS = PRODUCTION
SEND_ANNOUNCEMENT_APPROVAL = PRODUCTION

# Whether to force sending emails, even if we aren't in production.
FORCE_EMAIL_SEND = False

# Don't require https for testing.
SESSION_COOKIE_SECURE = PRODUCTION
CSRF_COOKIE_SECURE = PRODUCTION

if not PRODUCTION:
    # We don't care about session security when running a testing instance.
    SECRET_KEY = "_5kc##e7(!4=4)h4slxlgm010l+43zd_84g@82771ay6no-1&i"
    # Trust X-Forwarded-For when testing
    SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTOCOL", "https")

# Internal IP ranges in production
_internal_ip_list = ["198.38.16.0/20", "2001:468:cc0::/48"]

if not PRODUCTION:
    # Additional Internal IP ranges for debugging
    _internal_ip_list.extend(["127.0.0.0/8", "10.0.0.0/8"])

INTERNAL_IPS = helpers.GlobList(_internal_ip_list)

# Used for Printing access; FCPS external/internal IP ranges
_tj_ip_list = _internal_ip_list + ["151.188.0.0/18", "151.188.192.0/18", "10.0.0.0/8"]

TJ_IPS = helpers.GlobList(_tj_ip_list)

PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# What login_required decorator redirects to
# https://docs.djangoproject.com/en/dev/ref/settings/#login-url
LOGIN_URL = "/login"
LOGIN_REDIRECT_URL = "/"

# Whether to perform an HTTP redirect to append a slash
APPEND_SLASH = False

# Email notifications backend and mailserver configuration
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "mail.tjhsst.edu"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_SUBJECT_PREFIX = "[Ion] "
EMAIL_ANNOUNCEMENTS = True

# Address to send messages from
EMAIL_FROM = "ion-noreply@tjhsst.edu"

# Use PostgreSQL database
DATABASES = {"default": {"ENGINE": "django_prometheus.db.backends.postgresql", "CONN_MAX_AGE": 30}}  # type: Dict[str,Dict[str,Any]]

# Address to send feedback messages to
FEEDBACK_EMAIL = "intranet@tjhsst.edu"

# Address to send approval messages to
APPROVAL_EMAIL = "intranet-approval@tjhsst.edu"

FILE_UPLOAD_HANDLERS = ["django.core.files.uploadhandler.MemoryFileUploadHandler", "django.core.files.uploadhandler.TemporaryFileUploadHandler"]

# The maximum number of pages in one document that can be
# printed through the printing functionality (determined through pdfinfo)
PRINTING_PAGES_LIMIT = 15

# The maximum file upload and download size for files
FILES_MAX_UPLOAD_SIZE = 200 * 1024 * 1024
FILES_MAX_DOWNLOAD_SIZE = 200 * 1024 * 1024

DATA_UPLOAD_MAX_MEMORY_SIZE = FILES_MAX_UPLOAD_SIZE

# Custom error view for CSRF errors; if unspecified, caught by nginx with a generic error
CSRF_FAILURE_VIEW = "intranet.apps.error.views.handle_csrf_view"

############################################
#    OPSEC: GIVE A REASON FOR SILENCING    #
#    SYSTEM CHECKS IF YOU ADD ONE HERE!    #
############################################

SILENCED_SYSTEM_CHECKS = [
    # W001 doesn't apply, as we use nginx to handle SecurityMiddleware's functions.
    "security.W001",
    # Suppress W019, as we use frames in the signage module.
    "security.W019",
]

# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = "America/New_York"

# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = "en-us"

SITE_ID = 1

# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = False

# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale.
USE_L10N = True

# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True

# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
#
# Not used.
MEDIA_ROOT = os.path.join(os.path.dirname(PROJECT_ROOT), "uploads")

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
#
# Not used.
MEDIA_URL = ""

TEST_RUNNER = "django.test.runner.DiscoverRunner"

# Absolute path to the directory static files should be collected to.
# Don"t put anything in this directory yourself; store your static files
# in apps" "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
#
# This is the folder that Nginx serves as /static in production
STATIC_ROOT = os.path.join(PROJECT_ROOT, "collected_static")

# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = "/static/"

# Additional locations of static files
STATICFILES_DIRS = [
    # Put strings here, like "/home/html/static" or "C:/www/django/static".
    # Always use forward slashes, even on Windows.
    # Don"t forget to use absolute paths, not relative paths.
    os.path.join(PROJECT_ROOT, "static")
]

# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = [
    "django.contrib.staticfiles.finders.FileSystemFinder",
    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
    "pipeline.finders.PipelineFinder",
]

STATICFILES_STORAGE = "pipeline.storage.PipelineStorage"

PIPELINE = {
    "CSS_COMPRESSOR": None,
    "COMPILERS": ["pipeline.compilers.sass.SASSCompiler"],
    "STYLESHEETS": {
        "base": {"source_filenames": ["css/base.scss", "css/themes.scss", "css/responsive.scss"], "output_filename": "css/base.css"},
        "eighth.admin": {"source_filenames": ["css/eighth.common.scss", "css/eighth.admin.scss"], "output_filename": "css/eighth.admin.css"},
        "eighth.signup": {"source_filenames": ["css/eighth.common.scss", "css/eighth.signup.scss"], "output_filename": "css/eighth.signup.css"},
    },
}  # type: Dict[str,Any]

LIST_OF_INDEPENDENT_CSS = [
    "about",
    "api",
    "login",
    "emerg",
    "files",
    "schedule",
    "theme.blue",
    "page_base",
    "responsive.core",
    "search",
    "dashboard",
    "events",
    "schedule.widget",
    "dashboard.widgets",
    "profile",
    "polls",
    "groups",
    "board",
    "announcements.form",
    "polls.form",
    "preferences",
    "signage.base",
    "signage.touch",
    "signage.touch.landscape",
    "eighth.common",
    "eighth.attendance",
    "eighth.profile",
    "eighth.schedule",
    "eighth.maintenance",
    "lostfound",
    "welcome",
    "hoco_ribbon",
    "hoco_scores",
    "oauth",
    "bus",
    "signage.page",
    "courses",
    "sessionmgmt",
    "dark/base",
    "dark/login",
    "dark/schedule",
    "dark/events",
    "dark/dashboard",
    "dark/dashboard.widgets",
    "dark/schedule.widget",
    "dark/nav",
    "dark/cke",
    "dark/polls",
    "dark/bus",
    "dark/files",
    "dark/welcome",
    "dark/preferences",
    "dark/about",
    "dark/lostfound",
    "dark/eighth.signup",
    "dark/eighth.attendance",
    "dark/select",
    "dark/eighth.schedule",
    "dark/oauth",
    "dark/sessionmgmt",
]

for name in LIST_OF_INDEPENDENT_CSS:
    PIPELINE["STYLESHEETS"].update(helpers.single_css_map(name))

AUTHENTICATION_BACKENDS = (
    "django.contrib.auth.backends.ModelBackend",
    "intranet.apps.auth.backends.MasterPasswordAuthenticationBackend",
    "intranet.apps.auth.backends.KerberosAuthenticationBackend",
    "oauth2_provider.backends.OAuth2Backend",
)
# Default to Argon2, see https://docs.djangoproject.com/en/dev/topics/auth/passwords/#argon2-usage
PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.Argon2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
    "django.contrib.auth.hashers.BCryptPasswordHasher",
]

# Use the custom User model defined in apps/users/models.py
AUTH_USER_MODEL = "users.User"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": (os.path.join(PROJECT_ROOT, "templates"),),
        "OPTIONS": {
            "context_processors": (
                "django.contrib.auth.context_processors.auth",  # Authentication; must be defined first
                "django.template.context_processors.debug",  # Django default
                "django.template.context_processors.request",  # Django default
                "django.contrib.messages.context_processors.messages",  # For page messages
                "intranet.apps.context_processors.ion_base_url",  # For determining the base url
                "intranet.apps.context_processors.nav_categorizer",  # For determining the category in the navbar
                "intranet.apps.context_processors.global_warning",  # For showing a global warning throughout the application (in page_base.html)
                "intranet.apps.eighth.context_processors.start_date",  # For determining the eighth pd start date
                "intranet.apps.eighth.context_processors.absence_count",  # For showing the absence count in the navbar
                "intranet.apps.eighth.context_processors.enable_waitlist",  # For checking if the waitlist is enabled
                "intranet.apps.context_processors.mobile_app",  # For the custom android app functionality (tbd?)
                "intranet.apps.context_processors.is_tj_ip",  # Whether on the internal TJ or FCPS network
                "intranet.apps.context_processors.show_homecoming",  # Sitewide custom themes (special events, etc)
                "intranet.apps.context_processors.global_custom_theme",  # Sitewide custom themes (special events, etc)
                "intranet.apps.context_processors.show_bus_button",
                "intranet.apps.context_processors.enable_dark_mode",
                "intranet.apps.context_processors.oauth_toolkit",  # Django OAuth Toolkit-related middleware
                "intranet.apps.context_processors.settings_export",  # "Exports" django.conf.settings as DJANGO_SETTINGS
                "intranet.apps.features.context_processors.feature_announcements",  # Feature announcements that need to be shown on the current page
            ),
            "debug": True,  # Only enabled if DEBUG is true as well
            "loaders": ("django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader"),
            "libraries": {"staticfiles": "django.contrib.staticfiles.templatetags.staticfiles"},
        },
    }
]  # type: List[Dict[str,Any]]

if PRODUCTION:
    TEMPLATES[0]["OPTIONS"]["loaders"] = [
        ("django.template.loaders.cached.Loader", ["django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader"])
    ]

if not PRODUCTION and os.getenv("WARN_INVALID_TEMPLATE_VARS", "NO") == "YES":
    TEMPLATES[0]["OPTIONS"]["string_if_invalid"] = helpers.InvalidString("%s")

MIDDLEWARE = [
    "intranet.middleware.url_slashes.FixSlashes",  # Remove slashes in URLs
    "intranet.middleware.same_origin.SameOriginMiddleware",  # 401s requests with an "Origin" header that doesn't match the "Host" header
    "django_prometheus.middleware.PrometheusBeforeMiddleware",  # Django Prometheus initial
    "django.middleware.common.CommonMiddleware",  # Django default
    "django.contrib.sessions.middleware.SessionMiddleware",  # Django sessions
    "django.middleware.csrf.CsrfViewMiddleware",  # Django CSRF
    "django.middleware.clickjacking.XFrameOptionsMiddleware",  # Django X-Frame-Options
    "django.contrib.auth.middleware.AuthenticationMiddleware",  # Django auth
    "oauth2_provider.middleware.OAuth2TokenMiddleware",  # Django Oauth toolkit
    "intranet.middleware.monitoring.PrometheusAccessMiddleware",  # Restricts access to Django Prometheus metrics to ALLOWED_METRIC_IPS and superusers
    "maintenance_mode.middleware.MaintenanceModeMiddleware",  # Maintenance mode
    "intranet.middleware.threadlocals.ThreadLocalsMiddleware",  # Thread locals
    "intranet.middleware.traceback.UserTracebackMiddleware",  # Include user in traceback
    "django.contrib.messages.middleware.MessageMiddleware",  # Messages
    "django_user_agents.middleware.UserAgentMiddleware",
    "intranet.middleware.session_management.SessionManagementMiddleware",  # Handles session management (might log the user out, so must be early)
    "intranet.middleware.ajax.AjaxNotAuthenticatedMiddleWare",  # See note in ajax.py
    "intranet.middleware.templates.AdminSelectizeLoadingIndicatorMiddleware",  # Selectize fixes
    "intranet.middleware.templates.NoReferrerMiddleware",  # Prevent malicious JS from changing the referring page
    "intranet.middleware.access_log.AccessLogMiddleWare",  # Access log
    "django_requestlogging.middleware.LogSetupMiddleware",  # Request logging
    "corsheaders.middleware.CorsMiddleware",  # CORS headers, for ext. API use
    "simple_history.middleware.HistoryRequestMiddleware",
    "django_prometheus.middleware.PrometheusAfterMiddleware",  # Django Prometheus after
    "intranet.middleware.dark_mode.DarkModeMiddleware",  # Dark mode-related middleware
    "django_referrer_policy.middleware.ReferrerPolicyMiddleware",  # Sets the Referrer-Policy header
]

# URLconf at urls.py
ROOT_URLCONF = "intranet.urls"

# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = "intranet.wsgi.application"

# Name of current virtualenv
VIRTUAL_ENV = os.path.basename(os.environ["VIRTUAL_ENV"]) if "VIRTUAL_ENV" in os.environ else "None"


[docs]def get_month_seconds(): return datetime.timedelta(hours=24).total_seconds() * 30
CACHE_AGE = { "dn_id_mapping": int(12 * get_month_seconds()), "user_grade": int(10 * get_month_seconds()), "user_classes": int(6 * get_month_seconds()), "user_photo": int(6 * get_month_seconds()), "class_teacher": int(6 * get_month_seconds()), "class_attribute": int(6 * get_month_seconds()), "user_attribute": int(2 * get_month_seconds()), "bell_schedule": int(datetime.timedelta(weeks=1).total_seconds()), "users_list": int(datetime.timedelta(hours=24).total_seconds()), "printers_list": int(datetime.timedelta(minutes=10).total_seconds()), "emerg": int(datetime.timedelta(minutes=5).total_seconds()), "sports_school_events": int(datetime.timedelta(hours=1).total_seconds()), } if not PRODUCTION and os.getenv("SHORT_CACHE", "NO") == "YES": # Make the cache age last just long enough to reload the page to # check if caching worked for key in CACHE_AGE: CACHE_AGE[key] = 60 # Cacheops configuration # may be removed in the future CACHEOPS_REDIS = {"host": "127.0.0.1", "port": 6379, "db": 1, "socket_timeout": 1} CACHEOPS = { "eighth.*": {"timeout": int(datetime.timedelta(hours=24).total_seconds())}, # Only used for caching activity, block lists "groups.*": {"timeout": int(datetime.timedelta(hours=24).total_seconds())}, # Only used for caching group list "users.UserDarkModeProperties": {"ops": "get", "timeout": int(datetime.timedelta(minutes=10).total_seconds())}, "features.FeatureAnnouncement": {"ops": "all", "timeout": int(datetime.timedelta(hours=1).total_seconds())}, "*.*": {"ops": (), "timeout": 5}, # Allow manual caching on everything else with a default timeout of 5 seconds } if not TESTING: # Settings for django-redis-sessions # We use a custom "wrapper" session engine that inherits from django-redis-session's session engine. # It allows customization of certain session-related behavior. See the comments in intranet/utils/session.py for more details. SESSION_ENGINE = "intranet.utils.session" SESSION_REDIS_HOST = "127.0.0.1" SESSION_REDIS_PORT = 6379 SESSION_REDIS_DB = 0 SESSION_REDIS_PREFIX = "ion:session" SESSION_REDIS = {"host": SESSION_REDIS_HOST, "port": SESSION_REDIS_PORT, "db": SESSION_REDIS_DB, "prefix": SESSION_REDIS_PREFIX} SESSION_COOKIE_AGE = int(datetime.timedelta(hours=2).total_seconds()) SESSION_SAVE_EVERY_REQUEST = True CACHES = { "default": { "OPTIONS": { # Avoid conflict between production and testing redis db "DB": (1 if PRODUCTION else 2) } } } # type: Dict[str,Dict[str,Any]] if TESTING or os.getenv("DUMMY_CACHE", "NO") == "YES" or NO_CACHE: CACHES["default"] = {"BACKEND": "intranet.utils.cache.DummyCache"} # extension of django.core.cache.backends.dummy.DummyCache else: CACHES["default"] = { "BACKEND": "redis_cache.RedisCache", "LOCATION": "127.0.0.1:6379", "OPTIONS": {"PARSER_CLASS": "redis.connection.HiredisParser", "PICKLE_VERSION": 4}, "KEY_PREFIX": "ion", } CSL_REALM = "CSL.TJHSST.EDU" # CSL Realm AD_REALM = "LOCAL.TJHSST.EDU" KINIT_TIMEOUT = 15 # seconds before pexpect timeouts FCPS_STUDENT_ID_LENGTH = 7 # Django REST framework configuration REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": ( "intranet.apps.auth.rest_permissions.DenyRestrictedPermission", # require authentication and deny restricted users ), "USE_ABSOLUTE_URLS": True, # Return native `Date` and `Time` objects in `serializer.data` "DATETIME_FORMAT": None, "DATE_FORMAT": None, "TIME_FORMAT": None, "EXCEPTION_HANDLER": "intranet.apps.api.utils.custom_exception_handler", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 50, "DEFAULT_AUTHENTICATION_CLASSES": ( "intranet.apps.api.authentication.ApiBasicAuthentication", "intranet.apps.api.authentication.CsrfExemptSessionAuthentication", # exempts CSRF checking on API "oauth2_provider.contrib.rest_framework.OAuth2Authentication", ), } # Django OAuth Toolkit configuration OAUTH2_PROVIDER = { # this is the list of available scopes "SCOPES": {"read": "Read scope", "write": "Write scope"}, # OAuth refresh tokens expire in 30 days "REFRESH_TOKEN_EXPIRE_SECONDS": 60 * 60 * 24 * 30, } OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth2_provider.Application" INSTALLED_APPS = [ # internal Django "django.contrib.auth", "django.contrib.admin", "django.contrib.admindocs", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", # Django plugins "django_extensions", # django-extensions "django_requestlogging", # django-requestlogging-redux "rest_framework", # django-rest-framework "maintenance_mode", # django-maintenance-mode "django_prometheus", # django-prometheus "pipeline", # django-pipeline "channels", # Intranet apps "intranet.apps", "intranet.apps.announcements", "intranet.apps.api", "intranet.apps.auth", "intranet.apps.bus", "intranet.apps.eighth", "intranet.apps.events", "intranet.apps.groups", "intranet.apps.search", "intranet.apps.schedule", "intranet.apps.notifications", "intranet.apps.feedback", "intranet.apps.users", "intranet.apps.preferences", "intranet.apps.files", "intranet.apps.printing", "intranet.apps.polls", "intranet.apps.signage", "intranet.apps.seniors", "intranet.apps.emerg", "intranet.apps.itemreg", "intranet.apps.lostfound", "intranet.apps.emailfwd", "intranet.apps.parking", "intranet.apps.dataimport", "intranet.apps.nomination", "intranet.apps.sessionmgmt", "intranet.apps.features", # Django plugins "widget_tweaks", "oauth2_provider", # django-oauth-toolkit "corsheaders", # django-cors-headers "cacheops", # django-cacheops "svg", # django-inline-svg "simple_history", # django-simple-history "django_referrer_policy", "django_user_agents", ] # Django Channels Configuration (we use this for websockets) CHANNEL_LAYERS = {"default": {"BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": {"hosts": [("127.0.0.1", 6379)]}}} ASGI_APPLICATION = "intranet.routing.application" PROMETHEUS_EXPORT_MIGRATIONS = False # Eighth period default block date format # Post Django 1.8.7, this can no longer be used in templates. EIGHTH_BLOCK_DATE_FORMAT = "D, N j, Y" # See http://docs.djangoproject.com/en/dev/topics/logging for # more details on how to customize your logging configuration. LOG_LEVEL = "DEBUG" if LOGGING_VERBOSE else "INFO" if os.getenv("LOG_LEVEL") in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"): LOG_LEVEL = os.environ["LOG_LEVEL"]
[docs]def get_log(name): # pylint: disable=redefined-outer-name; 'name' is used as the target of a for loop, so we can safely override it return [name] if (PRODUCTION and not TRAVIS) else []
# https://docs.djangoproject.com/en/dev/topics/logging/ LOGGING = { "version": 1, "disable_existing_loggers": True, "formatters": { "simple": {"format": "%(levelname)s: %(asctime)s - %(remote_addr)s - %(username)s - %(path_info)s\n\t%(message)s"}, "access": {"format": "%(message)s"}, "error": {"format": "%(asctime)s: \n%(message)s"}, }, "filters": { "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}, "request": {"()": "django_requestlogging.logging_filters.RequestFilter"}, }, "handlers": { # Log in console "console": {"level": "DEBUG", "class": "logging.StreamHandler", "filters": ["request"], "formatter": "simple"}, # Log access in console "console_access": {"level": "DEBUG", "class": "logging.StreamHandler", "filters": ["request"], "formatter": "access"}, # Log access to file (DEBUG=FALSE) "access_log": { "level": "DEBUG", "filters": ["require_debug_false"], "class": "logging.handlers.TimedRotatingFileHandler", "formatter": "access", "filename": "/var/log/ion/app_access.log", # Rollover on Sundays; preserve 20 weeks "when": "W6", "interval": 1, "backupCount": 20, "delay": True, }, # Log auth to file (DEBUG=FALSE) "auth_log": { "level": "DEBUG", "filters": ["require_debug_false"], "class": "logging.handlers.TimedRotatingFileHandler", "formatter": "access", "filename": "/var/log/ion/app_auth.log", # Rollover on Sundays; preserve 20 weeks "when": "W6", "interval": 1, "backupCount": 20, "delay": True, }, # Log error to file (DEBUG=FALSE) "error_log": { "level": "ERROR", "filters": ["require_debug_false", "request"], "class": "logging.FileHandler", "formatter": "error", "filename": "/var/log/ion/app_error.log", "delay": True, }, }, "loggers": { # Django errors get sent to console and error logfile "django": {"handlers": ["console"] + get_log("error_log"), "level": "ERROR", "propagate": True}, # Intranet errors go to console and error logfile "intranet": {"handlers": ["console"] + get_log("error_log"), "level": LOG_LEVEL, "propagate": True}, # Intranet access logs to accesslog "intranet_access": {"handlers": ["console_access"] + get_log("access_log"), "level": "DEBUG", "propagate": False}, # Intranet auth logs to authlog "intranet_auth": {"handlers": ["console_access"] + get_log("auth_log"), "level": "DEBUG", "propagate": False}, # errors that relate to sentry "sentry.errors": {"level": "DEBUG", "handlers": ["console"], "propagate": False}, }, } # The debug toolbar is always loaded, unless you manually override SHOW_DEBUG_TOOLBAR SHOW_DEBUG_TOOLBAR = os.getenv("SHOW_DEBUG_TOOLBAR", "YES") == "YES" if SHOW_DEBUG_TOOLBAR: DEBUG_TOOLBAR_PATCH_SETTINGS = False # Boolean value defines whether enabled by default _panels = [ ("debug_toolbar.panels.versions.VersionsPanel", False), ("debug_toolbar.panels.timer.TimerPanel", True), ("debug_toolbar.panels.settings.SettingsPanel", False), ("debug_toolbar.panels.headers.HeadersPanel", False), ("debug_toolbar.panels.request.RequestPanel", False), ("debug_toolbar.panels.sql.SQLPanel", True), ("debug_toolbar.panels.staticfiles.StaticFilesPanel", False), ("debug_toolbar.panels.templates.TemplatesPanel", False), ("debug_toolbar.panels.cache.CachePanel", False), ("debug_toolbar.panels.signals.SignalsPanel", False), ("debug_toolbar.panels.logging.LoggingPanel", True), ("debug_toolbar.panels.redirects.RedirectsPanel", False), ("debug_toolbar.panels.profiling.ProfilingPanel", False), ] # Only show debug toolbar when requested if in production. DEBUG_TOOLBAR_CONFIG = { "DISABLE_PANELS": [panel for panel, enabled in _panels if not enabled], "SHOW_TOOLBAR_CALLBACK": "intranet.utils.helpers.debug_toolbar_callback", } DEBUG_TOOLBAR_PANELS = [t[0] for t in _panels] # Add middleware MIDDLEWARE.extend( [ "intranet.middleware.templates.StripNewlinesMiddleware", # Strip newlines "debug_toolbar.middleware.DebugToolbarMiddleware", # Debug toolbar ] ) INSTALLED_APPS += ["debug_toolbar"] MAINTENANCE_MODE_TEMPLATE = "error/503.html" MAINTENANCE_MODE_IGNORE_SUPERUSER = True # Allow *.tjhsst.edu sites to access API, signage, and other resources CORS_ORIGIN_ALLOW_ALL = False # Uncomment to only allow XHR on API resources from TJ domains # CORS_URLS_REGEX = r'^/api/.*$' # Same origin frame options X_FRAME_OPTIONS = "SAMEORIGIN" # X-XSS-Protection: 1; mode=block # Already set on nginx level SECURE_BROWSER_XSS_FILTER = True # To accomodate for the fact that nginx "swallows" https connections # by forwarding to http://gunicorn # https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Add git information for the login page GIT = { "commit_short_hash": helpers.get_current_commit_short_hash(PROJECT_ROOT), "commit_long_hash": helpers.get_current_commit_long_hash(PROJECT_ROOT), "commit_info": helpers.get_current_commit_info(), "commit_date": helpers.get_current_commit_date(), "commit_github_url": helpers.get_current_commit_github_url(PROJECT_ROOT), } # Senior graduation year SENIOR_GRADUATION_YEAR = 2019 # Senior graduation date in Javascript-readable format SENIOR_GRADUATION = datetime.datetime(year=SENIOR_GRADUATION_YEAR, month=6, day=18, hour=19).strftime("%B %d %Y %H:%M:%S") # Month (1-indexed) after which a new school year begins # July = 7 YEAR_TURNOVER_MONTH = 7 # The hour on an eighth period day to lock teachers from # taking attendance (10PM) ATTENDANCE_LOCK_HOUR = 22 # The number of days to show an absence message (2 weeks) CLEAR_ABSENCE_DAYS = 14 # The address for FCPS' Emergency Announcement page FCPS_EMERGENCY_PAGE = "https://www.fcps.edu/alert_msg_feed" # type: str # The timeout for the request to FCPS' emergency page (in seconds) FCPS_EMERGENCY_TIMEOUT = 5 # How frequently the emergency announcement cache should be updated by the Celerybeat task. # This should be less than CACHE_AGE["emerg"]. FCPS_EMERGENCY_CACHE_UPDATE_INTERVAL = CACHE_AGE["emerg"] - 30 # Show an iframe with tjStar activity data if TJSTAR_MAP is None: TJSTAR_MAP = False SIMILAR_THRESHOLD = 5 # Substrings of user agents to not log in the Ion access logs NONLOGGABLE_USER_AGENT_SUBSTRINGS = ["Prometheus", "GoogleBot", "UptimeRobot"] # The location of the Celery broker (message transport) CELERY_BROKER_URL = "amqp://localhost" CELERY_ACCEPT_CONTENT = ["json", "pickle"] CELERY_TASK_SERIALIZER = "pickle" CELERY_BEAT_SCHEDULE = { "update-fcps-emergency-cache": { "task": "intranet.apps.emerg.tasks.update_emerg_cache_task", "schedule": FCPS_EMERGENCY_CACHE_UPDATE_INTERVAL, "args": (), }, "reset-routes": {"task": "intranet.apps.bus.tasks.reset_routes", "schedule": celery.schedules.crontab(hour=8, minute=0), "args": ()}, } MAINTENANCE_MODE = False # Django User Agents configuration USER_AGENTS_CACHE = "default" # The Referrer-policy header REFERRER_POLICY = "strict-origin-when-cross-origin" REAUTHENTICATION_EXPIRE_TIMEOUT = 2 * 60 * 60 # seconds EIGHTH_COORDINATOR_NAME = "Laura Slonina" # How often the signage JS sends a heartbeat SIGNAGE_HEARTBEAT_INTERVAL = 60 # No heartbeat after this many seconds means a sign will be considered offline SIGNAGE_HEARTBEAT_OFFLINE_TIMEOUT_SECS = 2 * 60 # Shows a warning message with yellow background on the login page # LOGIN_WARNING = "This is a message to display on the login page." # Shows a warning message with yellow background on the login and all interior pages # GLOBAL_WARNING = "This is a message to display throughout the application." try: from .secret import * # noqa except ImportError: pass # In-memory sqlite3 databases significantly speed up running tests. if TESTING: DATABASES["default"]["ENGINE"] = "django.db.backends.sqlite3" DATABASES["default"]["NAME"] = ":memory:" # Horrible hack to suppress all migrations to speed up the tests. MIGRATION_MODULES = helpers.MigrationMock() # FIXME: we really shouldn't have to do this. LOGGING_VERBOSE = re.search("-v ?[2-3]|--verbosity [2-3]", " ".join(sys.argv)) is not None elif PRODUCTION or SECRET_DATABASE_URL is not None: DATABASES["default"].update(helpers.parse_db_url(SECRET_DATABASE_URL)) else: # Default testing db config. DATABASES["default"].update({"NAME": "ion", "USER": "ion", "PASSWORD": "pwd"}) # Set up sentry logging if PRODUCTION: # This is implicitly set up but we do this just in case sentry_logging = LoggingIntegration( level=logging.INFO, event_level=logging.ERROR # Capture info and above as breadcrumbs # Send errors as events ) sentry_sdk.init(SENTRY_PUBLIC_DSN, integrations=[DjangoIntegration(), sentry_logging, CeleryIntegration()], send_default_pii=True)