Oleg Komarov
bd3a17a19b
Implementation for #72 based on https://github.com/justquick/django-activity-stream/ Summary: - create an Action object, specifying a corresponding Extension object as a `target` and other relevant objects (Rating, ApprovalActivity, AbuseReport) as `action_object` (search code for `action.send` to see where it happens) - there is one exception when we don't have an `action_object` - when we submit a new extension for a review, but this could be restructured as creating a first ApprovalActivity item, TODO revisit this - use `follow` provided by actstream to specify relations between users and extensions, use different flags (author, moderator, reviewer) to explicitly manage those relations; proactively call the `follow` to make sure that users are subscribed before an Action interesting to them is created; - one downside here is that each moderator needs to follow each extensions individually, but this potentially allows to explicitly unsubscribe from activity on extensions that are not interesting to a given user - introduce `VERB2FLAGS` mapping to define when a given Action needs to generate a Notification for a follower of a particular type (=flag) based on the Action's verb - process Notification records by a new `send_notifications` management command that sends emails - add a profile setting to disable notification emails First iteration includes only internal (`@blender.org`) emails. If you have a DB with some preexisting data, you need to run `./manage.py ensure_followers` command to retroactively create expected follow relations. Next steps (out of scope for this PR): - refine notification texts: current `Verb` usage may be not grammatical and is not covered by i18n - UI: templates for showing notifications in user profile, marking notifications as read, unread counter in the header (there is some views code in this PR, but it it not surfaced to the users yet) - remove the internal email check Co-authored-by: Anna Sirota <railla@noreply.localhost> Reviewed-on: #80 Reviewed-by: Anna Sirota <railla@noreply.localhost>
328 lines
9.3 KiB
Python
328 lines
9.3 KiB
Python
"""Django settings for blender_extensions project.
|
|
|
|
Generated by 'django-admin startproject' using Django 4.0.6.
|
|
|
|
For more information on this file, see
|
|
https://docs.djangoproject.com/en/4.0/topics/settings/
|
|
|
|
For the full list of settings and their values, see
|
|
https://docs.djangoproject.com/en/4.0/ref/settings/
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import os
|
|
import sys
|
|
|
|
import dj_database_url
|
|
|
|
TESTING = sys.argv[1:2] == ['test']
|
|
|
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
|
|
ADMIN_SITE_HEADER = 'Blender Extensions Admin'
|
|
|
|
# Quick-start development settings - unsuitable for production
|
|
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
|
|
|
# SECURITY WARNING: keep the secret key used in production secret!
|
|
SECRET_KEY = os.environ.get(
|
|
'SECRET_KEY',
|
|
'django-insecure-@!bx83z!nudfc3uvfmm+)n0a+ms1zz&z9jivwwj$&70&d48h2t',
|
|
)
|
|
|
|
# SECURITY WARNING: don't run with debug turned on in production!
|
|
DEBUG = bool(os.environ.get('DEBUG', True))
|
|
|
|
APPEND_SLASH = True
|
|
|
|
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'extensions.local').split(',')
|
|
|
|
AUTH_USER_MODEL = 'users.User'
|
|
|
|
# Application definition
|
|
|
|
INSTALLED_APPS = [
|
|
'users',
|
|
'teams',
|
|
'emails',
|
|
'releases',
|
|
'abuse',
|
|
'extensions',
|
|
'background_task',
|
|
'blender_id_oauth_client',
|
|
'common',
|
|
'files',
|
|
'loginas',
|
|
'notifications',
|
|
'pipeline',
|
|
'ratings',
|
|
'rangefilter',
|
|
'reviewers',
|
|
'stats',
|
|
'taggit',
|
|
'drf_spectacular',
|
|
'drf_spectacular_sidecar',
|
|
'rest_framework',
|
|
'waffle',
|
|
'django.contrib.admin',
|
|
'django.contrib.auth',
|
|
'django.contrib.contenttypes',
|
|
'django.contrib.sessions',
|
|
'django.contrib.sites',
|
|
'django.contrib.messages',
|
|
'django.contrib.staticfiles',
|
|
'django.contrib.flatpages',
|
|
'django.contrib.humanize',
|
|
'actstream',
|
|
]
|
|
|
|
MIDDLEWARE = [
|
|
'django.middleware.security.SecurityMiddleware',
|
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
'django.middleware.common.CommonMiddleware',
|
|
'django.middleware.csrf.CsrfViewMiddleware',
|
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
'django.contrib.messages.middleware.MessageMiddleware',
|
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
'common.middleware.threadlocal.ThreadLocalMiddleware',
|
|
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
|
|
]
|
|
|
|
ROOT_URLCONF = 'blender_extensions.urls'
|
|
|
|
TEMPLATES = [
|
|
{
|
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
'DIRS': [
|
|
BASE_DIR / 'assets_shared' / 'src' / 'templates',
|
|
],
|
|
'APP_DIRS': True,
|
|
'OPTIONS': {
|
|
'context_processors': [
|
|
'django.template.context_processors.debug',
|
|
'django.template.context_processors.request',
|
|
'django.contrib.auth.context_processors.auth',
|
|
'django.contrib.messages.context_processors.messages',
|
|
'loginas.context_processors.impersonated_session_status',
|
|
'common.context_processors.extra_context',
|
|
],
|
|
},
|
|
},
|
|
]
|
|
|
|
WSGI_APPLICATION = 'blender_extensions.wsgi.application'
|
|
|
|
|
|
# Database
|
|
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
|
|
|
DATABASES = {
|
|
'default': dj_database_url.config(default='sqlite:///{}'.format(BASE_DIR / 'db.sqlite3')),
|
|
}
|
|
|
|
# Password validation
|
|
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
|
|
|
|
AUTH_PASSWORD_VALIDATORS = [
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
},
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
},
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
},
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
},
|
|
]
|
|
|
|
|
|
# Internationalization
|
|
# https://docs.djangoproject.com/en/4.0/topics/i18n/
|
|
|
|
LANGUAGE_CODE = 'en-us'
|
|
|
|
TIME_ZONE = 'UTC'
|
|
|
|
USE_I18N = True
|
|
|
|
USE_TZ = True
|
|
|
|
SITE_ID = 1
|
|
|
|
# Static files (CSS, JavaScript, Images)
|
|
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
|
|
|
STATIC_URL = 'static/'
|
|
MEDIA_URL = 'media/'
|
|
STATIC_ROOT = os.environ.get('STATIC_ROOT', BASE_DIR / 'public/static')
|
|
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', BASE_DIR / 'public/media')
|
|
|
|
STATICFILES_DIRS = [BASE_DIR / 'assets_shared' / 'src', BASE_DIR / 'assets_shared' / 'assets']
|
|
STATICFILES_FINDERS = [
|
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
|
'pipeline.finders.PipelineFinder',
|
|
]
|
|
STATICFILES_STORAGE = 'pipeline.storage.PipelineManifestStorage'
|
|
|
|
# Default primary key field type
|
|
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
|
|
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
|
|
LOGGING = {
|
|
'version': 1,
|
|
'disable_existing_loggers': False,
|
|
'formatters': {
|
|
'default': {'format': '%(asctime)-15s %(levelname)8s %(name)s %(message)s'},
|
|
'verbose': {
|
|
'format': (
|
|
'%(asctime)s %(levelname)8s [%(pathname)s:%(lineno)d '
|
|
'%(funcName)s] %(name)s %(message)s '
|
|
),
|
|
},
|
|
},
|
|
'handlers': {
|
|
'console': {
|
|
'class': 'logging.StreamHandler',
|
|
'formatter': 'verbose',
|
|
},
|
|
},
|
|
'loggers': {
|
|
'django': {'level': 'INFO'},
|
|
},
|
|
'root': {'level': 'INFO', 'handlers': ['console']},
|
|
}
|
|
|
|
PIPELINE = {
|
|
'JS_COMPRESSOR': 'pipeline.compressors.jsmin.JSMinCompressor',
|
|
'CSS_COMPRESSOR': 'pipeline.compressors.NoopCompressor',
|
|
'JAVASCRIPT': {
|
|
'common': {
|
|
'source_filenames': (
|
|
'common/scripts/*.js',
|
|
'scripts/tutti/*.js',
|
|
),
|
|
'output_filename': 'js/common.js',
|
|
},
|
|
'extensions': {
|
|
'source_filenames': ('extensions/scripts/*.js',),
|
|
'output_filename': 'js/extensions.js',
|
|
},
|
|
},
|
|
'STYLESHEETS': {
|
|
'common': {
|
|
'source_filenames': (
|
|
'common/styles/main.sass',
|
|
'common/styles/*.scss',
|
|
),
|
|
'output_filename': 'css/common.css',
|
|
'extra_context': {'media': 'screen,projection'},
|
|
},
|
|
},
|
|
'COMPILERS': ('libsasscompiler.LibSassCompiler',),
|
|
'DISABLE_WRAPPER': True,
|
|
}
|
|
|
|
# Blender ID login and logout URLs
|
|
LOGIN_URL = '/oauth/login'
|
|
LOGOUT_URL = '/oauth/logout'
|
|
|
|
SENTRY_DSN = os.environ.get('SENTRY_DSN')
|
|
if SENTRY_DSN:
|
|
import sentry_sdk
|
|
from sentry_sdk.integrations.django import DjangoIntegration
|
|
|
|
sentry_sdk.init(
|
|
dsn=SENTRY_DSN,
|
|
integrations=[
|
|
DjangoIntegration(),
|
|
],
|
|
# Set traces_sample_rate to 1.0 to capture 100%
|
|
# of transactions for performance monitoring.
|
|
# We recommend adjusting this value in production.
|
|
traces_sample_rate=1.0,
|
|
# If you wish to associate users to errors (assuming you are using
|
|
# django.contrib.auth) you may enable sending PII data.
|
|
send_default_pii=False,
|
|
)
|
|
|
|
BLENDER_ID = {
|
|
# MUST end in a slash:
|
|
'BASE_URL': os.environ.get('BID_BASE_URL', 'http://id.local:8000/'),
|
|
'OAUTH_CLIENT': os.environ.get('BID_OAUTH_CLIENT', 'BLENDER-EXTENSIONS-DEV'),
|
|
'OAUTH_SECRET': os.environ.get(
|
|
'BID_OAUTH_SECRET', 'DEVELOPMENT-ONLY NON SECRET NEVER USE IN PRODUCTION'
|
|
),
|
|
'WEBHOOK_USER_MODIFIED_SECRET': os.environ.get(
|
|
'BID_WEBHOOK_USER_MODIFIED_SECRET', 'DEVELOPMENT-ONLY NON SECRET NEVER USE IN PRODUCTION'
|
|
),
|
|
}
|
|
|
|
TAGGIT_CASE_INSENSITIVE = True
|
|
|
|
ACTSTREAM_SETTINGS = {
|
|
'MANAGER': 'users.managers.CustomStreamManager',
|
|
'FETCH_RELATIONS': True,
|
|
}
|
|
|
|
REST_FRAMEWORK = {
|
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
|
}
|
|
|
|
SPECTACULAR_SETTINGS = {
|
|
'TITLE': 'Blender Extensions Platform API',
|
|
'DESCRIPTION': 'API for the Blender Extensions Platform website',
|
|
'VERSION': '1.0.0',
|
|
'SERVE_INCLUDE_SCHEMA': False,
|
|
# We use DRF sidecar so we don't rely on CDNs
|
|
'SWAGGER_UI_DIST': 'SIDECAR',
|
|
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
|
'REDOC_DIST': 'SIDECAR',
|
|
}
|
|
|
|
# Fallback user for logging
|
|
SYSTEM_USER_ID = 1
|
|
|
|
if TESTING:
|
|
# Avoid "ValueError: Missing staticfiles manifest entry for"
|
|
STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'
|
|
# Disable logging output when running tests
|
|
LOGGING = {
|
|
'version': 1,
|
|
'loggers': {
|
|
'': {
|
|
'level': 'CRITICAL',
|
|
},
|
|
},
|
|
}
|
|
|
|
if DEBUG:
|
|
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
|
|
INSTALLED_APPS.append('debug_toolbar')
|
|
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
|
|
INTERNAL_IPS = [
|
|
'127.0.0.1',
|
|
]
|
|
|
|
# Email configuration, by default uses console output
|
|
EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend')
|
|
DEFAULT_REPLY_TO_EMAIL = os.getenv(
|
|
'DEFAULT_REPLY_TO_EMAIL', 'Blender Extensions Platform <extensions@blender.org>'
|
|
)
|
|
DEFAULT_FROM_EMAIL = os.getenv(
|
|
'DEFAULT_FROM_EMAIL', 'Blender Extensions Platform <extensions@blender.org>'
|
|
)
|
|
EMAIL_HOST = os.getenv('EMAIL_HOST')
|
|
EMAIL_PORT = os.getenv('EMAIL_PORT', '587')
|
|
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
|
|
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
|
|
|
|
ACTSTREAM_SETTINGS = {
|
|
'MANAGER': 'actstream.managers.ActionManager',
|
|
}
|