diff --git a/.gitignore b/.gitignore index 09010b78e..faf371a8e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,45 @@ openstax/settings/local.py *.bk .vscode/settings.json .env + +# Django +*.pot +*.py[cod] +__pycache__/ +*.so +.Python +*.mo +*.sqlite3 +db.sqlite3 +db.sqlite3-journal +local_settings.py + +# Wagtail +*.bak +*.swp +*.swo +*~ + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Unit test / coverage reports +.tox/ +.coverage.* +coverage.xml +*.cover diff --git a/accounts/__init__.py b/accounts/__init__.py index e69de29bb..ac4d783d6 100644 --- a/accounts/__init__.py +++ b/accounts/__init__.py @@ -0,0 +1,5 @@ +""" +Accounts app for OpenStax CMS. +""" + +default_app_config = 'accounts.apps.AccountsConfig' diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 000000000..affbb33cd --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,21 @@ +""" +App configuration for the accounts app. +""" + +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + """ + App configuration for the accounts app. + """ + name = 'accounts' + verbose_name = 'Accounts' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + # Import signal handlers + # import accounts.signals \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py index e69de29bb..e604e9550 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -0,0 +1,5 @@ +""" +API app for OpenStax CMS. +""" + +default_app_config = 'api.apps.ApiConfig' diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 000000000..0e28bb303 --- /dev/null +++ b/api/apps.py @@ -0,0 +1,20 @@ +""" +App configuration for the api app. +""" + +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + """ + App configuration for the api app. + """ + name = 'api' + verbose_name = 'API' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + pass diff --git a/books/__init__.py b/books/__init__.py index e69de29bb..ce3b03489 100644 --- a/books/__init__.py +++ b/books/__init__.py @@ -0,0 +1,5 @@ +""" +Books app for OpenStax CMS. +""" + +default_app_config = 'books.apps.BooksConfig' diff --git a/books/apps.py b/books/apps.py new file mode 100644 index 000000000..f309fa3c1 --- /dev/null +++ b/books/apps.py @@ -0,0 +1,21 @@ +""" +App configuration for the books app. +""" + +from django.apps import AppConfig + + +class BooksConfig(AppConfig): + """ + App configuration for the books app. + """ + name = 'books' + verbose_name = 'Books' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + # Import signal handlers if they exist + # import books.signals \ No newline at end of file diff --git a/donations/__init__.py b/donations/__init__.py index e69de29bb..5496f5899 100644 --- a/donations/__init__.py +++ b/donations/__init__.py @@ -0,0 +1,5 @@ +""" +Donations app for OpenStax CMS. +""" + +default_app_config = 'donations.apps.DonationsConfig' diff --git a/donations/apps.py b/donations/apps.py index 8d1def31b..e9d832fa5 100644 --- a/donations/apps.py +++ b/donations/apps.py @@ -1,5 +1,21 @@ +""" +App configuration for the donations app. +""" + from django.apps import AppConfig class DonationsConfig(AppConfig): + """ + App configuration for the donations app. + """ name = 'donations' + verbose_name = 'Donations' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + # Import signal handlers if they exist + # import donations.signals diff --git a/errata/__init__.py b/errata/__init__.py index e69de29bb..7d065d2d1 100644 --- a/errata/__init__.py +++ b/errata/__init__.py @@ -0,0 +1,5 @@ +""" +Errata app for OpenStax CMS. +""" + +default_app_config = 'errata.apps.ErrataConfig' diff --git a/errata/apps.py b/errata/apps.py new file mode 100644 index 000000000..a15d23bcf --- /dev/null +++ b/errata/apps.py @@ -0,0 +1,19 @@ +""" +App configuration for the errata app. +""" + +from django.apps import AppConfig + + +class ErrataConfig(AppConfig): + """ + App configuration for the errata app. + """ + name = 'errata' + verbose_name = 'Errata' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ diff --git a/global_settings/__init__.py b/global_settings/__init__.py index 8c26b66d6..e348b9987 100644 --- a/global_settings/__init__.py +++ b/global_settings/__init__.py @@ -1 +1,5 @@ -#default_app_config = 'global_settings.apps.GlobalSettingsConfig' +""" +Global Settings app for OpenStax CMS. +""" + +default_app_config = 'global_settings.apps.GlobalSettingsConfig' diff --git a/global_settings/apps.py b/global_settings/apps.py index adc3f8fb6..4cbf8108a 100644 --- a/global_settings/apps.py +++ b/global_settings/apps.py @@ -1,12 +1,22 @@ +""" +App configuration for the global_settings app. +""" + from django.apps import AppConfig class GlobalSettingsConfig(AppConfig): + """ + App configuration for the global_settings app. + """ name = 'global_settings' - verbose_name = 'global_settings' - default = True - + verbose_name = 'Global Settings' + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ import global_settings.signals # noqa import api.signals import donations.signals diff --git a/mail/__init__.py b/mail/__init__.py index e69de29bb..e71448a5e 100644 --- a/mail/__init__.py +++ b/mail/__init__.py @@ -0,0 +1,5 @@ +""" +Mail app for OpenStax CMS. +""" + +default_app_config = 'mail.apps.MailConfig' diff --git a/mail/apps.py b/mail/apps.py new file mode 100644 index 000000000..572bbc021 --- /dev/null +++ b/mail/apps.py @@ -0,0 +1,21 @@ +""" +App configuration for the mail app. +""" + +from django.apps import AppConfig + + +class MailConfig(AppConfig): + """ + App configuration for the mail app. + """ + name = 'mail' + verbose_name = 'Mail' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + # Import signal handlers if they exist + # import mail.signals \ No newline at end of file diff --git a/news/__init__.py b/news/__init__.py index e69de29bb..0f5b9bc3a 100644 --- a/news/__init__.py +++ b/news/__init__.py @@ -0,0 +1,5 @@ +""" +News app for OpenStax CMS. +""" + +default_app_config = 'news.apps.NewsConfig' diff --git a/news/apps.py b/news/apps.py new file mode 100644 index 000000000..47b6d25b7 --- /dev/null +++ b/news/apps.py @@ -0,0 +1,21 @@ +""" +App configuration for the news app. +""" + +from django.apps import AppConfig + + +class NewsConfig(AppConfig): + """ + App configuration for the news app. + """ + name = 'news' + verbose_name = 'News' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + # Import signal handlers if they exist + # import news.signals \ No newline at end of file diff --git a/openstax/settings/__init__.py b/openstax/settings/__init__.py index 9b5ed21c9..d5ed97c92 100644 --- a/openstax/settings/__init__.py +++ b/openstax/settings/__init__.py @@ -1 +1,27 @@ +""" +OpenStax CMS Settings + +This module imports the appropriate settings based on the environment. +""" + +import os + +# Determine the environment +ENVIRONMENT = os.getenv('ENVIRONMENT', 'local') + +# Import the base settings from .base import * + +# Import environment-specific settings +if ENVIRONMENT == 'prod': + from .prod import * +elif ENVIRONMENT == 'test': + from .test import * +elif ENVIRONMENT == 'docker': + from .docker import * +else: + # Local development + try: + from .local import * + except ImportError: + pass diff --git a/openstax/settings/components/__init__.py b/openstax/settings/components/__init__.py new file mode 100644 index 000000000..a2d98b213 --- /dev/null +++ b/openstax/settings/components/__init__.py @@ -0,0 +1,20 @@ +""" +Settings components for OpenStax CMS. + +This package contains modular settings components that can be imported +and combined to create the final settings for different environments. +""" + +from . import core +from . import apps +from . import database +from . import storage +from . import security +from . import logging +from . import api +from . import accounts +from . import cron +from . import i18n +from . import sentry +from . import wagtail +from . import caching \ No newline at end of file diff --git a/openstax/settings/components/accounts.py b/openstax/settings/components/accounts.py new file mode 100644 index 000000000..7326660fd --- /dev/null +++ b/openstax/settings/components/accounts.py @@ -0,0 +1,35 @@ +""" +OpenStax Accounts settings for OpenStax CMS. + +This module contains settings related to OpenStax Accounts integration. +""" + +import os + +# Base URL for accounts +BASE_URL = os.getenv('BASE_URL', 'https://openstax.org') + +# OpenStax Accounts settings +ACCOUNTS_URL = os.getenv('ACCOUNTS_DOMAIN', f'{BASE_URL}/accounts') +AUTHORIZATION_URL = os.getenv('ACCOUNTS_AUTHORIZATION_URL', f'{ACCOUNTS_URL}/oauth/authorize') +ACCESS_TOKEN_URL = os.getenv('ACCOUNTS_ACCESS_TOKEN_URL', f'{ACCOUNTS_URL}/oauth/token') +USER_QUERY = os.getenv('ACCOUNTS_USER_QUERY', f'{ACCOUNTS_URL}/api/user?') +USERS_QUERY = os.getenv('ACCOUNTS_USERS_QUERY', f'{ACCOUNTS_URL}/api/users?') + +# Social auth settings +SOCIAL_AUTH_OPENSTAX_KEY = os.getenv('SOCIAL_AUTH_OPENSTAX_KEY') +SOCIAL_AUTH_OPENSTAX_SECRET = os.getenv('SOCIAL_AUTH_OPENSTAX_SECRET') +SOCIAL_AUTH_LOGIN_REDIRECT_URL = os.getenv('SOCIAL_AUTH_LOGIN_REDIRECT_URL', BASE_URL) +SOCIAL_AUTH_SANITIZE_REDIRECTS = os.getenv('SOCIAL_AUTH_SANITIZE_REDIRECTS') == 'True' + +# SSO settings +SSO_COOKIE_NAME = os.getenv('SSO_COOKIE_NAME', 'oxa') +SIGNATURE_PUBLIC_KEY = os.getenv('SSO_SIGNATURE_PUBLIC_KEY', """ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjvO/E8lO+ZJ7JMglbJyiF5/Ae +IIS2NKbIAMLBMPVBQY7mSqo6j/yxdVNKZCzYAMDWc/VvEfXQQJ2ipIUuDvO+SOwz +MewQ70hC71hC4s3dmOSLnixDJlnsVpcnKPEFXloObk/fcpK2Vw27e+yY+kIFmV2X +zrvTnmm9UJERp6tVTQIDAQAB +-----END PUBLIC KEY----- +""") +ENCRYPTION_PRIVATE_KEY = os.getenv('SSO_ENCRYPTION_PRIVATE_KEY', "c6d9b8683fddce8f2a39ac0565cf18ee") \ No newline at end of file diff --git a/openstax/settings/components/api.py b/openstax/settings/components/api.py new file mode 100644 index 000000000..fc8944eb7 --- /dev/null +++ b/openstax/settings/components/api.py @@ -0,0 +1,20 @@ +""" +REST Framework settings for OpenStax CMS. + +This module contains settings for Django REST Framework. +""" + +# Django Rest Framework settings +REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.TokenAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.AllowAny', + ), + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning', + 'DEFAULT_VERSION': 'v1', + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 100 +} \ No newline at end of file diff --git a/openstax/settings/components/apps.py b/openstax/settings/components/apps.py new file mode 100644 index 000000000..b35203a2c --- /dev/null +++ b/openstax/settings/components/apps.py @@ -0,0 +1,118 @@ +""" +Installed apps and middleware settings for OpenStax CMS. + +This module contains the list of installed apps and middleware. +""" + +# Installed apps +INSTALLED_APPS = [ + # Django core apps + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.postgres', + 'django.contrib.admin', + 'django.contrib.sitemaps', + + # Third-party apps + 'compressor', + 'taggit', + 'modelcluster', + 'rest_framework', + 'rest_framework.authtoken', + 'rest_auth', + 'django_crontab', + 'django_filters', + 'storages', + 'django_ses', + 'import_export', + 'rangefilter', + 'reversion', + 'wagtail_modeladmin', + + # Custom apps + 'accounts', + 'api', + 'pages', + 'books', + 'news', + 'snippets', + 'salesforce', + 'mail', + 'global_settings', + 'errata', + 'redirects', + 'oxauth', + 'webinars', + 'donations', + 'wagtailimportexport', + 'versions', + 'oxmenus', + + # Wagtail apps + 'wagtail', + 'wagtail.admin', + 'wagtail.documents', + 'wagtail.snippets', + 'wagtail.users', + 'wagtail.images', + 'wagtail.embeds', + 'wagtail.search', + 'wagtail.contrib.redirects', + 'wagtail.contrib.simple_translation', + 'wagtail.locales', + 'wagtail.contrib.forms', + 'wagtail.sites', + 'wagtail.api.v2', + 'wagtail.contrib.settings', +] + +# Middleware +MIDDLEWARE = [ + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'healthcheck.middleware.HealthCheckMiddleware', # has to be before CommonMiddleware + 'openstax.middleware.CommonMiddlewareAppendSlashWithoutRedirect', + 'openstax.middleware.CommonMiddlewareOpenGraphRedirect', + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'wagtail.contrib.redirects.middleware.RedirectMiddleware', + 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.cache.FetchFromCacheMiddleware', +] + +# Authentication backends +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', +) + +# Templates +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': False, + '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.contrib.messages.context_processors.messages', + 'django.template.context_processors.request', + ], + 'loaders': [ + ('django.template.loaders.cached.Loader', [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ]), + ], + }, + }, +] \ No newline at end of file diff --git a/openstax/settings/components/caching.py b/openstax/settings/components/caching.py new file mode 100644 index 000000000..4c4a7efaa --- /dev/null +++ b/openstax/settings/components/caching.py @@ -0,0 +1,49 @@ +""" +Caching settings for OpenStax CMS. + +This module contains caching-related settings for the application. +""" + +import os +from .core import BASE_DIR + +# Cache settings +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': os.getenv('REDIS_URL', 'redis://127.0.0.1:6379/1'), + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + 'PARSER_CLASS': 'redis.connection.HiredisParser', + 'CONNECTION_POOL_CLASS': 'redis.BlockingConnectionPool', + 'CONNECTION_POOL_CLASS_KWARGS': { + 'max_connections': 50, + 'timeout': 20, + }, + 'MAX_CONNECTIONS': 1000, + 'RETRY_ON_TIMEOUT': True, + 'SOCKET_TIMEOUT': 5, + 'SOCKET_CONNECT_TIMEOUT': 5, + 'KEY_PREFIX': 'openstax_cms', + 'TIMEOUT': 60 * 60 * 24, # 24 hours + } +} + +# Cache middleware settings +CACHE_MIDDLEWARE_ALIAS = 'default' +CACHE_MIDDLEWARE_SECONDS = 60 * 60 * 24 # 24 hours +CACHE_MIDDLEWARE_KEY_PREFIX = 'openstax_cms' + +# Session settings +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' +SESSION_CACHE_ALIAS = 'default' + +# Wagtail cache settings +WAGTAIL_CACHE = True +WAGTAIL_CACHE_BACKEND = 'default' +WAGTAIL_CACHE_TIMEOUT = 60 * 60 * 24 # 24 hours + +# Template fragment caching +TEMPLATE_FRAGMENT_CACHE_TIMEOUT = 60 * 60 * 24 # 24 hours + +# Database query caching +DB_CACHE_TIMEOUT = 60 * 60 * 24 # 24 hours \ No newline at end of file diff --git a/openstax/settings/components/core.py b/openstax/settings/components/core.py new file mode 100644 index 000000000..fb8f5a6d4 --- /dev/null +++ b/openstax/settings/components/core.py @@ -0,0 +1,70 @@ +""" +Core Django settings for OpenStax CMS. + +This module contains the fundamental Django settings that are common across all environments. +""" + +import os +import sys +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent + +# Default to a key starting with django-insecure for local development. +SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-wq21wtjo3@d_qfjvd-#td!%7gfy2updj2z+nev^k$iy%=m4_tr') + +# Determine if we're in debug mode +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' + +# Set allowed hosts based on environment +if DEBUG: + ALLOWED_HOSTS = ['*'] +else: + ENVIRONMENT = os.getenv('ENVIRONMENT', 'local') + if ENVIRONMENT == 'prod': + # Prod only + ALLOWED_HOSTS = ['openstax.org'] + else: + # All non-local and non-prod environments - allow all subdomains of openstax.org + ALLOWED_HOSTS = [".openstax.org"] + +# These should both be set to true. The openstax.middleware will handle resolving the URL +# without a redirect if needed. +APPEND_SLASH = True +WAGTAIL_APPEND_SLASH = True + +# urls.W002 warns about slashes at the start of URLs. But we need those so +# we don't have to have slashes at the end of URLs. So ignore. +SILENCED_SYSTEM_CHECKS = ['urls.W002'] + +# Admin configuration +ADMINS = ('Michael Volo', 'volo@rice.edu') + +# Default auto field +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +SITE_ID = 1 + +# URL configuration +ROOT_URLCONF = 'openstax.urls' +WSGI_APPLICATION = 'openstax.wsgi.application' + +# Base URL configuration +BASE_URL = os.getenv('BASE_URL') +if BASE_URL is None: + APPLICATION_DOMAIN = os.getenv('APPLICATION_DOMAIN') + if APPLICATION_DOMAIN is None: + if ENVIRONMENT == 'prod': + APPLICATION_DOMAIN = 'openstax.org' + elif ENVIRONMENT == 'test': + APPLICATION_DOMAIN = 'dev.openstax.org' + elif ENVIRONMENT == 'local': + APPLICATION_DOMAIN = 'dev.openstax.org' + else: + APPLICATION_DOMAIN = f'{ENVIRONMENT}.openstax.org' + BASE_URL = f'https://{APPLICATION_DOMAIN}' + +# Server host (used to populate links in the email) +HOST_LINK = os.getenv('HOST_LINK', BASE_URL) +if DEBUG: + HOST_LINK = 'http://localhost:8000' \ No newline at end of file diff --git a/openstax/settings/components/cron.py b/openstax/settings/components/cron.py new file mode 100644 index 000000000..0f5b4a61f --- /dev/null +++ b/openstax/settings/components/cron.py @@ -0,0 +1,29 @@ +""" +Cron job settings for OpenStax CMS. + +This module contains settings for scheduled tasks using django-crontab. +""" + +import os + +# Environment variables +ENVIRONMENT = os.getenv('ENVIRONMENT', 'local') + +# Cron jobs +CRONJOBS = [ + # ('0 2 * * *', 'django.core.management.call_command', ['delete_resource_downloads']), + ('0 6 * * *', 'django.core.management.call_command', ['update_resource_downloads']), + ('0 0 8 * *', 'django.core.management.call_command', ['update_schools_and_mapbox']), + ('0 10 * * *', 'django.core.management.call_command', ['update_partners']), + ('0 11 * * *', 'django.core.management.call_command', ['sync_thank_you_notes']), +] + +# Add production-specific cron jobs +if ENVIRONMENT == 'prod': + CRONJOBS.append(('0 6 1 * *', 'django.core.management.call_command', ['check_redirects'])) + CRONJOBS.append(('0 0 * * *', 'django.core.management.call_command', ['update_opportunities'])) + +# Cron job configuration +CRONTAB_COMMAND_PREFIX = os.getenv('CRONTAB_COMMAND_PREFIX', '') +CRONTAB_COMMAND_SUFFIX = os.getenv('CRONTAB_COMMAND_SUFFIX', '') +CRONTAB_LOCK_JOBS = os.getenv('CRONTAB_LOCK_JOBS') != 'False' \ No newline at end of file diff --git a/openstax/settings/components/database.py b/openstax/settings/components/database.py new file mode 100644 index 000000000..cc679b2e5 --- /dev/null +++ b/openstax/settings/components/database.py @@ -0,0 +1,31 @@ +""" +Database settings for OpenStax CMS. + +This module contains database configuration settings. +""" + +import os + +# Database configuration +DATABASES = { + 'default': { + 'ENGINE': "django.db.backends.postgresql", + 'NAME': os.getenv('DATABASE_NAME', 'oscms_prodcms'), + 'USER': os.getenv('DATABASE_USER', 'postgres'), + 'PASSWORD': os.getenv('DATABASE_PASSWORD', 'postgres'), + 'HOST': os.getenv('DATABASE_HOST', 'localhost'), + 'PORT': os.getenv('DATABASE_PORT', '5432'), + 'CONN_MAX_AGE': 60, + 'OPTIONS': { + 'connect_timeout': 10, + } + } +} + +# Salesforce configuration +SALESFORCE = { + 'username': os.getenv('SALESFORCE_USERNAME'), + 'password': os.getenv('SALESFORCE_PASSWORD'), + 'security_token': os.getenv('SALESFORCE_SECURITY_TOKEN'), + 'host': os.getenv('SALESFORCE_HOST', 'test'), +} \ No newline at end of file diff --git a/openstax/settings/components/i18n.py b/openstax/settings/components/i18n.py new file mode 100644 index 000000000..f76f479e8 --- /dev/null +++ b/openstax/settings/components/i18n.py @@ -0,0 +1,21 @@ +""" +Internationalization settings for OpenStax CMS. + +This module contains settings related to internationalization and localization. +""" + +# Internationalization settings +TIME_ZONE = 'America/Chicago' +LANGUAGE_CODE = 'en-us' +USE_I18N = True +USE_L10N = True +USE_TZ = True +DATE_FORMAT = 'j F Y' + +# Wagtail i18n settings +WAGTAIL_I18N_ENABLED = True + +WAGTAIL_CONTENT_LANGUAGES = [ + ('en', "English"), + ('es', "Spanish"), +] \ No newline at end of file diff --git a/openstax/settings/components/logging.py b/openstax/settings/components/logging.py new file mode 100644 index 000000000..e22a432eb --- /dev/null +++ b/openstax/settings/components/logging.py @@ -0,0 +1,68 @@ +""" +Logging settings for OpenStax CMS. + +This module contains settings for Django logging. +""" + +import os +import logging.config +from django.utils.log import DEFAULT_LOGGING + +# Logging configuration +LOGGING_CONFIG = None +LOGLEVEL = os.environ.get('LOGLEVEL', 'error').upper() +logging.config.dictConfig({ + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'healthcheck_filter': { + '()': 'healthcheck.filter.HealthCheckFilter' + }, + }, + 'formatters': { + 'default': { + # exact format is not important, this is the minimum information + 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', + }, + 'django.server': DEFAULT_LOGGING['formatters']['django.server'], + }, + 'handlers': { + # disable logs set with null handler + 'null': { + 'class': 'logging.NullHandler', + }, + # console logs to stderr + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'default', + }, + 'django.server': { + **DEFAULT_LOGGING['handlers']['django.server'], + 'filters': ['healthcheck_filter'] + }, + }, + 'loggers': { + # default for all undefined Python modules + '': { + 'level': LOGLEVEL, + 'handlers': ['console'], + }, + # Our application code + 'openstax': { + 'level': LOGLEVEL, + 'handlers': ['console'], + 'propagate': False, + }, + 'django.security.DisallowedHost': { + 'handlers': ['null'], + 'propagate': False, + }, + 'django.request': { + 'level': 'ERROR', + 'handlers': ['console'], + 'propagate': False, + }, + # Default runserver request logging + 'django.server': DEFAULT_LOGGING['loggers']['django.server'], + }, +}) \ No newline at end of file diff --git a/openstax/settings/components/security.py b/openstax/settings/components/security.py new file mode 100644 index 000000000..780e19ee2 --- /dev/null +++ b/openstax/settings/components/security.py @@ -0,0 +1,34 @@ +""" +Security settings for OpenStax CMS. + +This module contains security-related settings for the application. +""" + +# HSTS settings +SECURE_HSTS_SECONDS = 31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + +# Additional security settings +SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = 'DENY' + +# Content Security Policy +CSP_DEFAULT_SRC = ("'self'",) +CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "https://fonts.googleapis.com") +CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "'unsafe-eval'", "https://www.google-analytics.com") +CSP_IMG_SRC = ("'self'", "data:", "https://assets.openstax.org") +CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com") +CSP_CONNECT_SRC = ("'self'",) +CSP_FRAME_SRC = ("'self'",) +CSP_OBJECT_SRC = ("'none'",) +CSP_MEDIA_SRC = ("'self'",) +CSP_FRAME_ANCESTORS = ("'none'",) +CSP_FORM_ACTION = ("'self'",) +CSP_BASE_URI = ("'self'",) +CSP_INCLUDE_NONCE_IN = ['script-src', 'style-src'] \ No newline at end of file diff --git a/openstax/settings/components/sentry.py b/openstax/settings/components/sentry.py new file mode 100644 index 000000000..d701c98bb --- /dev/null +++ b/openstax/settings/components/sentry.py @@ -0,0 +1,21 @@ +""" +Sentry settings for OpenStax CMS. + +This module contains settings for Sentry error tracking. +""" + +import os +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration + +# Environment variables +ENVIRONMENT = os.getenv('ENVIRONMENT', 'local') + +# Sentry configuration +sentry_sdk.init( + dsn=os.getenv('SENTRY_DSN'), + integrations=[DjangoIntegration()], + traces_sample_rate=0.4, # limit the number of errors sent from production - 40% + send_default_pii=True, # this will send the user id of admin users only to sentry to help with debugging + environment=ENVIRONMENT +) \ No newline at end of file diff --git a/openstax/settings/components/storage.py b/openstax/settings/components/storage.py new file mode 100644 index 000000000..498d985ec --- /dev/null +++ b/openstax/settings/components/storage.py @@ -0,0 +1,110 @@ +""" +Storage settings for OpenStax CMS. + +Environment Variables: + AWS_STORAGE_BUCKET_NAME: S3 bucket name for asset storage + AWS_STORAGE_DIR: Directory within the bucket + AWS_S3_CUSTOM_DOMAIN: Custom domain for S3 assets +""" + +import os +from .core import BASE_DIR + +# Environment variables +ENVIRONMENT = os.getenv('ENVIRONMENT', 'local') +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' + +# AWS settings +# Amazon SES mail settings +DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'noreply@openstax.org') +SERVER_EMAIL = os.getenv('SERVER_EMAIL', 'noreply@openstax.org') +AWS_SES_FROM_EMAIL = 'noreply@openstax.org' +AWS_SES_REGION_NAME = os.getenv('AWS_SES_REGION_NAME', 'us-west-2') +AWS_SES_REGION_ENDPOINT = os.getenv('AWS_SES_REGION_ENDPOINT', 'email.us-west-2.amazonaws.com') +# Default to dummy email backend. Configure dev/production/local backend +EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend') + +# S3 settings +AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME', 'openstax-assets') +AWS_STORAGE_DIR = os.getenv('AWS_STORAGE_DIR', 'oscms-test') +AWS_S3_CUSTOM_DOMAIN = os.getenv('AWS_S3_CUSTOM_DOMAIN', 'assets.openstax.org') +AWS_DEFAULT_ACL = 'public-read' +AWS_HEADERS = {'Access-Control-Allow-Origin': '*'} + +# Static and media files +# Use local storage for media and static files in local environment +if DEBUG or ENVIRONMENT == 'test': + DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' +else: + # S3 media storage using custom backend + MEDIAFILES_LOCATION = '{}/media'.format(AWS_STORAGE_DIR) + MEDIA_URL = "https://%s/%s/media/" % (AWS_S3_CUSTOM_DOMAIN, AWS_STORAGE_DIR) + DEFAULT_FILE_STORAGE = 'openstax.custom_storages.MediaStorage' + +# Static files are stored on the server and served by nginx +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' +WHITENOISE_MAX_AGE = 604800 # 7 days + +# Enhanced WhiteNoise configuration +WHITENOISE_USE_FINDERS = True +WHITENOISE_MANIFEST_STRICT = False +WHITENOISE_ALLOW_ALL_ORIGINS = True +WHITENOISE_INDEX_FILE = True +WHITENOISE_COMPRESS = True +WHITENOISE_MIMETYPES = { + 'application/javascript': 'application/javascript', + 'text/css': 'text/css', + 'image/svg+xml': 'image/svg+xml', + 'application/json': 'application/json', + 'application/xml': 'application/xml', + 'text/xml': 'text/xml', + 'text/plain': 'text/plain', + 'image/png': 'image/png', + 'image/jpeg': 'image/jpeg', + 'image/gif': 'image/gif', + 'image/webp': 'image/webp', + 'image/x-icon': 'image/x-icon', + 'font/woff': 'font/woff', + 'font/woff2': 'font/woff2', + 'font/ttf': 'font/ttf', + 'font/otf': 'font/otf', + 'font/eot': 'font/eot', +} + +# 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. +STATIC_ROOT = os.path.join(BASE_DIR, 'public', 'static') + +# URL prefix for static files. +# Example: "http://openstax.org/static/" +STATIC_URL = '/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', + 'compressor.finders.CompressorFinder', +] + +# A "public" directory is used so it can be set as the root directory for nginx +# Media (uploaded) files are stored in S3 +# Note: used only if DEFAULT_FILE_STORAGE is set to local storage (local config set by DEBUG) +MEDIA_ROOT = os.path.join(BASE_DIR, 'public', 'media') + +# django-compressor settings +COMPRESS_PRECOMPILERS = ( + ('text/x-scss', 'django_libsass.SassCompiler'), +) + +# Enhanced django-compressor settings for better performance +COMPRESS_ENABLED = not DEBUG +COMPRESS_OFFLINE = not DEBUG +COMPRESS_CSS_FILTERS = [ + 'compressor.filters.css_default.CssAbsoluteFilter', + 'compressor.filters.cssmin.rCSSMinFilter', +] +COMPRESS_JS_FILTERS = [ + 'compressor.filters.jsmin.JSMinFilter', +] +COMPRESS_STORAGE = 'compressor.storage.CompressorFileStorage' \ No newline at end of file diff --git a/openstax/settings/components/wagtail.py b/openstax/settings/components/wagtail.py new file mode 100644 index 000000000..adec0bd8b --- /dev/null +++ b/openstax/settings/components/wagtail.py @@ -0,0 +1,81 @@ +""" +Wagtail CMS settings for OpenStax CMS. + +This module contains settings specific to Wagtail CMS. +""" + +import os + +# Wagtail site settings +WAGTAIL_SITE_NAME = 'OpenStax' +WAGTAILADMIN_BASE_URL = os.getenv('BASE_URL', 'https://openstax.org') +WAGTAILAPI_BASE_URL = os.getenv('WAGTAILAPI_BASE_URL', WAGTAILADMIN_BASE_URL) + +# Wagtail API settings +WAGTAILAPI_LIMIT_MAX = None +WAGTAIL_USAGE_COUNT_ENABLED = False +WAGTAIL_GRAVATAR_PROVIDER_URL = '//www.gravatar.com/avatar' + +# Wagtail document settings +WAGTAILADMIN_EXTERNAL_LINK_CONVERSION = 'exact' +WAGTAIL_REDIRECTS_FILE_STORAGE = 'cache' +WAGTAILFORMS_HELP_TEXT_ALLOW_HTML = True + +# Disable the workflow, we don't use them +WAGTAIL_WORKFLOW_ENABLED = False + +# Wagtail search settings +WAGTAILSEARCH_BACKENDS = { + 'default': { + 'BACKEND': 'wagtail.search.backends.database', + } +} + +# Wagtail image settings +WAGTAILIMAGES_EXTENSIONS = ["gif", "jpg", "jpeg", "png", "webp", "svg"] +WAGTAILIMAGES_FORMAT_CONVERSIONS = { + 'webp': 'webp', + 'jpeg': 'webp', + 'jpg': 'webp', + 'png': 'webp', +} +WAGTAILIMAGES_MAX_UPLOAD_SIZE = 20 * 1024 * 1024 # 20MB +DATA_UPLOAD_MAX_NUMBER_FIELDS = 10240 + +# Wagtail rich text editor settings +WAGTAILADMIN_RICH_TEXT_EDITORS = { + 'default': { + 'WIDGET': 'wagtail.admin.rich_text.DraftailRichTextArea', + 'OPTIONS': { + 'features': ['h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'bold', + 'italic', + 'ol', + 'ul', + 'hr', + 'link', + 'document-link', + 'image', + 'embed', + 'code', + 'blockquote', + 'superscript', + 'subscript', + 'strikethrough'] + } + }, +} + +# Wagtail embed settings +from wagtail.embeds.oembed_providers import youtube, vimeo +WAGTAILEMBEDS_FINDERS = [ + { + 'class': 'wagtail.embeds.finders.oembed', + 'providers': [youtube, vimeo] + } +] \ No newline at end of file diff --git a/openstax/settings/docker.py b/openstax/settings/docker.py index e77515182..095c51758 100644 --- a/openstax/settings/docker.py +++ b/openstax/settings/docker.py @@ -1,17 +1,22 @@ +""" +Docker settings for OpenStax CMS. + +This module contains settings specific to the Docker environment. +""" + from .base import * # If local.py is present, any settings in it will override those in base.py and dev.py. # Use this for any settings that are specific to this one installation, such as developer API keys. # local.py should not be checked in to version control. +# API keys EMBEDLY_KEY = 'get-one-from-http://embed.ly/' # GOOGLE_MAPS_KEY = 'get-one-from-https://code.google.com/apis/console/?noredirect' -# It is strongly recommended that you define a SECRET_KEY here, where it won't be visible -# in your version control system. +# Secret key for Docker environment SECRET_KEY = 'enter-a-long-unguessable-string-here' - # When developing Wagtail templates, we recommend django-debug-toolbar # for keeping track of page rendering times. To use it: # pip install django-debug-toolbar==1.0.1 @@ -28,6 +33,8 @@ # DEBUG_TOOLBAR_CONFIG = { # 'INTERCEPT_REDIRECTS': False, # } + +# Add CORS headers INSTALLED_APPS += ( 'corsheaders', ) @@ -35,6 +42,7 @@ 'corsheaders.middleware.CorsMiddleware', ) +# Database settings for Docker DATABASES = { 'default': { 'ENGINE': "django.db.backends.postgresql", @@ -46,28 +54,33 @@ } } -SALESFORCE = { 'username' : '', - 'password' : '', # password might need to be concatinated with security_token e.g. 'mypass1231' - 'security_token' : '', - 'sandbox': True } +# Salesforce settings for Docker +SALESFORCE = { + 'username': '', + 'password': '', # password might need to be concatenated with security_token e.g. 'mypass1231' + 'security_token': '', + 'sandbox': True +} +# Mapbox token for Docker MAPBOX_TOKEN = '' # should be the sk from mapbox ################# # Media # ################# -# locally, we want to use local storage for uploaded (media) files +# Media settings for Docker DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' ################# # CORS # ################# +# CORS settings for Docker CORS_ORIGIN_ALLOW_ALL = False CORS_ALLOW_CREDENTIALS = True CORS_ORIGIN_WHITELIST = [ 'http://localhost:3000' # http://localhost:3000, not http://localhost:3000/ ] -# As of Django 1.10, we need to be explicit with localhost being allowed +# Allow localhost in Docker ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '0.0.0.0'] diff --git a/openstax/settings/local.py.example b/openstax/settings/local.py.example index f6b70341a..0365850b6 100644 --- a/openstax/settings/local.py.example +++ b/openstax/settings/local.py.example @@ -1,4 +1,13 @@ -from .base import INSTALLED_APPS, MIDDLEWARE +""" +Local development settings for OpenStax CMS. + +This is an example file for local development settings. +Copy this file to local.py and modify as needed. +""" + +from .base import * + +# Add development tools INSTALLED_APPS += ( 'debug_toolbar', 'django_extensions', @@ -6,32 +15,29 @@ INSTALLED_APPS += ( MIDDLEWARE += ( 'debug_toolbar.middleware.DebugToolbarMiddleware', ) -# django-debug-toolbar settings + +# Debug toolbar settings DEBUG_TOOLBAR_CONFIG = { 'INTERCEPT_REDIRECTS': False, } -# settings for django_extensions when graphing models + +# Django extensions settings GRAPH_MODELS = { 'all_applications': True, 'group_models': True, } -# Set something here, we shouldn't check any secret keys into version control +# Secret key for local development SECRET_KEY = 'enter-a-long-unguessable-string-here' -# make sure debug is on and set host to your local dev env url -DEBUG=True +# Enable debug mode +DEBUG = True -################# -# Media # -################# -# locally, we want to use local storage for uploaded (media) files +# Media settings for local development DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' -################# -# CORS # -################# +# CORS settings for local development CORS_ORIGIN_ALLOW_ALL = False CORS_ALLOW_CREDENTIALS = True CORS_ORIGIN_WHITELIST = ( diff --git a/openstax/settings/prod.py b/openstax/settings/prod.py new file mode 100644 index 000000000..c283fd45c --- /dev/null +++ b/openstax/settings/prod.py @@ -0,0 +1,28 @@ +""" +Production settings for OpenStax CMS. + +This module contains settings specific to the production environment. +""" + +from .base import * + +# Set environment +ENVIRONMENT = 'prod' +DEBUG = False + +# Security settings +SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = 'DENY' +SECURE_HSTS_SECONDS = 31536000 # 1 year +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True + +# Email settings +EMAIL_BACKEND = 'django_ses.SESBackend' + +# Sentry settings +SENTRY_DSN = os.getenv('SENTRY_DSN') \ No newline at end of file diff --git a/openstax/settings/test.py b/openstax/settings/test.py index 5b196c203..22c79bedf 100644 --- a/openstax/settings/test.py +++ b/openstax/settings/test.py @@ -1,11 +1,20 @@ +""" +Test settings for OpenStax CMS. + +This module contains settings specific to the test environment. +""" + from .base import * +# Set environment DEBUG = True ENVIRONMENT = 'test' +# Use local storage for media and static files DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' +# Salesforce settings for testing SALESFORCE = { 'username': 'sf@openstax.org', 'password': 'supersecret', @@ -13,6 +22,6 @@ 'host': 'test', } -# silence whitenoise warnings for CI +# Silence whitenoise warnings for CI import warnings warnings.filterwarnings("ignore", message="No directory at", module="whitenoise.base") diff --git a/openstax/wsgi.py b/openstax/wsgi.py index bcf1cd704..573926128 100644 --- a/openstax/wsgi.py +++ b/openstax/wsgi.py @@ -21,8 +21,10 @@ # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() +from whitenoise import WhiteNoise -# Apply WSGI middleware here. -# from helloworld.wsgi import HelloWorldApplication -# application = HelloWorldApplication(application) +# Get the Django WSGI application +django_application = get_wsgi_application() + +# Wrap the Django application with WhiteNoise +application = WhiteNoise(django_application) diff --git a/oxauth/__init__.py b/oxauth/__init__.py index e69de29bb..f7a568970 100644 --- a/oxauth/__init__.py +++ b/oxauth/__init__.py @@ -0,0 +1,5 @@ +""" +OpenStax Auth app for OpenStax CMS. +""" + +default_app_config = 'oxauth.apps.OxauthConfig' diff --git a/oxauth/apps.py b/oxauth/apps.py new file mode 100644 index 000000000..0722d0655 --- /dev/null +++ b/oxauth/apps.py @@ -0,0 +1,21 @@ +""" +App configuration for the oxauth app. +""" + +from django.apps import AppConfig + + +class OxauthConfig(AppConfig): + """ + App configuration for the oxauth app. + """ + name = 'oxauth' + verbose_name = 'OpenStax Auth' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + # Import signal handlers if they exist + # import oxauth.signals \ No newline at end of file diff --git a/oxmenus/__init__.py b/oxmenus/__init__.py index e69de29bb..66bea4085 100644 --- a/oxmenus/__init__.py +++ b/oxmenus/__init__.py @@ -0,0 +1,5 @@ +""" +OpenStax Menus app for OpenStax CMS. +""" + +default_app_config = 'oxmenus.apps.OxmenusConfig' diff --git a/pages/__init__.py b/pages/__init__.py index e69de29bb..998e4a696 100644 --- a/pages/__init__.py +++ b/pages/__init__.py @@ -0,0 +1,5 @@ +""" +OpenStax Menus app for OpenStax CMS. +""" + +default_app_config = 'pages.apps.PagesConfig' diff --git a/pages/apps.py b/pages/apps.py new file mode 100644 index 000000000..bcd6513a4 --- /dev/null +++ b/pages/apps.py @@ -0,0 +1,21 @@ +""" +App configuration for the pages app. +""" + +from django.apps import AppConfig + + +class PagesConfig(AppConfig): + """ + App configuration for the pages app. + """ + name = 'pages' + verbose_name = 'Pages' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + # Import signal handlers if they exist + # import redirects.signals \ No newline at end of file diff --git a/pages/tests.py b/pages/tests.py index 5f3f9a0f0..a22ae952c 100644 --- a/pages/tests.py +++ b/pages/tests.py @@ -69,6 +69,7 @@ def test_can_create_team_page(self): team_header="Our Team") self.homepage.add_child(instance=team_page) self.assertCanCreateAt(page_models.HomePage, page_models.TeamPage) + self.assertCanCreateAt(page_models.RootPage, page_models.TeamPage) revision = team_page.save_revision() revision.publish() team_page.save() @@ -88,7 +89,7 @@ def test_can_create_about_us_page(self): ) self.homepage.add_child(instance=about_us) self.assertCanCreateAt(page_models.HomePage, page_models.AboutUsPage) - + self.assertCanCreateAt(page_models.RootPage, page_models.AboutUsPage) retrieved_page = Page.objects.get(id=about_us.id) self.assertEqual(retrieved_page.title, "About Us") @@ -103,6 +104,7 @@ def test_can_create_k12_main_page(self): ) self.homepage.add_child(instance=k12_page) self.assertCanCreateAt(page_models.HomePage, page_models.K12MainPage) + self.assertCanCreateAt(page_models.RootPage, page_models.K12MainPage) retrieved_page = Page.objects.get(id=k12_page.id) self.assertEqual(retrieved_page.title, "K12 Main Page") @@ -116,6 +118,8 @@ def test_can_create_contact_us_page(self): ) self.homepage.add_child(instance=contact_us_page) self.assertCanCreateAt(page_models.HomePage, page_models.ContactUs) + self.assertCanCreateAt(page_models.RootPage, page_models.ContactUs) + retrieved_page = Page.objects.get(id=contact_us_page.id) self.assertEqual(retrieved_page.title, "Contact Us") @@ -143,6 +147,7 @@ def test_can_create_general_page(self): ) self.homepage.add_child(instance=general_page) self.assertCanCreateAt(page_models.HomePage, page_models.GeneralPage) + self.assertCanCreateAt(page_models.RootPage, page_models.GeneralPage) retrieved_page = Page.objects.get(id=general_page.id) self.assertEqual(retrieved_page.title, "General Page") @@ -170,7 +175,7 @@ def test_can_create_supporters_page(self): ) self.homepage.add_child(instance=supporters_page) self.assertCanCreateAt(page_models.HomePage, page_models.Supporters) - + self.assertCanCreateAt(page_models.RootPage, page_models.Supporters) retrieved_page = Page.objects.get(id=supporters_page.id) self.assertEqual(retrieved_page.title, "Supporters Page") @@ -181,6 +186,7 @@ def test_can_create_tos_page(self): ) self.homepage.add_child(instance=tos_page) self.assertCanCreateAt(page_models.HomePage, page_models.TermsOfService) + self.assertCanCreateAt(page_models.RootPage, page_models.TermsOfService) retrieved_page = Page.objects.get(id=tos_page.id) self.assertEqual(retrieved_page.title, "Terms of Service Page") @@ -228,6 +234,7 @@ def test_can_create_faq_page(self): ) self.homepage.add_child(instance=faq_page) self.assertCanCreateAt(page_models.HomePage, page_models.FAQ) + self.assertCanCreateAt(page_models.RootPage, page_models.FAQ) retrieved_page = Page.objects.get(id=faq_page.id) self.assertEqual(retrieved_page.title, "FAQ Page") @@ -239,7 +246,7 @@ def test_can_create_accessibility_page(self): ) self.homepage.add_child(instance=accessibility_page) self.assertCanCreateAt(page_models.HomePage, page_models.Accessibility) - + self.assertCanCreateAt(page_models.RootPage, page_models.Accessibility) retrieved_page = Page.objects.get(id=accessibility_page.id) self.assertEqual(retrieved_page.title, "Accessibility Page") @@ -250,6 +257,7 @@ def test_can_create_licensing_page(self): ) self.homepage.add_child(instance=licensing_page) self.assertCanCreateAt(page_models.HomePage, page_models.Licensing) + self.assertCanCreateAt(page_models.RootPage, page_models.Licensing) retrieved_page = Page.objects.get(id=licensing_page.id) self.assertEqual(retrieved_page.title, "Licensing Page") @@ -271,7 +279,7 @@ def test_can_create_technology_page(self): ) self.homepage.add_child(instance=technology_page) self.assertCanCreateAt(page_models.HomePage, page_models.Technology) - + self.assertCanCreateAt(page_models.RootPage, page_models.Technology) retrieved_page = Page.objects.get(id=technology_page.id) self.assertEqual(retrieved_page.title, "Technology Page") @@ -282,6 +290,7 @@ def test_can_create_careers_page(self): ) self.homepage.add_child(instance=careers_page) self.assertCanCreateAt(page_models.HomePage, page_models.Careers) + self.assertCanCreateAt(page_models.RootPage, page_models.Careers) retrieved_page = Page.objects.get(id=careers_page.id) self.assertEqual(retrieved_page.title, "Careers Page") @@ -430,6 +439,7 @@ def test_can_create_impact_page(self): ) self.homepage.add_child(instance=impact_page) self.assertCanCreateAt(page_models.HomePage, page_models.Impact) + self.assertCanCreateAt(page_models.RootPage, page_models.Impact) retrieved_page = Page.objects.get(id=impact_page.id) self.assertEqual(retrieved_page.title, "Impact Page") @@ -458,7 +468,7 @@ def test_can_create_learning_research_page(self): ) self.homepage.add_child(instance=research_page) self.assertCanCreateAt(page_models.HomePage, page_models.LearningResearchPage) - + self.assertCanCreateAt(page_models.RootPage, page_models.LearningResearchPage) retrieved_page = Page.objects.get(id=research_page.id) self.assertEqual(retrieved_page.title, "Learning Research Page") @@ -468,6 +478,7 @@ def test_can_create_webinar_page(self): ) self.homepage.add_child(instance=webinar_page) self.assertCanCreateAt(page_models.HomePage, page_models.WebinarPage) + self.assertCanCreateAt(page_models.RootPage, page_models.WebinarPage) retrieved_page = Page.objects.get(id=webinar_page.id) self.assertEqual(retrieved_page.title, "Webinar Page") @@ -481,7 +492,7 @@ def test_can_create_form_headings_page(self): ) self.homepage.add_child(instance=form_page) self.assertCanCreateAt(page_models.HomePage, page_models.FormHeadings) - + self.assertCanCreateAt(page_models.RootPage, page_models.FormHeadings) retrieved_page = Page.objects.get(id=form_page.id) self.assertEqual(retrieved_page.title, "Form Headings Page") @@ -498,7 +509,7 @@ def test_can_create_ally_logos_page(self): ) self.homepage.add_child(instance=ally_page) self.assertCanCreateAt(page_models.HomePage, page_models.AllyLogos) - + self.assertCanCreateAt(page_models.RootPage, page_models.AllyLogos) retrieved_page = Page.objects.get(id=ally_page.id) self.assertEqual(retrieved_page.title, "Ally Logos Page") @@ -514,7 +525,7 @@ def test_can_create_assignable_page(self): ) self.homepage.add_child(instance=assignable_page) self.assertCanCreateAt(page_models.HomePage, page_models.Assignable) - + self.assertCanCreateAt(page_models.RootPage, page_models.Assignable) retrieved_page = Page.objects.get(id=assignable_page.id) self.assertEqual(retrieved_page.title, "Assignable Page") diff --git a/redirects/__init__.py b/redirects/__init__.py index e69de29bb..b499f867f 100644 --- a/redirects/__init__.py +++ b/redirects/__init__.py @@ -0,0 +1,5 @@ +""" +Redirects app for OpenStax CMS. +""" + +default_app_config = 'redirects.apps.RedirectsConfig' diff --git a/redirects/apps.py b/redirects/apps.py new file mode 100644 index 000000000..40bbee850 --- /dev/null +++ b/redirects/apps.py @@ -0,0 +1,21 @@ +""" +App configuration for the redirects app. +""" + +from django.apps import AppConfig + + +class RedirectsConfig(AppConfig): + """ + App configuration for the redirects app. + """ + name = 'redirects' + verbose_name = 'Redirects' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + # Import signal handlers if they exist + # import redirects.signals \ No newline at end of file diff --git a/requirements/base.txt b/requirements/base.txt index 9e4509fbb..4b4b5dd4d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -17,6 +17,8 @@ django-ses django-storages django-taggit>=6.1.0 djangorestframework +django-redis +hiredis html2text # news feed mapbox Pillow diff --git a/salesforce/__init__.py b/salesforce/__init__.py index e69de29bb..a632ecef0 100644 --- a/salesforce/__init__.py +++ b/salesforce/__init__.py @@ -0,0 +1,5 @@ +""" +Salesforce app for OpenStax CMS. +""" + +default_app_config = 'salesforce.apps.SalesforceConfig' diff --git a/salesforce/apps.py b/salesforce/apps.py new file mode 100644 index 000000000..02b70c0fa --- /dev/null +++ b/salesforce/apps.py @@ -0,0 +1,20 @@ +""" +App configuration for the salesforce app. +""" + +from django.apps import AppConfig + + +class SalesforceConfig(AppConfig): + """ + App configuration for the salesforce app. + """ + name = 'salesforce' + verbose_name = 'Salesforce' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + pass diff --git a/snippets/__init__.py b/snippets/__init__.py index e69de29bb..61994bbc2 100644 --- a/snippets/__init__.py +++ b/snippets/__init__.py @@ -0,0 +1,5 @@ +""" +Snippets app for OpenStax CMS. +""" + +default_app_config = 'snippets.apps.SnippetsConfig' diff --git a/snippets/apps.py b/snippets/apps.py new file mode 100644 index 000000000..4dc46db20 --- /dev/null +++ b/snippets/apps.py @@ -0,0 +1,20 @@ +""" +App configuration for the snippets app. +""" + +from django.apps import AppConfig + + +class SnippetsConfig(AppConfig): + """ + App configuration for the snippets app. + """ + name = 'snippets' + verbose_name = 'Snippets' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + pass diff --git a/versions/__init__.py b/versions/__init__.py index e69de29bb..05d095b76 100644 --- a/versions/__init__.py +++ b/versions/__init__.py @@ -0,0 +1,5 @@ +""" +Versions app for OpenStax CMS. +""" + +default_app_config = 'versions.apps.VersionsConfig' diff --git a/versions/apps.py b/versions/apps.py new file mode 100644 index 000000000..3b62fe880 --- /dev/null +++ b/versions/apps.py @@ -0,0 +1,21 @@ +""" +App configuration for the versions app. +""" + +from django.apps import AppConfig + + +class VersionsConfig(AppConfig): + """ + App configuration for the versions app. + """ + name = 'versions' + verbose_name = 'Versions' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + # Import signal handlers if they exist + # import versions.signals \ No newline at end of file diff --git a/webinars/__init__.py b/webinars/__init__.py index e69de29bb..ed1995ee5 100644 --- a/webinars/__init__.py +++ b/webinars/__init__.py @@ -0,0 +1,5 @@ +""" +Webinars app for OpenStax CMS. +""" + +default_app_config = 'webinars.apps.WebinarsConfig' diff --git a/webinars/apps.py b/webinars/apps.py new file mode 100644 index 000000000..396959b8d --- /dev/null +++ b/webinars/apps.py @@ -0,0 +1,20 @@ +""" +App configuration for the webinars app. +""" + +from django.apps import AppConfig + + +class WebinarsConfig(AppConfig): + """ + App configuration for the webinars app. + """ + name = 'webinars' + verbose_name = 'Webinars' + + def ready(self): + """ + Perform initialization when the app is ready. + Import signal handlers here to avoid circular imports. + """ + pass