From 44de49cbb07f3bfe93888897b792b8b42ff37df2 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 28 Sep 2017 17:25:10 -0400 Subject: [PATCH 01/16] Refactor settings into importable base. User deployment should use 'from allianceauth.settings.base import *' in their project settings. --- alliance_auth/.gitignore | 2 - alliance_auth/settings.py.example | 566 ------------------ .../settings}/__init__.py | 0 allianceauth/settings/base.py | 296 +++++++++ {alliance_auth => allianceauth}/wsgi.py | 2 +- manage.py | 2 +- 6 files changed, 298 insertions(+), 570 deletions(-) delete mode 100644 alliance_auth/.gitignore delete mode 100644 alliance_auth/settings.py.example rename {alliance_auth => allianceauth/settings}/__init__.py (100%) create mode 100644 allianceauth/settings/base.py rename {alliance_auth => allianceauth}/wsgi.py (87%) diff --git a/alliance_auth/.gitignore b/alliance_auth/.gitignore deleted file mode 100644 index 7c24c40c..00000000 --- a/alliance_auth/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/settings.py -!/*.example diff --git a/alliance_auth/settings.py.example b/alliance_auth/settings.py.example deleted file mode 100644 index e6e2f5a4..00000000 --- a/alliance_auth/settings.py.example +++ /dev/null @@ -1,566 +0,0 @@ -# -*- coding: UTF-8 -*- -""" -Django settings for alliance_auth project. - -Generated by 'django-admin startproject' using Django 1.10.1. - -For more information on this file, see -https://docs.djangoproject.com/en/1.10/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.10/ref/settings/ -""" - -import os - -from django.contrib import messages -from celery.schedules import crontab - -INSTALLED_APPS = [ - # Core apps - required to function - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.humanize', - 'django_celery_beat', - 'bootstrapform', - 'bootstrap_pagination', - 'sortedm2m', - 'esi', - 'allianceauth', - 'allianceauth.authentication', - 'allianceauth.services', - 'allianceauth.eveonline', - 'allianceauth.groupmanagement', - 'allianceauth.notifications', - 'allianceauth.thirdparty.navhelper', - - - # Optional apps - remove if not desired - 'allianceauth.corputils', - 'allianceauth.hrapplications', - 'allianceauth.timerboard', - 'allianceauth.srp', - 'allianceauth.optimer', - 'allianceauth.fleetup', - 'allianceauth.fleetactivitytracking', - 'allianceauth.permissions_tool', - - # Services - remove if not used - 'allianceauth.services.modules.mumble', - 'allianceauth.services.modules.discord', - 'allianceauth.services.modules.discourse', - 'allianceauth.services.modules.ips4', - 'allianceauth.services.modules.market', - 'allianceauth.services.modules.openfire', - 'allianceauth.services.modules.seat', - 'allianceauth.services.modules.smf', - 'allianceauth.services.modules.phpbb3', - 'allianceauth.services.modules.xenforo', - 'allianceauth.services.modules.teamspeak3', -] - -##################################################### -## -## Django Project Configuration -## -##################################################### -# Don't touch unless you know what you're doing. -# Scroll down for Auth Configuration -##################################################### - -# Celery configuration -BROKER_URL = 'redis://localhost:6379/0' -CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler" -CELERYBEAT_SCHEDULE = { - 'esi_cleanup_callbackredirect': { - 'task': 'esi.tasks.cleanup_callbackredirect', - 'schedule': crontab(hour='*/4'), - }, - 'esi_cleanup_token': { - 'task': 'esi.tasks.cleanup_token', - 'schedule': crontab(day_of_month='*/1'), - }, - 'run_model_update': { - 'task': 'allianceauth.eveonline.tasks.run_model_update', - 'schedule': crontab(minute=0, hour="*/6"), - }, - 'check_all_character_ownership': { - 'task': 'allianceauth.authentication.tasks.check_all_character_ownership', - 'schedule': crontab(hour='*/4'), - } -} - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -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', - 'django.middleware.locale.LocaleMiddleware', -] - -ROOT_URLCONF = 'allianceauth.urls' - -LOCALE_PATHS = ( - os.path.join(BASE_DIR, 'locale/'), -) - -ugettext = lambda s: s -LANGUAGES = ( - ('en', ugettext('English')), - ('de', ugettext('German')), -) - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - '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', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'allianceauth.context_processors.auth_settings', - 'allianceauth.notifications.context_processors.user_notification_count', - 'allianceauth.groupmanagement.context_processors.can_manage_groups', - ], - }, - }, -] - -WSGI_APPLICATION = 'alliance_auth.wsgi.application' - - -# Password validation -# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -AUTHENTICATION_BACKENDS = ['allianceauth.authentication.backends.StateBackend', 'django.contrib.auth.backends.ModelBackend'] -LOGIN_URL = 'auth_login_user' - -# Internationalization -# https://docs.djangoproject.com/en/1.10/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.10/howto/static-files/ - -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, "static") - -# Bootstrap messaging css workaround -MESSAGE_TAGS = { - messages.ERROR: 'danger' -} - -CACHES = { - "default": { - "BACKEND": "redis_cache.RedisCache", - "LOCATION": "localhost:6379", - "OPTIONS": { - "DB": 1, - } - } -} - -##################################################### -## -## Auth Configuration -## -##################################################### - -############################### -# Required Django Settings -############################### -# SECRET_KEY - Random alphanumeric string for cryptographic signing -# http://www.miniwebtool.com/django-secret-key-generator/ -# DEBUG - True to display stack traces when errors are encountered. -# Set this False once auth is installed and working for security. -# ALLOWED_HOSTS - A list of hosts to serve content on. Wildcards accepted. -# Requests for hosts not listed here will be rejected. -# Example: ['example.com', 'auth.example.com'] -# DATABASES - Auth database connection information. -################################ -SECRET_KEY = '' -DEBUG = True -ALLOWED_HOSTS = [] -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'alliance_auth', - 'USER': 'allianceserver', - 'PASSWORD': '', - 'HOST': '127.0.0.1', - 'PORT': '3306', - }, -} - -#################################### -# SITE_NAME - Name of the auth site. -#################################### -SITE_NAME = 'Alliance Auth' - -################# -# EMAIL SETTINGS -################# -# DEFAULT_FROM_EMAIL - no-reply email address -# DOMAIN - The Alliance Auth domain (or subdomain) address, no leading http:// -# EMAIL_HOST - SMTP Server URL -# EMAIL_PORT - SMTP Server PORT -# EMAIL_HOST_USER - Email Username (for gmail, the entire address) -# EMAIL_HOST_PASSWORD - Email Password -# EMAIL_USE_TLS - Set to use TLS encryption -################# -DEFAULT_FROM_EMAIL = 'no-reply@example.com' -DOMAIN = 'example.com' -EMAIL_HOST = 'smtp.gmail.com' -EMAIL_PORT = 587 -EMAIL_HOST_USER = '' -EMAIL_HOST_PASSWORD = '' -EMAIL_USE_TLS = True - -################### -# SSO Settings -################### -# Get client ID and client secret from registering an app at -# https://developers.eveonline.com/ -# Callback URL should be https://example.com/sso/callback -################### -ESI_SSO_CLIENT_ID = '' -ESI_SSO_CLIENT_SECRET = '' -ESI_SSO_CALLBACK_URL = '' - -################# -# Login Settings -################# -# LOGIN_REDIRECT_URL - default destination when logging in if no redirect specified -# LOGOUT_REDIRECT_URL - destination after logging out -# Both of these redirects accept values as per the django redirect shortcut -# https://docs.djangoproject.com/en/1.10/topics/http/shortcuts/#redirect -# - url names eg 'authentication:dashboard' -# - relative urls eg '/dashboard' -# - absolute urls eg 'http://example.com/dashboard' -# LOGIN_TOKEN_SCOPES - scopes required on new tokens when logging in. Cannot be blank. -# ACCOUNT_ACTIVATION_DAYS - number of days email verification tokens are valid for -################## -LOGIN_REDIRECT_URL = 'authentication:dashboard' -LOGOUT_REDIRECT_URL = 'authentication:dashboard' -LOGIN_TOKEN_SCOPES = ['esi-characters.read_opportunities.v1'] -ACCOUNT_ACTIVATION_DAYS = 1 - -##################################################### -## -## Service Configuration -## -##################################################### - -##################### -# Alliance Market -##################### -MARKET_URL = 'http://example.com/market' -MARKET_DB = { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'alliance_market', - 'USER': 'allianceserver-market', - 'PASSWORD': '', - 'HOST': '127.0.0.1', - 'PORT': '3306', -} - -######################## -# XenForo Configuration -######################## -XENFORO_ENDPOINT = 'example.com/api.php' -XENFORO_DEFAULT_GROUP = 0 -XENFORO_APIKEY = '' -##################### - -###################### -# Jabber Configuration -###################### -# JABBER_URL - Jabber address url, no leading http:// -# JABBER_PORT - Jabber service portal -# JABBER_SERVER - Jabber server url (server name, eg 'example.com'), no leading http:// -# OPENFIRE_ADDRESS - Address of the openfire admin console including port -# Please use http:// with 9090 or https:// with 9091 -# eg 'http://example.com:9090' or 'https://example.com:9091' -# OPENFIRE_SECRET_KEY - Openfire REST API secret key -# BROADCAST_USER - Broadcast user username (before @ sign) -# BROADCAST_USER_PASSWORD - Broadcast user password -# BROADCAST_SERVICE_NAME - Name of the Openfire service to broadcast to, usually "broadcast" -###################### -JABBER_URL = '' -JABBER_PORT = 5223 -JABBER_SERVER = '' -OPENFIRE_ADDRESS = '' -OPENFIRE_SECRET_KEY = '' -BROADCAST_USER = "broadcast" + "@" + JABBER_URL -BROADCAST_USER_PASSWORD = '' -BROADCAST_SERVICE_NAME = "broadcast" - -###################################### -# Mumble Configuration -###################################### -# MUMBLE_URL - Mumble server url, tolerates leading http://, mumble://, or none -# eg 'http://example.com', 'mumble://example.com', or 'example.com' -###################################### -MUMBLE_URL = '' - -###################################### -# PHPBB3 Configuration -###################################### -PHPBB3_URL = 'http://example.com/phpbb/' -PHPBB3_DB = { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'alliance_forum', - 'USER': 'allianceserver-phpbb3', - 'PASSWORD': '', - 'HOST': '127.0.0.1', - 'PORT': '3306', -} - -###################################### -# Teamspeak3 Configuration -###################################### -# TEAMSPEAK3_SERVER_IP - Teamspeak3 server ip -# TEAMSPEAK3_SERVER_PORT - Teamspeak3 server port -# TEAMSPEAK3_SERVERQUERY_USER - Teamspeak3 serverquery username -# TEAMSPEAK3_SERVERQUERY_PASSWORD - Teamspeak3 serverquery password -# TEAMSPEAK3_VIRTUAL_SERVER - Virtual server id -# TEAMSPEAK3_AUTHED_GROUP_ID - Default authed group id -# TEAMSPEAK3_PUBLIC_URL - teamspeak3 public url used for link creation -###################################### -TEAMSPEAK3_SERVER_IP = '127.0.0.1' -TEAMSPEAK3_SERVER_PORT = 10011 -TEAMSPEAK3_SERVERQUERY_USER = 'serveradmin' -TEAMSPEAK3_SERVERQUERY_PASSWORD = '' -TEAMSPEAK3_VIRTUAL_SERVER = 1 -TEAMSPEAK3_PUBLIC_URL = '' - -###################################### -# Discord Configuration -###################################### -# DISCORD_GUILD_ID - ID of the guild to manage -# DISCORD_BOT_TOKEN - OAuth token of the app bot user -# DISCORD_INVITE_CODE - Alphanumeric string invite code to the server -# Do not include the leading http://discord.gg/ -# DISCORD_APP_ID - OAuth app client ID -# DISCORD_APP_SECRET - OAuth app secret -# DISCORD_CALLBACK_URL - OAuth callback url -# Should be of the form 'http://example.com/discord/callback' -# DISCORD_SYNC_NAMES - Force discord nicknames to be set to eve char name -###################################### -DISCORD_GUILD_ID = '' -DISCORD_BOT_TOKEN = '' -DISCORD_INVITE_CODE = '' -DISCORD_APP_ID = '' -DISCORD_APP_SECRET = '' -DISCORD_CALLBACK_URL = '' -DISCORD_SYNC_NAMES = False - -###################################### -# Discourse Configuration -###################################### -# DISCOURSE_URL - Web address of the forums (no trailing slash). Include http:// -# DISCOURSE_API_USERNAME - API account username -# DISCOURSE_API_KEY - API Key -# DISCOURSE_SSO_SECRET - SSO secret key -###################################### -DISCOURSE_URL = '' -DISCOURSE_API_USERNAME = '' -DISCOURSE_API_KEY = '' -DISCOURSE_SSO_SECRET = '' - -##################################### -# IPS4 Configuration -##################################### -# IPS4_URL - Base URL of the IPS4 install (no trailing slash). Include http:// -##################################### -IPS4_URL = '' -IPS4_DB = { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'alliance_ips4', - 'USER': 'allianceserver-ips4', - 'PASSWORD': '', - 'HOST': '127.0.0.1', - 'PORT': '3306', -} - -##################################### -# SEAT Configuration -##################################### -# SEAT_URL - Base URL of the seat install (no trailing slash). Include http:// -# SEAT_XTOKEN - API key X-Token provided by SeAT -##################################### -SEAT_URL = '' -SEAT_XTOKEN = '' - -###################################### -# SMF Configuration -###################################### -# SMF_URL - Web address of the forums. Include leading http:// -###################################### -SMF_URL = '' -SMF_DB = { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'alliance_smf', - 'USER': 'allianceserver-smf', - 'PASSWORD': '', - 'HOST': '127.0.0.1', - 'PORT': '3306', -} - -###################################### -# Fleet-Up Configuration -###################################### -# FLEETUP_APP_KEY - The app key from http://fleet-up.com/Api/MyApps -# FLEETUP_USER_ID - The user id from http://fleet-up.com/Api/MyKeys -# FLEETUP_API_ID - The API id from http://fleet-up.com/Api/MyKeys -# FLEETUP_GROUP_ID - The id of the group you want to pull data from, see http://fleet-up.com/Api/Endpoints#groups_mygroupmemberships -###################################### -FLEETUP_APP_KEY = os.environ.get('AA_FLEETUP_APP_KEY', '') -FLEETUP_USER_ID = os.environ.get('AA_FLEETUP_USER_ID', '') -FLEETUP_API_ID = os.environ.get('AA_FLEETUP_API_ID', '') -FLEETUP_GROUP_ID = os.environ.get('AA_FLEETUP_GROUP_ID', '') - -##################################################### -## -## Logging Configuration -## -##################################################### -# Set log_file and console level to desired state: -# DEBUG - basically stack trace, explains every step -# INFO - model creation, deletion, updates, etc -# WARN - unexpected function outcomes that do not impact user -# ERROR - unexcpeted function outcomes which prevent user from achieving desired outcome -# EXCEPTION - something critical went wrong, unhandled -##################################### -# Recommended level for log_file is INFO, console is DEBUG -# Change log level of individual apps below to narrow your debugging -##################################### -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", - 'datefmt' : "%d/%b/%Y %H:%M:%S" - }, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, - }, - 'handlers': { - 'log_file': { - 'level': 'INFO', # edit this line to change logging level to file - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(BASE_DIR,'log/allianceauth.log'), - 'formatter': 'verbose', - 'maxBytes': 1024*1024*5, # edit this line to change max log file size - 'backupCount': 5, # edit this line to change number of log backups - }, - 'console': { - 'level': 'DEBUG', # edit this line to change logging level to console - 'class': 'logging.StreamHandler', - 'formatter': 'verbose', - }, - 'notifications': { # creates notifications for users with logging_notifications permission - 'level': 'ERROR', # edit this line to change logging level to notifications - 'class': 'allianceauth.notifications.handlers.NotificationHandler', - 'formatter': 'verbose', - }, - }, - 'loggers': { - 'allianceauth': { - 'handlers': ['log_file', 'console', 'notifications'], - 'level': 'DEBUG', - }, - 'celerytask': { - 'handlers': ['log_file', 'console', 'notifications'], - 'level': 'DEBUG', - }, - 'portal': { - 'handlers': ['log_file', 'console', 'notifications'], - 'level': 'DEBUG', - }, - 'registration': { - 'handlers': ['log_file', 'console', 'notifications'], - 'level': 'DEBUG', - }, - 'util': { - 'handlers': ['log_file', 'console', 'notifications'], - 'level': 'DEBUG', - }, - 'django': { - 'handlers': ['log_file', 'console'], - 'level': 'ERROR', - }, - } -} - -##################################################### -## -## Magic Block -## -##################################################### -# This block automagically inserts needed settings if -# certain services are installed. -# Don't touch. -######################################### - -# Conditionally add databases only if configured -if 'services.modules.phpbb3' in INSTALLED_APPS: - DATABASES['phpbb3'] = PHPBB3_DB -if 'services.modules.smf' in INSTALLED_APPS: - DATABASES['smf'] = SMF_DB -if 'services.modules.market' in INSTALLED_APPS: - DATABASES['market'] = MARKET_DB -if 'services.modules.ips4' in INSTALLED_APPS: - DATABASES['ips4'] = IPS4_DB - - -# Conditionally add periodic tasks for services if installed -if 'services.modules.teamspeak3' in INSTALLED_APPS: - CELERYBEAT_SCHEDULE['run_ts3_group_update'] = { - 'task': 'services.modules.teamspeak3.tasks.run_ts3_group_update', - 'schedule': crontab(minute='*/30'), - } diff --git a/alliance_auth/__init__.py b/allianceauth/settings/__init__.py similarity index 100% rename from alliance_auth/__init__.py rename to allianceauth/settings/__init__.py diff --git a/allianceauth/settings/base.py b/allianceauth/settings/base.py new file mode 100644 index 00000000..3b557046 --- /dev/null +++ b/allianceauth/settings/base.py @@ -0,0 +1,296 @@ +# -*- coding: UTF-8 -*- +""" +Django settings for alliance_auth project. + +Generated by 'django-admin startproject' using Django 1.10.1. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" + +import os + +from django.contrib import messages +from celery.schedules import crontab + +INSTALLED_APPS = [ + # Core apps - required to function + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.humanize', + 'django_celery_beat', + 'bootstrapform', + 'bootstrap_pagination', + 'sortedm2m', + 'esi', + 'allianceauth', + 'allianceauth.authentication', + 'allianceauth.services', + 'allianceauth.eveonline', + 'allianceauth.groupmanagement', + 'allianceauth.notifications', + 'allianceauth.thirdparty.navhelper', + + # Optional apps - remove if not desired +# 'allianceauth.corputils', +# 'allianceauth.hrapplications', +# 'allianceauth.timerboard', +# 'allianceauth.srp', +# 'allianceauth.optimer', +# 'allianceauth.fleetup', +# 'allianceauth.fleetactivitytracking', +# 'allianceauth.permissions_tool', + + # Services - remove if not used +# 'allianceauth.services.modules.mumble', +# 'allianceauth.services.modules.discord', +# 'allianceauth.services.modules.discourse', +# 'allianceauth.services.modules.ips4', +# 'allianceauth.services.modules.market', +# 'allianceauth.services.modules.openfire', +# 'allianceauth.services.modules.seat', +# 'allianceauth.services.modules.smf', +# 'allianceauth.services.modules.phpbb3', +# 'allianceauth.services.modules.xenforo', +# 'allianceauth.services.modules.teamspeak3', +] + +# Celery configuration +BROKER_URL = 'redis://localhost:6379/0' +CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler" +CELERYBEAT_SCHEDULE = { + 'esi_cleanup_callbackredirect': { + 'task': 'esi.tasks.cleanup_callbackredirect', + 'schedule': crontab(hour='*/4'), + }, + 'esi_cleanup_token': { + 'task': 'esi.tasks.cleanup_token', + 'schedule': crontab(day_of_month='*/1'), + }, + 'run_model_update': { + 'task': 'allianceauth.eveonline.tasks.run_model_update', + 'schedule': crontab(minute=0, hour="*/6"), + }, + 'check_all_character_ownership': { + 'task': 'allianceauth.authentication.tasks.check_all_character_ownership', + 'schedule': crontab(hour='*/4'), + } +} + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +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', + 'django.middleware.locale.LocaleMiddleware', +] + +ROOT_URLCONF = 'allianceauth.urls' + +LOCALE_PATHS = ( + os.path.join(BASE_DIR, 'locale/'), +) + +ugettext = lambda s: s +LANGUAGES = ( + ('en', ugettext('English')), + ('de', ugettext('German')), +) + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + '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', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'allianceauth.notifications.context_processors.user_notification_count', + 'allianceauth.groupmanagement.context_processors.can_manage_groups', + ], + }, + }, +] + +WSGI_APPLICATION = 'allianceauth.wsgi.application' + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +AUTHENTICATION_BACKENDS = ['allianceauth.authentication.backends.StateBackend', + 'django.contrib.auth.backends.ModelBackend'] + +# Internationalization +# https://docs.djangoproject.com/en/1.10/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, "static") + +# Bootstrap messaging css workaround +MESSAGE_TAGS = { + messages.ERROR: 'danger' +} + +CACHES = { + "default": { + "BACKEND": "redis_cache.RedisCache", + "LOCATION": "localhost:6379", + "OPTIONS": { + "DB": 1, + } + } +} + +SECRET_KEY = 'this is a very bad secret key you should change' +DEBUG = True +ALLOWED_HOSTS = ['*'] +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': str(os.path.join(BASE_DIR, 'alliance_auth.sqlite')), + }, +} + +SITE_NAME = 'Alliance Auth' + +################# +# Login Settings +################# +# LOGIN_REDIRECT_URL - default destination when logging in if no redirect specified +# LOGOUT_REDIRECT_URL - destination after logging out +# Both of these redirects accept values as per the django redirect shortcut +# https://docs.djangoproject.com/en/1.11/topics/http/shortcuts/#redirect +# - url names eg 'authentication:dashboard' +# - relative urls eg '/dashboard' +# - absolute urls eg 'http://example.com/dashboard' +# LOGIN_TOKEN_SCOPES - scopes required on new tokens when logging in. Cannot be blank. +# ACCOUNT_ACTIVATION_DAYS - number of days email verification tokens are valid for +################## +LOGIN_URL = 'auth_login_user' +LOGIN_REDIRECT_URL = 'authentication:dashboard' +LOGOUT_REDIRECT_URL = 'authentication:dashboard' +LOGIN_TOKEN_SCOPES = ['esi-characters.read_opportunities.v1'] +ACCOUNT_ACTIVATION_DAYS = 1 + +##################################################### +## +## Logging Configuration +## +##################################################### +# Set log_file and console level to desired state: +# DEBUG - basically stack trace, explains every step +# INFO - model creation, deletion, updates, etc +# WARN - unexpected function outcomes that do not impact user +# ERROR - unexcpeted function outcomes which prevent user from achieving desired outcome +# EXCEPTION - something critical went wrong, unhandled +##################################### +# Recommended level for log_file is INFO, console is DEBUG +# Change log level of individual apps below to narrow your debugging +##################################### +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", + 'datefmt': "%d/%b/%Y %H:%M:%S" + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'handlers': { + 'log_file': { + 'level': 'INFO', # edit this line to change logging level to file + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': os.path.join(BASE_DIR, 'log/allianceauth.log'), + 'formatter': 'verbose', + 'maxBytes': 1024 * 1024 * 5, # edit this line to change max log file size + 'backupCount': 5, # edit this line to change number of log backups + }, + 'console': { + 'level': 'DEBUG', # edit this line to change logging level to console + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + 'notifications': { # creates notifications for users with logging_notifications permission + 'level': 'ERROR', # edit this line to change logging level to notifications + 'class': 'allianceauth.notifications.handlers.NotificationHandler', + 'formatter': 'verbose', + }, + }, + 'loggers': { + 'allianceauth': { + 'handlers': ['log_file', 'console', 'notifications'], + 'level': 'DEBUG', + }, + 'celerytask': { + 'handlers': ['log_file', 'console', 'notifications'], + 'level': 'DEBUG', + }, + 'portal': { + 'handlers': ['log_file', 'console', 'notifications'], + 'level': 'DEBUG', + }, + 'registration': { + 'handlers': ['log_file', 'console', 'notifications'], + 'level': 'DEBUG', + }, + 'util': { + 'handlers': ['log_file', 'console', 'notifications'], + 'level': 'DEBUG', + }, + 'django': { + 'handlers': ['log_file', 'console'], + 'level': 'ERROR', + }, + } +} diff --git a/alliance_auth/wsgi.py b/allianceauth/wsgi.py similarity index 87% rename from alliance_auth/wsgi.py rename to allianceauth/wsgi.py index a5087167..39a09438 100644 --- a/alliance_auth/wsgi.py +++ b/allianceauth/wsgi.py @@ -10,7 +10,7 @@ https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "alliance_auth.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "allianceauth.settings.base") # virtualenv wrapper, uncomment below to activate # activate_env=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'env/bin/activate_this.py') diff --git a/manage.py b/manage.py index e6768c3c..7eec003b 100644 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "alliance_auth.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "allianceauth.settings.base") try: from django.core.management import execute_from_command_line except ImportError: From cd6963daa6e55f692a1a60cd5fcb3a31944aa6ff Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 28 Sep 2017 20:08:05 -0400 Subject: [PATCH 02/16] Restructure tests folder. Remove DOMAIN setting. Use localhost for services revoked emails. --- .gitattributes | 1 - .gitignore | 8 - allianceauth/context_processors.py | 1 - allianceauth/log/.gitignore | 2 + .../services/modules/phpbb3/manager.py | 2 +- allianceauth/services/modules/smf/manager.py | 2 +- allianceauth/settings/base.py | 51 +-- allianceauth/srp/managers.py | 4 +- runtests.py | 2 +- test_allianceauth/settings.py | 409 ------------------ {test_allianceauth => tests}/__init__.py | 0 tests/settings.py | 195 +++++++++ {test_allianceauth => tests}/urls.py | 1 + {test_allianceauth => tests}/views.py | 0 14 files changed, 215 insertions(+), 463 deletions(-) delete mode 100644 .gitattributes create mode 100644 allianceauth/log/.gitignore delete mode 100644 test_allianceauth/settings.py rename {test_allianceauth => tests}/__init__.py (100%) create mode 100644 tests/settings.py rename {test_allianceauth => tests}/urls.py (99%) rename {test_allianceauth => tests}/views.py (100%) diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 9369413a..00000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*/*.py.example linguist-language=Python diff --git a/.gitignore b/.gitignore index c1610880..f183222b 100644 --- a/.gitignore +++ b/.gitignore @@ -53,20 +53,12 @@ docs/_build/ # PyBuilder target/ -.vagrant/ -alliance_auth/settings.py *Thumbs.db nginx_config.txt -# custom staticfiles -static/* - #celerybeat *.pid celerybeat-schedule #pycharm .idea/* - -#log folder -log/* diff --git a/allianceauth/context_processors.py b/allianceauth/context_processors.py index acbdb6f8..ae596ac3 100644 --- a/allianceauth/context_processors.py +++ b/allianceauth/context_processors.py @@ -3,6 +3,5 @@ from django.conf import settings def auth_settings(request): return { - 'DOMAIN': settings.DOMAIN, 'SITE_NAME': settings.SITE_NAME, } diff --git a/allianceauth/log/.gitignore b/allianceauth/log/.gitignore new file mode 100644 index 00000000..73edf032 --- /dev/null +++ b/allianceauth/log/.gitignore @@ -0,0 +1,2 @@ +!.gitignore +* \ No newline at end of file diff --git a/allianceauth/services/modules/phpbb3/manager.py b/allianceauth/services/modules/phpbb3/manager.py index 621de22e..01b2dec8 100755 --- a/allianceauth/services/modules/phpbb3/manager.py +++ b/allianceauth/services/modules/phpbb3/manager.py @@ -195,7 +195,7 @@ class Phpbb3Manager: cursor = connections['phpbb3'].cursor() password = Phpbb3Manager.__gen_hash(Phpbb3Manager.__generate_random_pass()) - revoke_email = "revoked@" + settings.DOMAIN + revoke_email = "revoked@localhost" try: pwhash = Phpbb3Manager.__gen_hash(password) cursor.execute(Phpbb3Manager.SQL_DIS_USER, [revoke_email, pwhash, username]) diff --git a/allianceauth/services/modules/smf/manager.py b/allianceauth/services/modules/smf/manager.py index f0816209..ff9f346a 100644 --- a/allianceauth/services/modules/smf/manager.py +++ b/allianceauth/services/modules/smf/manager.py @@ -235,7 +235,7 @@ class SmfManager: cursor = connections['smf'].cursor() password = cls.generate_random_pass() - revoke_email = "revoked@" + settings.DOMAIN + revoke_email = "revoked@localhost" try: pwhash = cls.gen_hash(username, password) cursor.execute(cls.SQL_DIS_USER, [revoke_email, pwhash, username]) diff --git a/allianceauth/settings/base.py b/allianceauth/settings/base.py index 3b557046..ae7729b5 100644 --- a/allianceauth/settings/base.py +++ b/allianceauth/settings/base.py @@ -37,29 +37,6 @@ INSTALLED_APPS = [ 'allianceauth.groupmanagement', 'allianceauth.notifications', 'allianceauth.thirdparty.navhelper', - - # Optional apps - remove if not desired -# 'allianceauth.corputils', -# 'allianceauth.hrapplications', -# 'allianceauth.timerboard', -# 'allianceauth.srp', -# 'allianceauth.optimer', -# 'allianceauth.fleetup', -# 'allianceauth.fleetactivitytracking', -# 'allianceauth.permissions_tool', - - # Services - remove if not used -# 'allianceauth.services.modules.mumble', -# 'allianceauth.services.modules.discord', -# 'allianceauth.services.modules.discourse', -# 'allianceauth.services.modules.ips4', -# 'allianceauth.services.modules.market', -# 'allianceauth.services.modules.openfire', -# 'allianceauth.services.modules.seat', -# 'allianceauth.services.modules.smf', -# 'allianceauth.services.modules.phpbb3', -# 'allianceauth.services.modules.xenforo', -# 'allianceauth.services.modules.teamspeak3', ] # Celery configuration @@ -127,6 +104,7 @@ TEMPLATES = [ 'django.template.context_processors.tz', 'allianceauth.notifications.context_processors.user_notification_count', 'allianceauth.groupmanagement.context_processors.can_manage_groups', + 'allianceauth.context_processors.auth_settings', ], }, }, @@ -272,25 +250,20 @@ LOGGING = { 'handlers': ['log_file', 'console', 'notifications'], 'level': 'DEBUG', }, - 'celerytask': { - 'handlers': ['log_file', 'console', 'notifications'], - 'level': 'DEBUG', - }, - 'portal': { - 'handlers': ['log_file', 'console', 'notifications'], - 'level': 'DEBUG', - }, - 'registration': { - 'handlers': ['log_file', 'console', 'notifications'], - 'level': 'DEBUG', - }, - 'util': { - 'handlers': ['log_file', 'console', 'notifications'], - 'level': 'DEBUG', - }, 'django': { 'handlers': ['log_file', 'console'], 'level': 'ERROR', }, } } + + +def add_auth_apps(APPS): + """ + Merges required auth apps with a list of custom user apps for project settings. + Leaves order of passed INSTALLED_APPS unchanged (passed apps come first) to allow overriding templates/static/etc + https://docs.djangoproject.com/en/2.0/ref/settings/#installed-apps + :param APPS: INSTALLED_APPS list + :return: Merged INSTALLED_APPS + """ + APPS += [app for app in INSTALLED_APPS if app not in APPS] diff --git a/allianceauth/srp/managers.py b/allianceauth/srp/managers.py index 2c394c91..d59e5366 100644 --- a/allianceauth/srp/managers.py +++ b/allianceauth/srp/managers.py @@ -1,4 +1,4 @@ -from django.conf import settings +from allianceauth import NAME import requests import logging @@ -20,7 +20,7 @@ class SRPManager: def get_kill_data(kill_id): url = ("https://www.zkillboard.com/api/killID/%s/" % kill_id) headers = { - 'User-Agent': "%s Alliance Auth" % settings.DOMAIN, + 'User-Agent': NAME, 'Content-Type': 'application/json', } r = requests.get(url, headers=headers) diff --git a/runtests.py b/runtests.py index 45c5201c..cdf361a0 100644 --- a/runtests.py +++ b/runtests.py @@ -3,7 +3,7 @@ import os import sys if __name__ == "__main__": - os.environ['DJANGO_SETTINGS_MODULE'] = 'test_allianceauth.settings' + os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' try: from django.core.management import execute_from_command_line diff --git a/test_allianceauth/settings.py b/test_allianceauth/settings.py deleted file mode 100644 index 01b29444..00000000 --- a/test_allianceauth/settings.py +++ /dev/null @@ -1,409 +0,0 @@ -""" -Alliance Auth Test Suite Django settings. -""" - -import os - -from django.contrib import messages - -import alliance_auth - -# Use nose to run all tests -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' - -NOSE_ARGS = [ - #'--with-coverage', - #'--cover-package=', - #'--exe', # If your tests need this to be found/run, check they py files are not chmodded +x -] - -# Celery configuration -CELERY_ALWAYS_EAGER = True # Forces celery to run locally for testing - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(alliance_auth.__file__))) - -SECRET_KEY = 'testing only' - -DEBUG = False - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.humanize', - 'django_celery_beat', - 'bootstrapform', - 'esi', - 'bootstrap_pagination', - 'allianceauth', - 'allianceauth.authentication', - 'allianceauth.services', - 'allianceauth.eveonline', - 'allianceauth.groupmanagement', - 'allianceauth.hrapplications', - 'allianceauth.timerboard', - 'allianceauth.srp', - 'allianceauth.optimer', - 'allianceauth.corputils', - 'allianceauth.fleetactivitytracking', - 'allianceauth.fleetup', - 'allianceauth.notifications', - 'allianceauth.permissions_tool', - 'allianceauth.thirdparty.navhelper', - - 'allianceauth.services.modules.mumble', - 'allianceauth.services.modules.discord', - 'allianceauth.services.modules.discourse', - 'allianceauth.services.modules.ips4', - 'allianceauth.services.modules.market', - 'allianceauth.services.modules.openfire', - 'allianceauth.services.modules.seat', - 'allianceauth.services.modules.smf', - 'allianceauth.services.modules.phpbb3', - 'allianceauth.services.modules.xenforo', - 'allianceauth.services.modules.teamspeak3', - 'django_nose', -] - -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', - 'django.middleware.locale.LocaleMiddleware', -] - -ROOT_URLCONF = 'test_allianceauth.urls' - -LOCALE_PATHS = ( - os.path.join(BASE_DIR, 'locale/'), -) - -ugettext = lambda s: s -LANGUAGES = ( - ('en', ugettext('English')), - ('de', ugettext('German')), -) -LOGIN_TOKEN_SCOPES = ['esi-characters.read_opportunities.v1'] -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - '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', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'allianceauth.context_processors.auth_settings', - 'allianceauth.notifications.context_processors.user_notification_count', - 'allianceauth.groupmanagement.context_processors.can_manage_groups', - ], - }, - }, -] - -# Database -# https://docs.djangoproject.com/en/1.10/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'alliance_auth', - 'USER': os.environ.get('AA_DB_DEFAULT_USER', None), - 'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', None), - 'HOST': os.environ.get('AA_DB_DEFAULT_HOST', None) - }, -} - -LOGIN_URL = 'auth_login_user' - -SUPERUSER_STATE_BYPASS = 'True' == os.environ.get('AA_SUPERUSER_STATE_BYPASS', 'True') - -# Internationalization -# https://docs.djangoproject.com/en/1.10/topics/i18n/ - -LANGUAGE_CODE = os.environ.get('AA_LANGUAGE_CODE', 'en') - -TIME_ZONE = os.environ.get('AA_TIME_ZONE', 'UTC') - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) - -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, "static") - -# Bootstrap messaging css workaround -MESSAGE_TAGS = { - messages.ERROR: 'danger' -} - -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', - } -} - -AUTHENTICATION_BACKENDS = ['allianceauth.authentication.backends.StateBackend', 'django.contrib.auth.backends.ModelBackend'] - -##################################################### -## -## Auth configuration starts here -## -##################################################### - -################# -# EMAIL SETTINGS -################# -# DOMAIN - The alliance auth domain_url -# EMAIL_HOST - SMTP Server URL -# EMAIL_PORT - SMTP Server PORT -# EMAIL_HOST_USER - Email Username (for gmail, the entire address) -# EMAIL_HOST_PASSWORD - Email Password -# EMAIL_USE_TLS - Set to use TLS encryption -################# -DOMAIN = os.environ.get('AA_DOMAIN', 'https://example.com') -SITE_NAME = os.environ.get('AA_SITE_NAME', 'Test Alliance Auth') -EMAIL_HOST = os.environ.get('AA_EMAIL_HOST', 'smtp.example.com') -EMAIL_PORT = int(os.environ.get('AA_EMAIL_PORT', '587')) -EMAIL_HOST_USER = os.environ.get('AA_EMAIL_HOST_USER', '') -EMAIL_HOST_PASSWORD = os.environ.get('AA_EMAIL_HOST_PASSWORD', '') -EMAIL_USE_TLS = 'True' == os.environ.get('AA_EMAIL_USE_TLS', 'True') - -################### -# SSO Settings -################### -# Optional SSO. -# Get client ID and client secret from registering an app at -# https://developers.eveonline.com/ -# Callback URL should be http://mydomain.com/sso/callback -# Leave callback blank to hide SSO button on login page -################### -ESI_SSO_CLIENT_ID = os.environ.get('AA_ESI_SSO_CLIENT_ID', '') -ESI_SSO_CLIENT_SECRET = os.environ.get('AA_ESI_SSO_CLIENT_SECRET', '') -ESI_SSO_CALLBACK_URL = os.environ.get('AA_ESI_SSO_CALLBACK_URL', '') - -####################### -# EVE Provider Settings -####################### -# EVEONLINE_CHARACTER_PROVIDER - Name of default data source for getting eve character data -# EVEONLINE_CORP_PROVIDER - Name of default data source for getting eve corporation data -# EVEONLINE_ALLIANCE_PROVIDER - Name of default data source for getting eve alliance data -# EVEONLINE_ITEMTYPE_PROVIDER - Name of default data source for getting eve item type data -# -# Available sources are 'esi' and 'xml'. Leaving blank results in the default 'esi' being used. -####################### -EVEONLINE_CHARACTER_PROVIDER = os.environ.get('AA_EVEONLINE_CHARACTER_PROVIDER', 'xml') -EVEONLINE_CORP_PROVIDER = os.environ.get('AA_EVEONLINE_CORP_PROVIDER', 'xml') -EVEONLINE_ALLIANCE_PROVIDER = os.environ.get('AA_EVEONLINE_ALLIANCE_PROVIDER', 'xml') -EVEONLINE_ITEMTYPE_PROVIDER = os.environ.get('AA_EVEONLINE_ITEMTYPE_PROVIDER', 'xml') - -##################### -# Alliance Market -##################### -MARKET_URL = os.environ.get('AA_MARKET_URL', 'http://yourdomain.com/market') - -##################### -# HR Configuration -##################### -# JACK_KNIFE_URL - Url for the audit page of API Jack knife -# Should seriously replace with your own. -##################### -JACK_KNIFE_URL = os.environ.get('AA_JACK_KNIFE_URL', 'http://example.com/eveapi/audit.php') - -######################## -# XenForo Configuration -######################## -XENFORO_ENDPOINT = os.environ.get('AA_XENFORO_ENDPOINT', 'example.com/api.php') -XENFORO_DEFAULT_GROUP = os.environ.get('AA_XENFORO_DEFAULT_GROUP', 0) -XENFORO_APIKEY = os.environ.get('AA_XENFORO_APIKEY', 'yourapikey') -##################### - -###################### -# Jabber Configuration -###################### -# JABBER_URL - Jabber address url -# JABBER_PORT - Jabber service portal -# JABBER_SERVER - Jabber server url -# OPENFIRE_ADDRESS - Address of the openfire admin console including port -# Please use http with 9090 or https with 9091 -# OPENFIRE_SECRET_KEY - Openfire REST API secret key -# BROADCAST_USER - Broadcast user JID -# BROADCAST_USER_PASSWORD - Broadcast user password -###################### -JABBER_URL = os.environ.get('AA_JABBER_URL', "example.com") -JABBER_PORT = int(os.environ.get('AA_JABBER_PORT', '5223')) -JABBER_SERVER = os.environ.get('AA_JABBER_SERVER', "example.com") -OPENFIRE_ADDRESS = os.environ.get('AA_OPENFIRE_ADDRESS', "http://example.com:9090") -OPENFIRE_SECRET_KEY = os.environ.get('AA_OPENFIRE_SECRET_KEY', "somekey") -BROADCAST_USER = os.environ.get('AA_BROADCAST_USER', "broadcast@") + JABBER_URL -BROADCAST_USER_PASSWORD = os.environ.get('AA_BROADCAST_USER_PASSWORD', "somepassword") -BROADCAST_SERVICE_NAME = os.environ.get('AA_BROADCAST_SERVICE_NAME', "broadcast") - -###################################### -# Mumble Configuration -###################################### -# MUMBLE_URL - Mumble server url -# MUMBLE_SERVER_ID - Mumble server id -###################################### -MUMBLE_URL = os.environ.get('AA_MUMBLE_URL', "example.com") -MUMBLE_SERVER_ID = int(os.environ.get('AA_MUMBLE_SERVER_ID', '1')) - -###################################### -# PHPBB3 Configuration -###################################### -PHPBB3_URL = os.environ.get('AA_FORUM_URL', '') - -###################################### -# Teamspeak3 Configuration -###################################### -# TEAMSPEAK3_SERVER_IP - Teamspeak3 server ip -# TEAMSPEAK3_SERVER_PORT - Teamspeak3 server port -# TEAMSPEAK3_SERVERQUERY_USER - Teamspeak3 serverquery username -# TEAMSPEAK3_SERVERQUERY_PASSWORD - Teamspeak3 serverquery password -# TEAMSPEAK3_VIRTUAL_SERVER - Virtual server id -# TEAMSPEAK3_AUTHED_GROUP_ID - Default authed group id -# TEAMSPEAK3_PUBLIC_URL - teamspeak3 public url used for link creation -###################################### -TEAMSPEAK3_SERVER_IP = os.environ.get('AA_TEAMSPEAK3_SERVER_IP', '127.0.0.1') -TEAMSPEAK3_SERVER_PORT = int(os.environ.get('AA_TEAMSPEAK3_SERVER_PORT', '10011')) -TEAMSPEAK3_SERVERQUERY_USER = os.environ.get('AA_TEAMSPEAK3_SERVERQUERY_USER', 'serveradmin') -TEAMSPEAK3_SERVERQUERY_PASSWORD = os.environ.get('AA_TEAMSPEAK3_SERVERQUERY_PASSWORD', 'passwordhere') -TEAMSPEAK3_VIRTUAL_SERVER = int(os.environ.get('AA_TEAMSPEAK3_VIRTUAL_SERVER', '1')) -TEAMSPEAK3_PUBLIC_URL = os.environ.get('AA_TEAMSPEAK3_PUBLIC_URL', 'example.com') - -###################################### -# Discord Configuration -###################################### -# DISCORD_GUILD_ID - ID of the guild to manage -# DISCORD_BOT_TOKEN - oauth token of the app bot user -# DISCORD_INVITE_CODE - invite code to the server -# DISCORD_APP_ID - oauth app client ID -# DISCORD_APP_SECRET - oauth app secret -# DISCORD_CALLBACK_URL - oauth callback url -# DISCORD_SYNC_NAMES - enable to force discord nicknames to be set to eve char name (bot needs Manage Nicknames permission) -###################################### -DISCORD_GUILD_ID = os.environ.get('AA_DISCORD_GUILD_ID', '0118999') -DISCORD_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', 'bottoken') -DISCORD_INVITE_CODE = os.environ.get('AA_DISCORD_INVITE_CODE', 'invitecode') -DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', 'appid') -DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', 'secret') -DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord/callback') -DISCORD_SYNC_NAMES = 'True' == os.environ.get('AA_DISCORD_SYNC_NAMES', 'False') - -###################################### -# Discourse Configuration -###################################### -# DISCOURSE_URL - Web address of the forums (no trailing slash) -# DISCOURSE_API_USERNAME - API account username -# DISCOURSE_API_KEY - API Key -# DISCOURSE_SSO_SECRET - SSO secret key -###################################### -DISCOURSE_URL = os.environ.get('AA_DISCOURSE_URL', 'https://example.com') -DISCOURSE_API_USERNAME = os.environ.get('AA_DISCOURSE_API_USERNAME', '') -DISCOURSE_API_KEY = os.environ.get('AA_DISCOURSE_API_KEY', '') -DISCOURSE_SSO_SECRET = 'd836444a9e4084d5b224a60c208dce14' -# Example secret from https://meta.discourse.org/t/official-single-sign-on-for-discourse/13045 - -##################################### -# IPS4 Configuration -##################################### -# IPS4_URL - base url of the IPS4 install (no trailing slash) -# IPS4_API_KEY - API key provided by IPS4 -##################################### -IPS4_URL = os.environ.get('AA_IPS4_URL', 'http://example.com/ips4') -IPS4_API_KEY = os.environ.get('AA_IPS4_API_KEY', '') - -##################################### -# SEAT Configuration -##################################### -# SEAT_URL - base url of the seat install (no trailing slash) -# SEAT_XTOKEN - API key X-Token provided by SeAT -##################################### -SEAT_URL = os.environ.get('AA_SEAT_URL', 'http://example.com/seat') -SEAT_XTOKEN = os.environ.get('AA_SEAT_XTOKEN', 'tokentokentoken') - -###################################### -# SMF Configuration -###################################### -SMF_URL = os.environ.get('AA_SMF_URL', '') - -###################################### -# Fleet-Up Configuration -###################################### -# FLEETUP_APP_KEY - The app key from http://fleet-up.com/Api/MyApps -# FLEETUP_USER_ID - The user id from http://fleet-up.com/Api/MyKeys -# FLEETUP_API_ID - The API id from http://fleet-up.com/Api/MyKeys -# FLEETUP_GROUP_ID - The id of the group you want to pull data from, see http://fleet-up.com/Api/Endpoints#groups_mygroupmemberships -###################################### -FLEETUP_APP_KEY = os.environ.get('AA_FLEETUP_APP_KEY', '') -FLEETUP_USER_ID = os.environ.get('AA_FLEETUP_USER_ID', '') -FLEETUP_API_ID = os.environ.get('AA_FLEETUP_API_ID', '') -FLEETUP_GROUP_ID = os.environ.get('AA_FLEETUP_GROUP_ID', '') - -PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.MD5PasswordHasher', -] - - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", - 'datefmt' : "%d/%b/%Y %H:%M:%S" - }, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, - }, - 'handlers': { - 'console': { - 'level': 'DEBUG', # edit this line to change logging level to console - 'class': 'logging.StreamHandler', - 'formatter': 'verbose', - }, - 'notifications': { # creates notifications for users with logging_notifications permission - 'level': 'ERROR', # edit this line to change logging level to notifications - 'class': 'allianceauth.notifications.handlers.NotificationHandler', - 'formatter': 'verbose', - }, - }, - 'loggers': { - 'allianceauth': { - 'handlers': ['console', 'notifications'], - 'level': 'DEBUG', - }, - 'celerytask': { - 'handlers': ['console', 'notifications'], - 'level': 'DEBUG', - }, - 'django': { - 'handlers': ['console', 'notifications'], - 'level': 'ERROR', - }, - } -} - -LOGGING = None # Comment out to enable logging for debugging diff --git a/test_allianceauth/__init__.py b/tests/__init__.py similarity index 100% rename from test_allianceauth/__init__.py rename to tests/__init__.py diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 00000000..01d68dcd --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,195 @@ +""" +Alliance Auth Test Suite Django settings. +""" + +from allianceauth.settings.base import * + +# Use nose to run all tests +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +NOSE_ARGS = [ + #'--with-coverage', + #'--cover-package=', + #'--exe', # If your tests need this to be found/run, check they py files are not chmodded +x +] + +# Celery configuration +CELERY_ALWAYS_EAGER = True # Forces celery to run locally for testing + +INSTALLED_APPS = [ + 'allianceauth.hrapplications', + 'allianceauth.timerboard', + 'allianceauth.srp', + 'allianceauth.optimer', + 'allianceauth.corputils', + 'allianceauth.fleetactivitytracking', + 'allianceauth.fleetup', + 'allianceauth.permissions_tool', + 'allianceauth.services.modules.mumble', + 'allianceauth.services.modules.discord', + 'allianceauth.services.modules.discourse', + 'allianceauth.services.modules.ips4', + 'allianceauth.services.modules.market', + 'allianceauth.services.modules.openfire', + 'allianceauth.services.modules.seat', + 'allianceauth.services.modules.smf', + 'allianceauth.services.modules.phpbb3', + 'allianceauth.services.modules.xenforo', + 'allianceauth.services.modules.teamspeak3', + 'django_nose', +] + +add_auth_apps(INSTALLED_APPS) + +ROOT_URLCONF = 'tests.urls' + +CACHES['default'] = {'BACKEND': 'django.core.cache.backends.db.DatabaseCache'} + +##################### +# Alliance Market +##################### +MARKET_URL = 'http://yourdomain.com/market' + +##################### +# HR Configuration +##################### +# JACK_KNIFE_URL - Url for the audit page of API Jack knife +# Should seriously replace with your own. +##################### +JACK_KNIFE_URL = 'http://example.com/eveapi/audit.php' + +######################## +# XenForo Configuration +######################## +XENFORO_ENDPOINT = 'example.com/api.php' +XENFORO_DEFAULT_GROUP = 0 +XENFORO_APIKEY = 'yourapikey' +##################### + +###################### +# Jabber Configuration +###################### +# JABBER_URL - Jabber address url +# JABBER_PORT - Jabber service portal +# JABBER_SERVER - Jabber server url +# OPENFIRE_ADDRESS - Address of the openfire admin console including port +# Please use http with 9090 or https with 9091 +# OPENFIRE_SECRET_KEY - Openfire REST API secret key +# BROADCAST_USER - Broadcast user JID +# BROADCAST_USER_PASSWORD - Broadcast user password +###################### +JABBER_URL = "example.com" +JABBER_PORT = 5223 +JABBER_SERVER = "example.com" +OPENFIRE_ADDRESS = "http://example.com:9090" +OPENFIRE_SECRET_KEY = "somekey" +BROADCAST_USER = "broadcast@" + JABBER_URL +BROADCAST_USER_PASSWORD = "somepassword" +BROADCAST_SERVICE_NAME = "broadcast" + +###################################### +# Mumble Configuration +###################################### +# MUMBLE_URL - Mumble server url +# MUMBLE_SERVER_ID - Mumble server id +###################################### +MUMBLE_URL = "example.com" +MUMBLE_SERVER_ID = 1 + +###################################### +# PHPBB3 Configuration +###################################### +PHPBB3_URL = '' + +###################################### +# Teamspeak3 Configuration +###################################### +# TEAMSPEAK3_SERVER_IP - Teamspeak3 server ip +# TEAMSPEAK3_SERVER_PORT - Teamspeak3 server port +# TEAMSPEAK3_SERVERQUERY_USER - Teamspeak3 serverquery username +# TEAMSPEAK3_SERVERQUERY_PASSWORD - Teamspeak3 serverquery password +# TEAMSPEAK3_VIRTUAL_SERVER - Virtual server id +# TEAMSPEAK3_AUTHED_GROUP_ID - Default authed group id +# TEAMSPEAK3_PUBLIC_URL - teamspeak3 public url used for link creation +###################################### +TEAMSPEAK3_SERVER_IP = '127.0.0.1' +TEAMSPEAK3_SERVER_PORT = 10011 +TEAMSPEAK3_SERVERQUERY_USER = 'serveradmin' +TEAMSPEAK3_SERVERQUERY_PASSWORD = 'passwordhere' +TEAMSPEAK3_VIRTUAL_SERVER = 1 +TEAMSPEAK3_PUBLIC_URL = 'example.com' + +###################################### +# Discord Configuration +###################################### +# DISCORD_GUILD_ID - ID of the guild to manage +# DISCORD_BOT_TOKEN - oauth token of the app bot user +# DISCORD_INVITE_CODE - invite code to the server +# DISCORD_APP_ID - oauth app client ID +# DISCORD_APP_SECRET - oauth app secret +# DISCORD_CALLBACK_URL - oauth callback url +# DISCORD_SYNC_NAMES - enable to force discord nicknames to be set to eve char name (bot needs Manage Nicknames permission) +###################################### +DISCORD_GUILD_ID = '0118999' +DISCORD_BOT_TOKEN = 'bottoken' +DISCORD_INVITE_CODE = 'invitecode' +DISCORD_APP_ID = 'appid' +DISCORD_APP_SECRET = 'secret' +DISCORD_CALLBACK_URL = 'http://example.com/discord/callback' +DISCORD_SYNC_NAMES = 'True' == 'False' + +###################################### +# Discourse Configuration +###################################### +# DISCOURSE_URL - Web address of the forums (no trailing slash) +# DISCOURSE_API_USERNAME - API account username +# DISCOURSE_API_KEY - API Key +# DISCOURSE_SSO_SECRET - SSO secret key +###################################### +DISCOURSE_URL = 'https://example.com' +DISCOURSE_API_USERNAME = '' +DISCOURSE_API_KEY = '' +DISCOURSE_SSO_SECRET = 'd836444a9e4084d5b224a60c208dce14' +# Example secret from https://meta.discourse.org/t/official-single-sign-on-for-discourse/13045 + +##################################### +# IPS4 Configuration +##################################### +# IPS4_URL - base url of the IPS4 install (no trailing slash) +# IPS4_API_KEY - API key provided by IPS4 +##################################### +IPS4_URL = 'http://example.com/ips4' +IPS4_API_KEY = '' + +##################################### +# SEAT Configuration +##################################### +# SEAT_URL - base url of the seat install (no trailing slash) +# SEAT_XTOKEN - API key X-Token provided by SeAT +##################################### +SEAT_URL = 'http://example.com/seat' +SEAT_XTOKEN = 'tokentokentoken' + +###################################### +# SMF Configuration +###################################### +SMF_URL = '' + +###################################### +# Fleet-Up Configuration +###################################### +# FLEETUP_APP_KEY - The app key from http://fleet-up.com/Api/MyApps +# FLEETUP_USER_ID - The user id from http://fleet-up.com/Api/MyKeys +# FLEETUP_API_ID - The API id from http://fleet-up.com/Api/MyKeys +# FLEETUP_GROUP_ID - The id of the group you want to pull data from, see http://fleet-up.com/Api/Endpoints#groups_mygroupmemberships +###################################### +FLEETUP_APP_KEY = '' +FLEETUP_USER_ID = '' +FLEETUP_API_ID = '' +FLEETUP_GROUP_ID = '' + +PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.MD5PasswordHasher', +] + +LOGGING = None # Comment out to enable logging for debugging diff --git a/test_allianceauth/urls.py b/tests/urls.py similarity index 99% rename from test_allianceauth/urls.py rename to tests/urls.py index 53993dfd..8c2b2233 100644 --- a/test_allianceauth/urls.py +++ b/tests/urls.py @@ -1,4 +1,5 @@ from django.conf.urls import url + import allianceauth.urls from . import views diff --git a/test_allianceauth/views.py b/tests/views.py similarity index 100% rename from test_allianceauth/views.py rename to tests/views.py From b3d02b0c37b0d6f8db4e333ab20642062b987d70 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 28 Sep 2017 22:33:17 -0400 Subject: [PATCH 03/16] Add missing python3.4 typing requirement typing is included python3.5+ --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index a3409457..5271f772 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,9 @@ setup( install_requires=install_requires, extras_require={ 'testing': testing_extras, + ':python_version=="3.4"': ['typing'], }, + python_requires='~=3.4', license='GPLv2', packages=['allianceauth'], url='https://github.com/allianceauth/allianceauth', From 93eca76bf82cf109d15ab20111e3ef29670da374 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 28 Sep 2017 23:05:43 -0400 Subject: [PATCH 04/16] Remove legacy setup comments Remove legacy update script --- setup.py | 3 --- update.sh | 4 ---- 2 files changed, 7 deletions(-) delete mode 100755 update.sh diff --git a/setup.py b/setup.py index 5271f772..7e9e35dc 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,6 @@ install_requires = [ 'django-redis-cache>=1.7.1', 'django-celery-beat', - # Openfire 'openfire-restapi', 'sleekxmpp', @@ -41,8 +40,6 @@ setup( author='Alliance Auth', author_email='adarnof@gmail.com', description='Eve alliance auth for the 99 percent', - # Any changes in these package requirements - # should be reflected in requirements.txt as well. install_requires=install_requires, extras_require={ 'testing': testing_extras, diff --git a/update.sh b/update.sh deleted file mode 100755 index 936222e0..00000000 --- a/update.sh +++ /dev/null @@ -1,4 +0,0 @@ -pip install --upgrade -r requirements.txt -python manage.py migrate --fake-initial -yes yes | python manage.py collectstatic -c -python manage.py check --deploy \ No newline at end of file From c68574efc3b10d3a2fe10869ff040aa31ac08f68 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 5 Oct 2017 00:33:18 -0400 Subject: [PATCH 05/16] Create project template for deployment. --- .../__init__.py | 0 allianceauth/project_template/manage.py | 22 ++++++++++++ .../project_template/project_name/__init__.py | 0 .../project_name}/log/.gitignore | 0 .../project_name/settings/__init__.py | 0 .../project_name}/settings/base.py | 18 +++------- .../project_name/settings/local.py | 36 +++++++++++++++++++ .../project_name/static/.gitkeep | 0 .../project_name/templates/.gitkeep | 0 .../project_template/project_name/urls.py | 6 ++++ .../project_template/project_name/wsgi.py | 14 ++++++++ allianceauth/wsgi.py | 20 ----------- manage.py | 2 +- tests/settings.py | 6 ++-- 14 files changed, 85 insertions(+), 39 deletions(-) rename allianceauth/{settings => project_template}/__init__.py (100%) create mode 100644 allianceauth/project_template/manage.py create mode 100644 allianceauth/project_template/project_name/__init__.py rename allianceauth/{ => project_template/project_name}/log/.gitignore (100%) create mode 100644 allianceauth/project_template/project_name/settings/__init__.py rename allianceauth/{ => project_template/project_name}/settings/base.py (92%) create mode 100644 allianceauth/project_template/project_name/settings/local.py create mode 100644 allianceauth/project_template/project_name/static/.gitkeep create mode 100644 allianceauth/project_template/project_name/templates/.gitkeep create mode 100644 allianceauth/project_template/project_name/urls.py create mode 100644 allianceauth/project_template/project_name/wsgi.py delete mode 100644 allianceauth/wsgi.py diff --git a/allianceauth/settings/__init__.py b/allianceauth/project_template/__init__.py similarity index 100% rename from allianceauth/settings/__init__.py rename to allianceauth/project_template/__init__.py diff --git a/allianceauth/project_template/manage.py b/allianceauth/project_template/manage.py new file mode 100644 index 00000000..843d8a7d --- /dev/null +++ b/allianceauth/project_template/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name}}.settings.local") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/allianceauth/project_template/project_name/__init__.py b/allianceauth/project_template/project_name/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/log/.gitignore b/allianceauth/project_template/project_name/log/.gitignore similarity index 100% rename from allianceauth/log/.gitignore rename to allianceauth/project_template/project_name/log/.gitignore diff --git a/allianceauth/project_template/project_name/settings/__init__.py b/allianceauth/project_template/project_name/settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/settings/base.py b/allianceauth/project_template/project_name/settings/base.py similarity index 92% rename from allianceauth/settings/base.py rename to allianceauth/project_template/project_name/settings/base.py index 1630d76a..20f52cb2 100644 --- a/allianceauth/settings/base.py +++ b/allianceauth/project_template/project_name/settings/base.py @@ -61,7 +61,8 @@ CELERYBEAT_SCHEDULE = { } # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = os.path.dirname(PROJECT_DIR) MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', @@ -172,7 +173,7 @@ ALLOWED_HOSTS = ['*'] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': str(os.path.join(BASE_DIR, 'alliance_auth.sqlite')), + 'NAME': str(os.path.join(PROJECT_DIR, 'alliance_auth.sqlite3')), }, } @@ -228,7 +229,7 @@ LOGGING = { 'log_file': { 'level': 'INFO', # edit this line to change logging level to file 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(BASE_DIR, 'log/allianceauth.log'), + 'filename': os.path.join(PROJECT_DIR, 'log/allianceauth.log'), 'formatter': 'verbose', 'maxBytes': 1024 * 1024 * 5, # edit this line to change max log file size 'backupCount': 5, # edit this line to change number of log backups @@ -255,14 +256,3 @@ LOGGING = { }, } } - - -def add_auth_apps(APPS): - """ - Merges required auth apps with a list of custom user apps for project settings. - Leaves order of passed INSTALLED_APPS unchanged (passed apps come first) to allow overriding templates/static/etc - https://docs.djangoproject.com/en/2.0/ref/settings/#installed-apps - :param APPS: INSTALLED_APPS list - :return: Merged INSTALLED_APPS - """ - APPS += [app for app in INSTALLED_APPS if app not in APPS] diff --git a/allianceauth/project_template/project_name/settings/local.py b/allianceauth/project_template/project_name/settings/local.py new file mode 100644 index 00000000..614afede --- /dev/null +++ b/allianceauth/project_template/project_name/settings/local.py @@ -0,0 +1,36 @@ +from .base import * + +# These are required for Django to function properly +ROOT_URLCONF = '{{ project_name }}.urls' +WSGI_APPLICATION = '{{ project_name }}.wsgi.application' +STATICFILES_DIRS = [ + os.path.join(PROJECT_DIR, 'static'), +] +TEMPLATES[0]['DIRS'] += [os.path.join(PROJECT_DIR, 'templates')] +SECRET_KEY = '{{ secret_key }}' + +# Change this to change the name of the auth site +SITE_NAME = '{{ project_name }}' + +# Change this to enable/disable debug mode +DEBUG = False + + +###################################### +# SSO Settings # +###################################### +# Register an application at +# https://developers.eveonline.com +# and fill out these settings. +# Be sure to set the callback URL to +# https://example.com/sso/callback +# substituting your domain for example.com +###################################### +ESI_SSO_CLIENT_ID = '' +ESI_SSO_CLIENT_SECRET = '' +ESI_SSO_CALLBACK_URL = '' + + +###################################### +# Add any custom settings below here # +###################################### diff --git a/allianceauth/project_template/project_name/static/.gitkeep b/allianceauth/project_template/project_name/static/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/project_template/project_name/templates/.gitkeep b/allianceauth/project_template/project_name/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/project_template/project_name/urls.py b/allianceauth/project_template/project_name/urls.py new file mode 100644 index 00000000..ecff76d8 --- /dev/null +++ b/allianceauth/project_template/project_name/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import include, url +from allianceauth import urls + +urlpatterns = [ + url(r'', include(urls)), +] diff --git a/allianceauth/project_template/project_name/wsgi.py b/allianceauth/project_template/project_name/wsgi.py new file mode 100644 index 00000000..e13622f3 --- /dev/null +++ b/allianceauth/project_template/project_name/wsgi.py @@ -0,0 +1,14 @@ +""" +WSGI config for {{ project_name }} project. +It exposes the WSGI callable as a module-level variable named ``application``. +For more information on this file, see +https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.local") + +application = get_wsgi_application() diff --git a/allianceauth/wsgi.py b/allianceauth/wsgi.py deleted file mode 100644 index 39a09438..00000000 --- a/allianceauth/wsgi.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -WSGI config for alliance_auth project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ -""" - -import os -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "allianceauth.settings.base") - -# virtualenv wrapper, uncomment below to activate -# activate_env=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'env/bin/activate_this.py') -# execfile(activate_env, dict(__file__=activate_env)) - - -application = get_wsgi_application() diff --git a/manage.py b/manage.py index 7eec003b..6afed5b2 100644 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "allianceauth.settings.base") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "allianceauth.project_template.project_name.settings.base") try: from django.core.management import execute_from_command_line except ImportError: diff --git a/tests/settings.py b/tests/settings.py index 01d68dcd..f653c0e3 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -2,7 +2,7 @@ Alliance Auth Test Suite Django settings. """ -from allianceauth.settings.base import * +from allianceauth.project_template.project_name.settings.base import * # Use nose to run all tests TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' @@ -16,7 +16,7 @@ NOSE_ARGS = [ # Celery configuration CELERY_ALWAYS_EAGER = True # Forces celery to run locally for testing -INSTALLED_APPS = [ +INSTALLED_APPS += [ 'allianceauth.hrapplications', 'allianceauth.timerboard', 'allianceauth.srp', @@ -39,8 +39,6 @@ INSTALLED_APPS = [ 'django_nose', ] -add_auth_apps(INSTALLED_APPS) - ROOT_URLCONF = 'tests.urls' CACHES['default'] = {'BACKEND': 'django.core.cache.backends.db.DatabaseCache'} From c22d3a99670153af2497228c03234557e1c68e64 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 5 Oct 2017 00:52:45 -0400 Subject: [PATCH 06/16] Command line utility to create project. Shamelessly stolen from wagtail. --- allianceauth/bin/__init__.py | 0 allianceauth/bin/allianceauth.py | 79 ++++++++++++++++++++++++++++++++ setup.py | 4 ++ 3 files changed, 83 insertions(+) create mode 100644 allianceauth/bin/__init__.py create mode 100644 allianceauth/bin/allianceauth.py diff --git a/allianceauth/bin/__init__.py b/allianceauth/bin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/bin/allianceauth.py b/allianceauth/bin/allianceauth.py new file mode 100644 index 00000000..9094d409 --- /dev/null +++ b/allianceauth/bin/allianceauth.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +import os +from optparse import OptionParser + +from django.core.management import ManagementUtility + + +def create_project(parser, options, args): + # Validate args + if len(args) < 2: + parser.error("Please specify a name for your Alliance Auth installation") + elif len(args) > 3: + parser.error("Too many arguments") + + project_name = args[1] + try: + dest_dir = args[2] + except IndexError: + dest_dir = None + + # Make sure given name is not already in use by another python package/module. + try: + __import__(project_name) + except ImportError: + pass + else: + parser.error("'%s' conflicts with the name of an existing " + "Python module and cannot be used as a project " + "name. Please try another name." % project_name) + + print("Creating an Alliance Auth project called %(project_name)s" % {'project_name': project_name}) # noqa + + # Create the project from the Alliance Auth template using startapp + + # First find the path to Alliance Auth + import allianceauth + allianceauth_path = os.path.dirname(allianceauth.__file__) + template_path = os.path.join(allianceauth_path, 'project_template') + + # Call django-admin startproject + utility_args = ['django-admin.py', + 'startproject', + '--template=' + template_path, + project_name] + + if dest_dir: + utility_args.append(dest_dir) + + utility = ManagementUtility(utility_args) + utility.execute() + + print("Success! %(project_name)s has been created" % {'project_name': project_name}) # noqa + + +COMMANDS = { + 'start': create_project, +} + + +def main(): + # Parse options + parser = OptionParser(usage="Usage: %prog start project_name [directory]") + (options, args) = parser.parse_args() + + # Find command + try: + command = args[0] + except IndexError: + parser.print_help() + return + + if command in COMMANDS: + COMMANDS[command](parser, options, args) + else: + parser.error("Unrecognised command: " + command) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/setup.py b/setup.py index 7e9e35dc..3e49f6cc 100644 --- a/setup.py +++ b/setup.py @@ -51,4 +51,8 @@ setup( url='https://github.com/allianceauth/allianceauth', zip_safe=False, include_package_data=True, + entry_points=""" + [console_scripts] + allianceauth=allianceauth.bin.allianceauth:main + """, ) From 03447abf5c14961b690f1a9efcc6890704e97275 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 5 Oct 2017 01:23:33 -0400 Subject: [PATCH 07/16] Update base settings with command. --- allianceauth/bin/allianceauth.py | 42 +++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/allianceauth/bin/allianceauth.py b/allianceauth/bin/allianceauth.py index 9094d409..6bf70cb1 100644 --- a/allianceauth/bin/allianceauth.py +++ b/allianceauth/bin/allianceauth.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import os from optparse import OptionParser - from django.core.management import ManagementUtility @@ -52,14 +51,51 @@ def create_project(parser, options, args): print("Success! %(project_name)s has been created" % {'project_name': project_name}) # noqa +def update_settings(parser, options, args): + if len(args) < 2: + parser.error("Please specify the path to your Alliance Auth installation") + elif len(args) > 2: + parser.error("Too many arguments") + + project_path = args[1] + + # find the target settings/base.py file, handing both the project and app as valid paths + try: + # given path is to the app + settings_path = os.path.join(project_path, 'settings/base.py') + assert os.path.exists(settings_path) + except AssertionError: + try: + # given path is to the project, so find the app within it + dirname = os.path.split(project_path)[-1] + settings_path = os.path.join(project_path, dirname, 'settings/base.py') + assert os.path.exists(settings_path) + except AssertionError: + parser.error("Unable to locate the Alliance Auth project at %s" % project_path) + + # first find the path to the Alliance Auth template settings + import allianceauth + allianceauth_path = os.path.dirname(allianceauth.__file__) + template_path = os.path.join(allianceauth_path, 'project_template') + template_settings_path = os.path.join(template_path, 'project_name/settings/base.py') + + # overwrite the local project's base settings + print("Updating the settings at %s with the template at %s" % (settings_path, template_settings_path)) + with open(template_settings_path, 'r') as template, open(settings_path, 'w') as target: + target.write(template.read()) + + print("Successfully updated Alliance Auth settings.") + + COMMANDS = { 'start': create_project, + 'update': update_settings, } def main(): # Parse options - parser = OptionParser(usage="Usage: %prog start project_name [directory]") + parser = OptionParser(usage="Usage: %prog [start|update] project_name [directory]") (options, args) = parser.parse_args() # Find command @@ -76,4 +112,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() From d43b8cf0e5141cb28ecbddc1678301357fef9583 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 5 Oct 2017 03:10:27 -0400 Subject: [PATCH 08/16] Update install docs to reflect new magical commands. --- .../project_name/settings/base.py | 1 - .../project_name/settings/local.py | 32 ++++ .../auth/cloudflare/page_rules.jpg | Bin 145351 -> 0 bytes docs/installation/auth/allianceauth.md | 147 ++++++++++++++++++ docs/installation/auth/centos.md | 80 ---------- docs/installation/auth/cloudflare.md | 35 ----- docs/installation/auth/dependencies.md | 44 ------ docs/installation/auth/index.md | 9 +- docs/installation/auth/quickstart.md | 11 -- docs/installation/auth/ubuntu.md | 71 --------- 10 files changed, 181 insertions(+), 249 deletions(-) delete mode 100644 docs/_static/images/installation/auth/cloudflare/page_rules.jpg create mode 100644 docs/installation/auth/allianceauth.md delete mode 100644 docs/installation/auth/centos.md delete mode 100644 docs/installation/auth/cloudflare.md delete mode 100644 docs/installation/auth/dependencies.md delete mode 100644 docs/installation/auth/quickstart.md delete mode 100644 docs/installation/auth/ubuntu.md diff --git a/allianceauth/project_template/project_name/settings/base.py b/allianceauth/project_template/project_name/settings/base.py index 20f52cb2..ca5b1616 100644 --- a/allianceauth/project_template/project_name/settings/base.py +++ b/allianceauth/project_template/project_name/settings/base.py @@ -150,7 +150,6 @@ USE_TZ = True # https://docs.djangoproject.com/en/1.10/howto/static-files/ STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, "static") # Bootstrap messaging css workaround MESSAGE_TAGS = { diff --git a/allianceauth/project_template/project_name/settings/local.py b/allianceauth/project_template/project_name/settings/local.py index 614afede..69a0c63c 100644 --- a/allianceauth/project_template/project_name/settings/local.py +++ b/allianceauth/project_template/project_name/settings/local.py @@ -6,6 +6,7 @@ WSGI_APPLICATION = '{{ project_name }}.wsgi.application' STATICFILES_DIRS = [ os.path.join(PROJECT_DIR, 'static'), ] +STATIC_ROOT = "/var/www/{{ project_name }}/static/" TEMPLATES[0]['DIRS'] += [os.path.join(PROJECT_DIR, 'templates')] SECRET_KEY = '{{ secret_key }}' @@ -15,6 +16,23 @@ SITE_NAME = '{{ project_name }}' # Change this to enable/disable debug mode DEBUG = False +####################################### +# Database Settings # +####################################### +# Uncomment and change the database name +# and credentials to use MySQL/MariaDB. +# Leave commented to use sqlite3 +####################################### +""" +DATABASES['default'] = { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'alliance_auth', + 'USER': os.environ.get('AA_DB_DEFAULT_USER', ''), + 'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', ''), + 'HOST': os.environ.get('AA_DB_DEFAULT_HOST', '127.0.0.1'), + 'PORT': os.environ.get('AA_DB_DEFAULT_PORT', '3306'), +} +""" ###################################### # SSO Settings # @@ -30,6 +48,20 @@ ESI_SSO_CLIENT_ID = '' ESI_SSO_CLIENT_SECRET = '' ESI_SSO_CALLBACK_URL = '' +###################################### +# Email Settings # +###################################### +# Alliance Auth validates emails before +# new users can log in. +# It's recommended to use a free service +# like SparkPost or Mailgun to send email. +# https://www.sparkpost.com/docs/integrations/django/ +################# +EMAIL_HOST = '' +EMAIL_PORT = 587 +EMAIL_HOST_USER = '' +EMAIL_HOST_PASSWORD = '' +EMAIL_USE_TLS = True ###################################### # Add any custom settings below here # diff --git a/docs/_static/images/installation/auth/cloudflare/page_rules.jpg b/docs/_static/images/installation/auth/cloudflare/page_rules.jpg deleted file mode 100644 index ddaec23ba173262421484717229b34d71fa3b538..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 145351 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 diff --git a/docs/installation/auth/allianceauth.md b/docs/installation/auth/allianceauth.md new file mode 100644 index 00000000..41e9de15 --- /dev/null +++ b/docs/installation/auth/allianceauth.md @@ -0,0 +1,147 @@ +# Alliance Auth Installation + +```eval_rst +.. important:: + Installation is easiest as the root user. Log in as root or a user with sudo powers. +``` + +## Dependencies + +```eval_rst +.. hint:: + CentOS: A few packages are included in a non-default repository. Add it and update the package lists. :: + + yum -y install https://centos7.iuscommunity.org/ius-release.rpm + yum update +``` + +### Python + +Alliance Auth requires python3.4 or higher. Ensure it is installed on your server before proceeding. + +Ubuntu: + + apt-get install python3 python3-dev python3-setuptools python3-pip + +CentOS: + + yum install python36u python36u-devel python36u-setuptools python36u-pip bzip2-devel + +### Database + +It's recommended to use a database service instead of sqlite. Many options are available, but this guide will use MariaDB. Ensure it's installed on your server before proceeding. It can be installed with: + +Ubuntu: + + apt-get install mariadb-server mysql-client libmysqlclient-dev + +CentOS: + + yum install mariadb-server mariadb-devel mariadb + +### Redis and Other Tools + +A few extra utilities are also required for installation of packages. + +Ubuntu: + + apt-get install unzip git redis-server curl libssl-dev libbz2-dev libffi-dev + +CentOS: + + yum install gcc gcc-c++ unzip git redis curl + +```eval_rst +.. important:: + CentOS: Make sure redis is running before continuing: :: + + systemctl enable redis.service + systemctl start redis.service +``` + +## Database Setup + +Alliance Auth needs a MySQL user account and database. Create one as follows, replacing `PASSWORD` with an actual secure password: + + mysql -u root -p + CREATE USER 'allianceserver'@'localhost' IDENTIFIED BY 'PASSWORD'; + CREATE DATABASE alliance_auth; + GRANT ALL PRIVILEGES ON alliance_auth . * TO 'allianceserver'@'localhost'; + +Secure your MySQL / MariaDB server by typing `mysql_secure_installation` + +## Auth Install + +### User Account + +For security and permissions, it’s highly recommended you create a separate user to install under. + +Ubuntu: + + adduser --disabled-login allianceserver + +CentOS: + + useradd -s /bin/nologin allianceserver + +### Virtual Environment + +Create a Python virtual environment and put it somewhere convenient (e.g. `/home/allianceserver/venv/auth/`) + + python3 -m venv /home/allianceserver/venv/auth/ + +```eval_rst +.. tip:: + A virtual environment provides support for creating a lightweight "copy" of Python with their own site directories. Each virtual environment has its own Python binary (allowing creation of environments with various Python versions) and can have its own independent set of installed Python packages in its site directories. You can read more about virtual environments on the Python_ docs. +.. _Python: https://docs.python.org/3/library/venv.html +``` + +Activate the virtualenv using `source /home/allianceserver/venv/auth/bin/activate`. Note the `/bin/activate` on the end of the path. + +```eval_rst +.. hint:: + Each time you come to do maintenance on your Alliance Auth installation, you should activate your virtual environment first. When finished, deactivate it with the 'deactivate' command. +``` + +### Alliance Auth Project + +Now you can install the library using `pip install allianceauth`. This will install Alliance Auth and all its python dependencies. + +Now you need to create the application that will run the Alliance Auth install. Ensure you are in the allianceserver home directory by issuing `cd /home/allianceserver`. + +Issue `allianceauth start myauth` to bootstrap the Django application that will run Alliance Auth. You can rename it from `myauth` to anything you'd like: this name is shown by default as the site name but that can be changed later. + +The settings file needs configuring. Edit the template at `myauth/myauth/settings/local.py`. Be sure to configure the EVE SSO and Email settings. + +Django needs to install models to the database before it can start. + + python /home/allianceserver/myauth/manage.py migrate + +Now we need to round up all the static files required to render templates. Make a directory to serve them from and populate it. + + mkdir /var/www/myauth/static + python /home/allianceserver/myauth/manage.py collectstatic + chown -R www-data:www-data /var/www/myauth/static + +Check to ensure your settings are valid. + + python /home/allianceserver/myauth/manage.py check + +And finally ensure the allianceserver user has read/write permissions to this directory before proceeding. + + chown -R allianceserver:allianceserver /home/allianceserver/myauth + +## Superuser + +Before proceeding it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account. + + python /home/allianceserver/myauth/manage.py createsuperuser + +```eval_rst +.. important:: + Be sure to add a main character to this account before attempting to activate services with it. +``` + +## Webserver + +Once installed, move onto the [Gunicorn Guide](gunicorn.md) and decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md). You will also need to install [supervisor](supervisor.md) to run the background tasks. diff --git a/docs/installation/auth/centos.md b/docs/installation/auth/centos.md deleted file mode 100644 index 1fae060e..00000000 --- a/docs/installation/auth/centos.md +++ /dev/null @@ -1,80 +0,0 @@ -# CentOS Installation - -It's recommended to update all packages before proceeding. - `sudo yum update` - `sudo yum upgrade` - `sudo reboot` - -Now install all [dependencies](dependencies.md). - - sudo yum install xxxxxxx - -replacing the x's with the list of packages. - -Make sure redis is running before continuing: - - systemctl enable redis.service - systemctl start redis.service - -For security and permissions, it's highly recommended you create a user to install under who is not the root account. - - sudo adduser allianceserver - sudo passwd allianceserver - -This user needs sudo powers. Add them by editing the sudoers file: - - sudo nano /etc/sudoers - -Find the line which says `root ALL=(ALL) ALL` - beneath it add another line `allianceserver ALL=(ALL) ALL` - now reboot. - -**From this point on you need to be logged in as the allianceserver user** - -Start your mariadb server `sudo systemctl start mariadb` - -Secure your MYSQL / Maria-db server by typing `mysql_secure_installation ` - -AllianceAuth needs a MySQL user account. Create one as follows, replacing `PASSWORD` with an actual secure password: - - mysql -u root -p - CREATE USER 'allianceserver'@'localhost' IDENTIFIED BY 'PASSWORD'; - GRANT ALL PRIVILEGES ON * . * TO 'allianceserver'@'localhost'; - -Now we need to make the requisite database. - - create database alliance_auth; - -Create a Python virtual environment and put it somewhere convenient (e.g. `~/venv/aauth/`) - - python3.6 -m venv /path/to/new/virtual/environment - -A virtual environment provides support for creating a lightweight "copy" of Python with their own site directories. Each virtual environment has its own Python binary (allowing creation of environments with various Python versions) and can have its own independent set of installed Python packages in its site directories. You can read more about virtual environments on the [Python docs](https://docs.python.org/3/library/venv.html). - -Activate the virtualenv using `source /path/to/new/virtual/environment/bin/activate`. Note the `/bin/activate` on the end of the path. Each time you come to do maintenance on your Alliance Auth installation, you should activate your virtual environment first. - -Now you can install the library using `pip install allianceauth`. This will install Alliance Auth and all its python dependencies. - -Ensure you are in the allianceserver home directory by issuing `cd ~`. - -Now you need to create the application that will run the Alliance Auth install. - -Issue `django-admin startproject myauth` to bootstrap the Django application that will run Auth. You can rename it from `myauth` anything you'd like, the name is not important for auth. - -Grab the example settings file from the [Alliance Auth repository](https://github.com/allianceauth/allianceauth/blob/master/alliance_auth/settings.py.example) for the relevant version you're installing. - -The settings file needs configuring. See [this lengthy guide](settings.md) for specifics. - -Django needs to install models to the database before it can start. - - python manage.py migrate - -Now we need to round up all the static files required to render templates. Answer ‘yes’ when prompted. - - python manage.py collectstatic - -Test the server by starting it manually. - - python manage.py runserver 0.0.0.0:8000 - -If you see an error, stop, read it, and resolve it. If the server comes up and you can access it at `yourip:8000`, you're golden. It's ok to stop the server if you're going to be installing a WSGI server to run it. **Do not use runserver in production!** - -Once installed, move onto the [Gunicorn Guide](gunicorn.md) and decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md). You will also need to install [supervisor](supervisor.md) to run the background tasks. \ No newline at end of file diff --git a/docs/installation/auth/cloudflare.md b/docs/installation/auth/cloudflare.md deleted file mode 100644 index 13ec9f71..00000000 --- a/docs/installation/auth/cloudflare.md +++ /dev/null @@ -1,35 +0,0 @@ -# Cloudflare - -CloudFlare offers free SSL and DDOS mitigation services. Why not take advantage of it? - -## Setup - -You’ll need to register an account on [CloudFlare’s site.](https://www.cloudflare.com/) - -Along the top bar, select `Add Site` - -Enter your domain name. It will scan records and let you know you can add the site. Continue setup. - -On the next page you should see an A record for example.com pointing at your server IP. If not, manually add one: - - A example.com my.server.ip.address Automatic TTL - -Add the record and ensure the cloud under Status is orange. If not, click it. This ensures traffic gets screened by CloudFlare. - -If you want forums or kb on a subdomain, and want these to be protected by CloudFlare, add an additional record for for each subdomain in the following format, ensuring the cloud is orange: - - CNAME subdomain example.com Automatic TTL - -CloudFlare blocks ports outside 80 and 443 on hosts it protects. This means, if the cloud is orange, only web traffic will get through. We need to reconfigure AllianceAuth to provide services under a subdomain. Configure these subdomains as above, but ensure the cloud is not orange (arrow should go around a grey cloud). - -## Redirect to HTTPS - -Now we need to configure the https redirect to force all traffic to https. Along the top bar of CloudFlare, select `Page Rules`. Add a new rule, Pattern is example.com, toggle the `Always use https` to ON, and save. It’ll take a few minutes to propagate. - -![infographic](/_static/images/installation/auth/cloudflare/page_rules.jpg) - -## Update Auth URLs - -Edit settings.py and replace everything that has a HTTP with HTTPS (except anything with a port on the end, like `OPENFIRE_ADDRESS`) - -And there we have it. You’re DDOS-protected with free SSL. \ No newline at end of file diff --git a/docs/installation/auth/dependencies.md b/docs/installation/auth/dependencies.md deleted file mode 100644 index af16d715..00000000 --- a/docs/installation/auth/dependencies.md +++ /dev/null @@ -1,44 +0,0 @@ -# Dependencies - -## Ubuntu - -Tested on Ubuntu 14.04LTS, 16.04LTS and 17.04. Package names and repositories may vary. Please note that 14.04LTS comes with Python 3.4 which may be insufficient. - -### Core -Required for base auth site - -#### Python - - python3 python3-dev python3-setuptools python3-pip - -#### MySQL - - mariadb-server mysql-client libmysqlclient-dev - -#### Utilities - - unzip git redis-server curl libssl-dev libbz2-dev libffi-dev - -## CentOS 7 - -Tested on CentOS 7 - -### Add The IUS Repository - - sudo yum -y install https://centos7.iuscommunity.org/ius-release.rpm - yum update - -### Core -Required for base auth site - -#### Python - - python36u python36u-devel python36u-setuptools python36u-pip bzip2-devel - -#### MySQL - - mariadb-server mariadb-devel mariadb - -#### Utilities - - gcc gcc-c++ unzip git redis curl nano diff --git a/docs/installation/auth/index.md b/docs/installation/auth/index.md index 0e314277..d245828b 100644 --- a/docs/installation/auth/index.md +++ b/docs/installation/auth/index.md @@ -3,14 +3,9 @@ ```eval_rst .. toctree:: - dependencies - ubuntu - centos - settings + allianceauth + gunicorn nginx apache - gunicorn - cloudflare supervisor - quickstart ``` diff --git a/docs/installation/auth/quickstart.md b/docs/installation/auth/quickstart.md deleted file mode 100644 index d5a43e6f..00000000 --- a/docs/installation/auth/quickstart.md +++ /dev/null @@ -1,11 +0,0 @@ -# Quick Start - -Once you’ve installed AllianceAuth, perform these steps to get yourself up and running. - -First you need a superuser account. You can use this as a personal account. From the command line, `python manage.py createsuperuser` and follow the prompts. - -The big goal of AllianceAuth is the automation of group membership, so we’ll need some groups. In the admin interface, select `Groups`, then at the top-right select `Add Group`. Give it a name and select permissions. Special characters (including spaces) are removing before syncing to services, so try not to have group names which will be the same upon cleaning. Repeat for all the groups you see fit, whenever you need a new one. Check the [groups documentation](../../features/groups.md) for more details on group configuration. - -### Background Processes - -To start the background processes you should utilise [supervisor](supervisor.md). Previously screen was suggested to keep these tasks running, however this is no longer the preferred method. diff --git a/docs/installation/auth/ubuntu.md b/docs/installation/auth/ubuntu.md deleted file mode 100644 index 0616e2fd..00000000 --- a/docs/installation/auth/ubuntu.md +++ /dev/null @@ -1,71 +0,0 @@ -# Ubuntu Installation - -It’s recommended to update all packages before proceeding. - - sudo apt-get update - sudo apt-get upgrade - sudo reboot - -Now install all [dependencies](dependencies.md). - - sudo apt-get install xxxxxxx -replacing the xs with the list of packages. - -For security and permissions, it’s highly recommended you create a user to install under who is not the root account. - - sudo adduser allianceserver - -This user needs sudo powers. Add them by editing the sudoers file: - - sudo nano /etc/sudoers - -Find the line which says `root ALL=(ALL:ALL) ALL` - beneath it add another line `allianceserver ALL=(ALL:ALL) ALL` - now reboot. - -**From this point on you need to be logged in as the allianceserver user** - -AllianceAuth needs a MySQL user account. Create one as follows, replacing `PASSWORD` with an actual secure password: - - mysql -u root -p - CREATE USER 'allianceserver'@'localhost' IDENTIFIED BY 'PASSWORD'; - GRANT ALL PRIVILEGES ON * . * TO 'allianceserver'@'localhost'; - -Now we need to make the requisite database. - - create database alliance_auth; - - -Create a Python virtual environment and put it somewhere convenient (e.g. `~/venv/aauth/`) - - python3 -m venv /path/to/new/virtual/environment - -A virtual environment provides support for creating a lightweight "copy" of Python with their own site directories. Each virtual environment has its own Python binary (allowing creation of environments with various Python versions) and can have its own independent set of installed Python packages in its site directories. You can read more about virtual environments on the [Python docs](https://docs.python.org/3/library/venv.html). - -Activate the virtualenv using `source /path/to/new/virtual/environment/bin/activate`. Note the `/bin/activate` on the end of the path. Each time you come to do maintenance on your Alliance Auth installation, you should activate your virtual environment first. - -Now you can install the library using `pip install allianceauth`. This will install Alliance Auth and all its python dependencies. - -Ensure you are in the allianceserver home directory by issuing `cd ~`. - -Now you need to create the application that will run the Alliance Auth install. - -Issue `django-admin startproject myauth` to bootstrap the Django application that will run Auth. You can rename it from `myauth` anything you'd like, the name is not important for auth. - -Grab the example settings file from the [Alliance Auth repository](https://github.com/allianceauth/allianceauth/blob/master/alliance_auth/settings.py.example) for the relevant version you're installing. - -The settings file needs configuring. See [this lengthy guide](settings.md) for specifics. - -Django needs to install models to the database before it can start. - - python manage.py migrate - -Now we need to round up all the static files required to render templates. Answer ‘yes’ when prompted. - - python manage.py collectstatic - -Test the server by starting it manually. - - python manage.py runserver 0.0.0.0:8000 - -If you see an error, stop, read it, and resolve it. If the server comes up and you can access it at `yourip:8000`, you're golden. It's ok to stop the server if you're going to be installing a WSGI server to run it. **Do not use runserver in production!** - -Once installed, move onto the [Gunicorn Guide](gunicorn.md) and decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md). You will also need to install [supervisor](supervisor.md) to run the background tasks. From 2efb45943c6d14518623054e6289287526dbb088 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 5 Oct 2017 11:00:32 -0400 Subject: [PATCH 09/16] Do not check coverage of project template. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 6df2ec18..c2d0bab1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,6 +6,7 @@ source = omit = */migrations/* */example/* + */project_template/* [report] exclude_lines = From 3ff29ba3b0eb95d2db077ad48edb49a174b8de20 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 5 Oct 2017 11:08:43 -0400 Subject: [PATCH 10/16] Do not check coverage of bin commands. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index c2d0bab1..39b49674 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,6 +7,7 @@ omit = */migrations/* */example/* */project_template/* + */bin/* [report] exclude_lines = From d54d80b0b8a262e9a63292332fda4a033609200c Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 5 Oct 2017 12:48:33 -0400 Subject: [PATCH 11/16] Include updating instructions. Tweak wording of install sections. --- docs/installation/auth/allianceauth.md | 32 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/installation/auth/allianceauth.md b/docs/installation/auth/allianceauth.md index 41e9de15..3b9e10de 100644 --- a/docs/installation/auth/allianceauth.md +++ b/docs/installation/auth/allianceauth.md @@ -7,6 +7,8 @@ ## Dependencies +Alliance Auth can be installed on any operating system. Dependencies are provided below for two of the most popular server platforms, Ubuntu and CentOS. To install on your favourite flavour of linux, identify and install equivalent packages to the ones listed here. + ```eval_rst .. hint:: CentOS: A few packages are included in a non-default repository. Add it and update the package lists. :: @@ -21,15 +23,15 @@ Alliance Auth requires python3.4 or higher. Ensure it is installed on your serve Ubuntu: - apt-get install python3 python3-dev python3-setuptools python3-pip + apt-get install python3 python3-dev python3-venv python3-setuptools python3-pip CentOS: - yum install python36u python36u-devel python36u-setuptools python36u-pip bzip2-devel + yum install python36u python36u-devel python36u-setuptools python36u-pip ### Database -It's recommended to use a database service instead of sqlite. Many options are available, but this guide will use MariaDB. Ensure it's installed on your server before proceeding. It can be installed with: +It's recommended to use a database service instead of sqlite. Many options are available, but this guide will use MariaDB. Ubuntu: @@ -49,26 +51,25 @@ Ubuntu: CentOS: - yum install gcc gcc-c++ unzip git redis curl + yum install gcc gcc-c++ unzip git redis curl bzip2-devel ```eval_rst .. important:: - CentOS: Make sure redis is running before continuing: :: - + CentOS: Make sure redis is running before continuing. :: + systemctl enable redis.service systemctl start redis.service ``` ## Database Setup -Alliance Auth needs a MySQL user account and database. Create one as follows, replacing `PASSWORD` with an actual secure password: +Alliance Auth needs a MySQL user account and database. Open an SQL shell with `mysql -u root -p` and create them as follows, replacing `PASSWORD` with an actual secure password: - mysql -u root -p CREATE USER 'allianceserver'@'localhost' IDENTIFIED BY 'PASSWORD'; CREATE DATABASE alliance_auth; GRANT ALL PRIVILEGES ON alliance_auth . * TO 'allianceserver'@'localhost'; -Secure your MySQL / MariaDB server by typing `mysql_secure_installation` +Close the SQL shell and secure your database server with the `mysql_secure_installation` command. ## Auth Install @@ -105,11 +106,11 @@ Activate the virtualenv using `source /home/allianceserver/venv/auth/bin/activat ### Alliance Auth Project -Now you can install the library using `pip install allianceauth`. This will install Alliance Auth and all its python dependencies. +You can install the library using `pip install allianceauth`. This will install Alliance Auth and all its python dependencies. Now you need to create the application that will run the Alliance Auth install. Ensure you are in the allianceserver home directory by issuing `cd /home/allianceserver`. -Issue `allianceauth start myauth` to bootstrap the Django application that will run Alliance Auth. You can rename it from `myauth` to anything you'd like: this name is shown by default as the site name but that can be changed later. +The `allianceauth start myauth` command will bootstrap a Django project which will run Alliance Auth. You can rename it from `myauth` to anything you'd like: this name is shown by default as the site name but that can be changed later. The settings file needs configuring. Edit the template at `myauth/myauth/settings/local.py`. Be sure to configure the EVE SSO and Email settings. @@ -145,3 +146,12 @@ Before proceeding it is essential to create a superuser account. This account wi ## Webserver Once installed, move onto the [Gunicorn Guide](gunicorn.md) and decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md). You will also need to install [supervisor](supervisor.md) to run the background tasks. + + +## Updating + +Periodically [new releases](https://github.com/allianceauth/allianceauth/releases/) are issued with bug fixes and new features. To update your install, simply activate your virtual environment and update with `pip install --upgrade allianceauth`. Be sure to read the release notes which will highlight changes. + +Some releases come with changes to settings: update your project's settings with `allianceauth update /home/allianceserver/myauth`. + +Always restart celery and gunicorn after updating. \ No newline at end of file From 09b788fef630ce003f921ddb6d05dc8490040b0c Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 5 Oct 2017 12:50:51 -0400 Subject: [PATCH 12/16] Remove legacy settings documentation. Settings are self-documented in the new install template. Services install docs should be updated to indicate settings need to be added and how to configure them. --- docs/installation/auth/settings.md | 376 ----------------------------- 1 file changed, 376 deletions(-) delete mode 100644 docs/installation/auth/settings.md diff --git a/docs/installation/auth/settings.md b/docs/installation/auth/settings.md deleted file mode 100644 index 6d1afdea..00000000 --- a/docs/installation/auth/settings.md +++ /dev/null @@ -1,376 +0,0 @@ -# Settings Overview - -The `alliance_auth/settings.py` file is used to pass settings to the django app needed to run. - -### Words of Warning - -Certain fields are quite sensitive to leading `http://` and trailing `/` - if you see these present in the default text, be sure to include them in your values. - -Every variable value is opened and closed with a single apostrophe `'` - please do not include these in your values or it will break things. If you absolutely must, replace them at the opening and closing of the value with double quotes `"`. - -Certain variables are booleans, and come in a form that looks like this: - - MEMBER_CORP_GROUPS = 'True' == os.environ.get('AA_MEMBER_CORP_GROUPS', 'True') - -They're handled as strings because when settings are exported from shell commands (eg `export AA_MEMBER_CORP_GROUPS False`) they're interpreted as strings, so a string comparison is done. - -When changing these booleans, edit the setting within the brackets (eg `('AA_MEMBER_CORP_GROUPS', 'True')` vs `('AA_MEMBER_CORP_GROUPS', 'False')`) and not the `True` earlier in the statement. Otherwise these will have unexpected behaviours. - -# Fields to Modify - -## Required - - [SECRET_KEY](#secret-key) - - Use [this tool](http://www.miniwebtool.com/django-secret-key-generator/) to generate a key on initial install - - [DEBUG](#debug) - - If issues are encountered, set this to `True` to view a more detailed error report, otherwise set `False` - - [ALLOWED_HOSTS](#allowed-hosts) - - This restricts web addresses auth will answer to. Separate with commas. - - Should include localhost `127.0.0.1` and `example.com` - - To allow from all, include `'*'` - - [DATABASES](#databases) - - Fill out the database name and user credentials to manage the auth database. - - [DOMAIN](#domain) - - Set to the domain name AllianceAuth will be accessible under - - [EMAIL_HOST_USER](#email-host-user) - - Username to send emails from. If gmail account, the full gmail address. - - [EMAIL_HOST_PASSWORD](#email-host-password) - - Password for the email user. - - [CORP_IDS](#corp-ids) - - List of corp IDs who are members. Exclude if their alliance is in `ALLIANCE_IDS` - - [ALLIANCE_IDS](#alliance-ids) - - List of alliance IDs who are members. - - [ESI_SSO_CLIENT_ID](#esi-sso-client_id) - - EVE application ID from the developers site. See the [SSO Configuration Instruction](#sso-settings) - - [ESI_SSO_CLIENT_SECRET](#esi-sso-client-secret) - - EVE application secret from the developers site. - - [ESI_SSO_CALLBACK_URL](#esi-sso-callback-url) - - OAuth callback URL. Should be `https://mydomain.com/sso/callback` - -## Services -### IPBoard -If using IPBoard, the following need to be set in accordance with the [install instructions](../services/ipboard3.md) - - [IPBOARD_ENDPOINT](#ipboard-endpoint) - - [IPBOARD_APIKEY](#ipboard-apikey) - - [IPBOARD_APIMODULE](#ipboard-apimodule) - -### XenForo -If using XenForo, the following need to be set in accordance with the [install instructions](../services/xenforo.md) - - [XENFORO_ENDPOINT](#xenforo-endpoint) - - [XENFORO_APIKEY](#xenforo-apikey) - -### Openfire -If using Openfire, the following need to be set in accordance with the [install instructions](../services/openfire.md) - - [JABBER_URL](#jabber-url) - - [JABBER_PORT](#jabber-port) - - [JABBER_SERVER](#jabber-server) - - [OPENFIRE_ADDRESS](#openfire-address) - - [OPENFIRE_SECRET_KEY](#openfire-secret-key) - - [BROADCAST_USER](#broadcast-user) - - [BROADCAST_USER_PASSWORD](#broadcast-user-password) - - [BROADCAST_SERVICE_NAME](#broadcast-service-name) - - [BROADCAST_IGNORE_INVALID_CERT](#broadcast-ignore-invalid-cert) - -### Mumble -If using Mumble, the following needs to be set to the address of the mumble server: - - [MUMBLE_URL](#mumble-url) - -### PHPBB3 -If using phpBB3, the database needs to be defined. - -### Teamspeak3 -If using Teamspeak3, the following need to be set in accordance with the [install instrictions](../services/teamspeak3.md) - - [TEAMSPEAK3_SERVER_IP](#teamspeak3-server-ip) - - [TEAMSPEAK3_SERVER_PORT](#teamspeak3-server-port) - - [TEAMSPEAK3_SERVERQUERY_USER](#teamspeak3-serverquery-user) - - [TEAMSPEAK3_SERVERQUERY_PASSWORD](#teamspeak3-serverquery-password) - - [TEAMSPEAK3_VIRTUAL_SERVER](#teamspeak3-virtual-server) - - [TEAMSPEAK3_PUBLIC_URL](#teamspeak3-public-url) - -### Discord -If connecting to a Discord server, set the following in accordance with the [install instructions](../services/discord.md) - - [DISCORD_GUILD_ID](#discord-guild-id) - - [DISCORD_BOT_TOKEN](#discord-bot-token) - - [DISCORD_INVITE_CODE](#discord-invite-code) - - [DISCORD_APP_ID](#discord-app-id) - - [DISCORD_APP_SECRET](#discord-app-secret) - - [DISCORD_CALLBACK_URL](#discord-callback-url) - - [DISCORD_SYNC_NAMES](#discord-sync-names) - -### Discourse -If connecting to Discourse, set the following in accordance with the [install instructions](../services/discourse.md) - - [DISCOURSE_URL](#discourse-url) - - [DISCOURSE_API_USERNAME](#discourse-api-username) - - [DISCOURSE_API_KEY](#discourse-api-key) - - [DISCOURSE_SSO_SECRET](#discourse-sso-secret) - -### IPSuite4 -If using IPSuite4 (aka IPBoard4) the following are required: - - [IPS4_URL](#ips4-url) - - the database needs to be defined - -### SMF -If using SMF the following are required: - - [SMF_URL](#smf-url) - - the database needs to be defined - -## Optional -### Standings -To allow access to blues, a corp API key is required to pull standings from. Corp does not need to be owning corp or in owning alliance. Required mask is 16 (Communications/ContactList) - - [CORP_API_ID](#corp-api-id) - - [CORP_API_VCODE](#corp-api-vcode) - -### API Key Audit URL -To define what happens when an API is clicked, set according to [these instructions](#hr-configuration) - - [API_KEY_AUDIT_URL](#api-key-audit-url) - -### Auto Groups -Groups can be automatically assigned based on a user's corp or alliance. Set the following to `True` to enable this feature. - - [MEMBER_CORP_GROUPS](#member-corp-groups) - - [MEMBER_ALLIANCE_GROUPS](#member-alliance-groups) - - [BLUE_CORP_GROUPS](#blue-corp-groups) - - [BLUE_ALLIANCE_GROUPS](#blue-alliance-groups) - -### Fleet-Up -Fittings and operations can be imported from Fleet-Up. Define the following to do so. - - [FLEETUP_APP_KEY](#fleetup-app-key) - - [FLEETUP_USER_ID](#fleetup-user-id) - - [FLEETUP_API_ID](#fleetup-api-id) - - [FLEETUP_GROUP_ID](#fleetup-group-id) - -### CAPTCHA -To help prevent bots from registering and brute forcing the login. Get the reCaptcha keys from [here](https://www.google.com/recaptcha/intro/index.html) - - [CAPTCHA_ENABLED](#captcha_enabled) - - [RECAPTCHA_PUBLIC_KEY](#recaptcha_public_key) - - [RECAPTCHA_PRIVATE_KEY](#recaptcha_private_key) - - [NOCAPTCHA](#nocaptcha) - -# Description of Settings -## Django -### SECRET_KEY -A random string used in cryptographic functions, such as password hashing. Changing after installation will render all sessions and password reset tokens invalid. -### DEBUG -Replaces the generic `SERVER ERROR (500)` page when an error is encountered with a page containing a traceback and variables. May expose sensitive information so not recommended for production. -### ALLOWED_HOSTS -A list of addresses used to validate headers: AllianceAuth will block connection coming from any other address. This should be a list of URLs and IPs to allow. In most cases, just adding `'example.com'` is sufficient. This also accepts the `'*'` wildcard for testing purposes. -### DATABASES -List of databases available. Contains the Django database, and may include service ones if enabled. Service databases are defined in their individual sections and appended as needed automatically. -### LANGUAGE_CODE -Friendly name of the local language. -### TIME_ZONE -Friendly name of the local timezone. -### CAPTCHA_ENABLED -Enable Google reCaptcha -### RECAPTCHA_PUBLIC_KEY -Google reCaptcha public key -### RECAPTCHA_PRIVATE_KEY -Google reCaptcha private key -### NOCAPTCHA -Enable New No Captcha reCaptcha -### STATIC_URL -Absolute URL to serve static files from. -### STATIC_ROOT -Root folder to store static files in. -### SUPERUSER_STATE_BYPASS -Overrides superuser account states to always return True on membership tests. If issues are encountered, or you want to test access to certain portions of the site, set to False to respect true states of superusers. -## EMAIL SETTINGS -### DOMAIN -The URL to which emails will link. -### EMAIL_HOST -The host address of the email server. -### EMAIL_PORT -The host port of the email server. -### EMAIL_HOST_USER -The username to authenticate as on the email server. For GMail, this is the full address. -### EMAIL_HOST_PASSWORD -The password of the user used to authenticate on the email server. -### EMAIL_USE_TLS -Enable TLS connections to the email server. Default is True. -## Front Page Links -### KILLBOARD_URL -Link to a killboard. -### EXTERNAL_MEDIA_URL -Link to another media site, eg YouTube channel. -### FORUM_URL -Link to forums. Also used as the phpbb3 URL if enabled. -### SITE_NAME -Name to show in the top-left corner of auth. -## SSO Settings -An application will need to be created on the developers site. Please select `Authenticated API Access`, and choose all scopes starting with `esi`. -### ESI_SSO_CLIENT_ID -The application cliend ID generated from the [developers site.](https://developers.eveonline.com) -### ESI_SSO_CLIENT_SECRET -The application secret key generated from the [developers site.](https://developers.eveonline.com) -### ESI_SSO_CALLBACK_URL -The callback URL for authentication handshake. Should be `https://example.com/sso/callback`. -## Default Group Settings -### DEFAULT_AUTH_GROUP -Name of the group members of the owning corp or alliance are put in. -### DEFAULT_BLUE_GROUP -Name of the group blues of the owning corp or alliance are put in. -### MEMBER_CORP_GROUPS -If `True`, add members to groups with their corp name, prefixed with `Corp_` -### MEMBER_ALLIANCE_GROUPS -If `True`, add members to groups with their alliance name, prefixed with `Alliance_` -### BLUE_CORP_GROUPS -If `True`, add blues to groups with their corp name, prefixed with `Corp_` -### BLUE_ALLIANCE_GROUPS -If `True`, add blues to groups with their alliance name, prefixed with `Alliance_` -## Tenant Configuration -Characters of any corp or alliance with their ID here will be treated as a member. -### CORP_IDS -EVE corp IDs of member corps. Separate with a comma. -### ALLIANCE_IDS -EVE alliance IDs of member alliances. Separate with a comma. -## Standings Configuration -To allow blues to access auth, standings must be pulled from a corp-level API. This API needs access mask 16 (ContactList). -### CORP_API_ID -The ID of an API key for a corp from which to pull standings, if desired. Needed for blues to gain access. -### CORP_API_VCODE -The verification code of an API key for a corp from which to pull standings, if desired. Needed for blues to gain access. -### BLUE_STANDING -The minimum standing value to consider blue. Default is 5.0 -### STANDING_LEVEL -Standings from the API come at two levels: `corp` and `alliance`. Select which level to consider here. -## API Configuration -### MEMBER_API_MASK -Required access mask for members' API keys to be considered valid. -### MEMBER_API_ACCOUNT -If `True`, require API keys from members to be account-wide, not character-restricted. -### BLUE_API_MASK -Required access mask for blues' API keys to be considered valid. -### BLUE_API_ACCOUNT -If `True`, require API keys from blues to be account-wide, not character-restricted. -### REJECT_OLD_APIS -Require each submitted API be newer than the latest submitted API. Protects against recycled or stolen API keys. -### REJECT_OLD_APIS_MARGIN -Allows newly submitted APIs to have their ID this value lower than the highest API ID on record and still be accepted. Default is 50, 0 is safest. -## EVE Provider Settings -Data about EVE objects (characters, corps, alliances) can come from two sources: the XML API or the EVE Swagger Interface. -These settings define the default source. - -For most situations, the EVE Swagger Interface is best. But if it goes down or experiences issues, these can be reverted to the XML API. - -Accepted values are `esi` and `xml`. -### EVEONLINE_CHARACTER_PROVIDER -The default data source to get character information. Default is `esi` -### EVEONLINE_CORP_PROVIDER -The default data source to get corporation information. Default is `esi` -### EVEONLINE_ALLIANCE_PROVIDER -The default data source to get alliance information. Default is `esi` -### EVEONLINE_ITEMTYPE_PROVIDER -The default data source to get item type information. Default is `esi` -## Alliance Market -### MARKET_URL -The web address to access the Evernus Alliance Market application. -### MARKET_DB -The Evernus Alliance Market database connection information. -## HR Configuration -### API_KEY_AUDIT_URL -This setting defines what happens when someone clicks on an API key (such as in corpstats or an application). - -Default behaviour is to show the verification code in a popup, but this can be set to link out to a website. - -The URL set here uses python string formatting notation. Variable names are enclosed in `{}` brackets. Three variable names are available: `api_id`, `vcode`, and `pk` (which is the primary key of the API in the database - only useful on the admin site). - -Example URL structures are provided. Jacknife can be installed on your server following [its setup guide.](../services/jacknife.md) -## IPBoard3 Configuration -### IPBOARD_ENDPOINT -URL to the `index.php` file of a IPBoard install's API server. -### IPBOARD_APIKEY -API key for accessing an IPBoard install's API -### IPBOARD_APIMODULE -Module to access while using the API -## XenForo Configuration -### XENFORO_ENDPOINT -The address of the XenForo API. Should look like `https://example.com/forum/api.php` -### XENFORO_DEFAULT_GROUP -The group ID of the group to assign to member. Default is 0. -### XENFORO_APIKEY -The API key generated from XenForo to allow API access. -## Jabber Configuration -### JABBER_URL -Address to instruct members to connect their jabber clients to, in order to reach an Openfire install. Usually just `example.com` -### JABBER_PORT -Port to instruct members to connect their jabber clients to, in order to reach an Openfire install. Usually 5223. -### JABBER_SERVER -Server name of an Openfire install. Usually `example.com` -### OPENFIRE_ADDRESS -URL of the admin web interface for an Openfire install. Usually `http://example.com:9090`. If HTTPS is desired, change port to 9091: `https://example.com:9091` -### OPENFIRE_SECRET_KEY -Secret key used to authenticate with an Openfire admin interface. -### BROADCAST_USER -Openfire user account used to send broadcasts from. Default is `Broadcast`. -### BROADCAST_USER_PASSWORD -Password to use when authenticating as the `BROADCAST_USER` -### BROADCAST_SERVICE_NAME -Name of the broadcast service running on an Openfire install. Usually `broadcast` -## Mumble Configuration -### MUMBLE_URL -Address to instruct members to connect their Mumble clients to. -### MUMBLE_SERVER_ID -Depreciated. We're too scared to delete it. -## Teamspeak3 Configuration -### TEAMSPEAK3_SERVER_IP -IP of a Teamspeak3 server on which to manage users. Usually `127.0.0.1` -### TEAMSPEAK3_SERVER_PORT -Port on which to connect to a Teamspeak3 server at the `TEAMSPEAK3_SERVER_IP`. Usually `10011` -### TEAMSPEAK3_SERVERQUERY_USER -User account with which to authenticate on a Teamspeak3 server. Usually `serveradmin`. -### TEAMSPEAK3_SERVERQUERY_PASSWORD -Password to use when authenticating as the `TEAMSPEAK3_SERVERQUERY_USER`. Provided during first startup or when you define a custom serverquery user. -### TEAMSPEAK3_VIRTUAL_SERVER -ID of the server on which to manage users. Usually `1`. -### TEAMSPEAK3_PUBLIC_URL -Address to instruct members to connect their Teamspeak3 clients to. Usually `example.com` -## Discord Configuration -### DISCORD_GUILD_ID -The ID of a Discord server on which to manage users. -### DISCORD_BOT_TOKEN -The bot token obtained from defining a bot on the [Discord developers site.](https://discordapp.com/developers/applications/me) -### DISCORD_INVITE_CODE -A no-limit invite code required to add users to the server. Must be generated from the Discord server itself (instant invite). -### DISCORD_APP_ID -The application ID obtained from defining an application on the [Discord developers site.](https://discordapp.com/developers/applications/me) -### DISCORD_APP_SECRET -The application secret key obtained from defining an application on the [Discord developers site.](https://discordapp.com/developers/applications/me) -### DISCORD_CALLBACK_URL -The callback URL used for authenticaiton flow. Should be `https://example.com/discord_callback`. Must match exactly the one used when defining the application. -### DISCORD_SYNC_NAMES -Override usernames on the server to match the user's main character. -## Discourse Configuration -### DISCOURSE_URL -The web address of the Discourse server to direct users to. -### DISCOURSE_API_USERNAME -Username of the account which generated the API key on Discourse. -### DISCOURSE_API_KEY -API key defined on Discourse. -### DISCOURSE_SSO_SECRET -The SSO secret key defined on Discourse. -## IPS4 Configuration -### IPS4_URL -URL of the IPSuite4 install to direct users to. -### IPS4_API_KEY -Depreciated. We're too scared to delete it. -### IPS4_DB -The database connection to manage users on. -## SMF Configuration -### SMF_URL -URL of the SMF install to direct users to. -### SMF_DB -The database connection to manage users on. -## Fleet-Up Configuration -### FLEETUP_APP_KEY -Application key as [defined on Fleet-Up.](http://fleet-up.com/Api/MyApps) -### FLEETUP_USER_ID -API user ID as [defined on Fleet-Up.](http://fleet-up.com/Api/MyKeys) -### FLEETUP_API_ID -API ID as [defined on Fleet-Up.](http://fleet-up.com/Api/MyKeys) -### FLEETUP_GROUP_ID -The group ID from which to pull data. Can be [retrieved from Fleet-Up](http://fleet-up.com/Api/Endpoints#groups_mygroupmemberships) -## Logging Configuration -This section is used to manage how logging messages are processed. - -To turn off logging notifications, change the `handlers` `notifications` `class` to `logging.NullHandler` - -## Danger Zone -Everything below logging is magic. **Do not touch.** From 86e92941df0406e55fed12d09b10e74faeee568a Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 5 Oct 2017 14:35:25 -0400 Subject: [PATCH 13/16] Move default logging directory. --- allianceauth/project_template/log/.gitkeep | 0 allianceauth/project_template/project_name/log/.gitignore | 2 -- allianceauth/project_template/project_name/settings/base.py | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 allianceauth/project_template/log/.gitkeep delete mode 100644 allianceauth/project_template/project_name/log/.gitignore diff --git a/allianceauth/project_template/log/.gitkeep b/allianceauth/project_template/log/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/project_template/project_name/log/.gitignore b/allianceauth/project_template/project_name/log/.gitignore deleted file mode 100644 index 73edf032..00000000 --- a/allianceauth/project_template/project_name/log/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -!.gitignore -* \ No newline at end of file diff --git a/allianceauth/project_template/project_name/settings/base.py b/allianceauth/project_template/project_name/settings/base.py index ca5b1616..630121d8 100644 --- a/allianceauth/project_template/project_name/settings/base.py +++ b/allianceauth/project_template/project_name/settings/base.py @@ -172,7 +172,7 @@ ALLOWED_HOSTS = ['*'] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': str(os.path.join(PROJECT_DIR, 'alliance_auth.sqlite3')), + 'NAME': str(os.path.join(BASE_DIR, 'alliance_auth.sqlite3')), }, } @@ -228,7 +228,7 @@ LOGGING = { 'log_file': { 'level': 'INFO', # edit this line to change logging level to file 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(PROJECT_DIR, 'log/allianceauth.log'), + 'filename': os.path.join(BASE_DIR, 'log/allianceauth.log'), 'formatter': 'verbose', 'maxBytes': 1024 * 1024 * 5, # edit this line to change max log file size 'backupCount': 5, # edit this line to change number of log backups From 1c1dfde2c4da6016b2154aa4b21fa5331931c5b5 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 5 Oct 2017 15:06:34 -0400 Subject: [PATCH 14/16] Mention database migrations in update docs. --- docs/installation/auth/allianceauth.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/installation/auth/allianceauth.md b/docs/installation/auth/allianceauth.md index 3b9e10de..71e8c173 100644 --- a/docs/installation/auth/allianceauth.md +++ b/docs/installation/auth/allianceauth.md @@ -153,5 +153,7 @@ Once installed, move onto the [Gunicorn Guide](gunicorn.md) and decide on whethe Periodically [new releases](https://github.com/allianceauth/allianceauth/releases/) are issued with bug fixes and new features. To update your install, simply activate your virtual environment and update with `pip install --upgrade allianceauth`. Be sure to read the release notes which will highlight changes. Some releases come with changes to settings: update your project's settings with `allianceauth update /home/allianceserver/myauth`. + +Some releases come with new or changed models. Update your database to reflect this with `python /home/allianceserver/myauth/manage.py migrate`. Always restart celery and gunicorn after updating. \ No newline at end of file From 3f33485ca91b3e5dd37592430eeaf5be9aac61c1 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Tue, 10 Oct 2017 00:29:57 -0400 Subject: [PATCH 15/16] Template supervisor conf. Update docs to reflect simple installation. Ensure allianceauth celery workers can find config file for development (doesn't work as celeryapp.py). --- allianceauth/bin/allianceauth.py | 3 + allianceauth/celery.py | 16 ++++ .../{ => project_template}/celeryapp.py | 4 +- allianceauth/project_template/supervisor.conf | 36 +++++++++ docs/installation/auth/allianceauth.md | 49 +++++++++-- docs/installation/auth/gunicorn.md | 24 +++--- docs/installation/auth/index.md | 1 - docs/installation/auth/supervisor.md | 81 ------------------- 8 files changed, 111 insertions(+), 103 deletions(-) create mode 100644 allianceauth/celery.py rename allianceauth/{ => project_template}/celeryapp.py (77%) create mode 100644 allianceauth/project_template/supervisor.conf delete mode 100644 docs/installation/auth/supervisor.md diff --git a/allianceauth/bin/allianceauth.py b/allianceauth/bin/allianceauth.py index 6bf70cb1..7aed897f 100644 --- a/allianceauth/bin/allianceauth.py +++ b/allianceauth/bin/allianceauth.py @@ -1,5 +1,6 @@ #!/usr/bin/env python import os +import sys from optparse import OptionParser from django.core.management import ManagementUtility @@ -40,6 +41,8 @@ def create_project(parser, options, args): utility_args = ['django-admin.py', 'startproject', '--template=' + template_path, + '--pythonpath=' + '/'.join(sys.executable.split('/')[:-1]), + '--ext=conf', project_name] if dest_dir: diff --git a/allianceauth/celery.py b/allianceauth/celery.py new file mode 100644 index 00000000..63c652e0 --- /dev/null +++ b/allianceauth/celery.py @@ -0,0 +1,16 @@ +import os +from celery import Celery + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'allianceauth.project_template.project_name.settings.base') + +from django.conf import settings # noqa + +app = Celery('alliance_auth') + +# Using a string here means the worker don't have to serialize +# the configuration object to child processes. +app.config_from_object('django.conf:settings') + +# Load task modules from all registered Django app configs. +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/allianceauth/celeryapp.py b/allianceauth/project_template/celeryapp.py similarity index 77% rename from allianceauth/celeryapp.py rename to allianceauth/project_template/celeryapp.py index 49c2c809..dc9b4d39 100644 --- a/allianceauth/celeryapp.py +++ b/allianceauth/project_template/celeryapp.py @@ -2,11 +2,11 @@ import os from celery import Celery # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'alliance_auth.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ project_name }}.settings.local') from django.conf import settings # noqa -app = Celery('alliance_auth') +app = Celery('{{ project_name }}') # Using a string here means the worker don't have to serialize # the configuration object to child processes. diff --git a/allianceauth/project_template/supervisor.conf b/allianceauth/project_template/supervisor.conf new file mode 100644 index 00000000..228b48db --- /dev/null +++ b/allianceauth/project_template/supervisor.conf @@ -0,0 +1,36 @@ +[program:beat] +command={{ pythonpath}}/celery -A {{ project_name }} beat +directory={{ project_directory }} +user=allianceserver +stdout_logfile={{ project_directory }}/log/beat.log +stderr_logfile={{ project_directory }}/log/beat.log +autostart=true +autorestart=true +startsecs=10 +priority=998 + +[program:worker] +command={{ pythonpath}}/celery -A {{ project_name }} worker +directory={{ project_directory }} +user=allianceserver +numprocs=1 +stdout_logfile={{ project_directory }}/log/worker.log +stderr_logfile={{ project_directory }}/log/worker.log +autostart=true +autorestart=true +startsecs=10 +stopwaitsecs = 600 +killasgroup=true +priority=998 + +[program:gunicorn] +user = allianceserver +directory={{ project_directory }} +command={{ pythonpath}}/gunicorn {{ project_name}}.wsgi --workers=3 --timeout 120 +autostart=true +autorestart=true +stopsignal=INT + +[group:{{ project_name }}] +programs=beat,worker,gunicorn +priority=999 \ No newline at end of file diff --git a/docs/installation/auth/allianceauth.md b/docs/installation/auth/allianceauth.md index 71e8c173..9b4579ed 100644 --- a/docs/installation/auth/allianceauth.md +++ b/docs/installation/auth/allianceauth.md @@ -1,7 +1,7 @@ # Alliance Auth Installation ```eval_rst -.. important:: +.. tip:: Installation is easiest as the root user. Log in as root or a user with sudo powers. ``` @@ -132,9 +132,49 @@ And finally ensure the allianceserver user has read/write permissions to this di chown -R allianceserver:allianceserver /home/allianceserver/myauth +## Background Tasks + +### Gunicorn + +To run the auth website a [WSGI Server](https://www.fullstackpython.com/wsgi-servers.html) is required. [Gunicorn](http://gunicorn.org/) is highly recommended for its ease of configuring. Installation is simple: `pip install gunicorn`. It can be manually called with `gunicorn myauth.wsgi` or automatically run using supervisor. + +Additional information is available in the [gunicorn](gunicorn.md) doc. + +### Supervisor + +[Supervisor](http://supervisord.org/) is a process watchdog service: it makes sure other processes are started automatically and kept running. It can be used to automatically start the WSGI server and celery workers for background tasks. Installation varies by OS: + +Ubuntu: + + apt-get install supervisor + +CentOS: + + yum install supervisor + systemctl enable supervisord.service + systemctl start supervisord.service + +Once installed it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the celery workers, celery task scheduler and gunicorn are all running. + + ln /home/allianceserver/myauth/supervisor.conf /etc/supervisor/conf.d/myauth.conf + supervisorctl reload + +You can check the status of the processes with `supervisorctl status`. Logs from these processes are available in `/home/allianceserver/myauth/log` named by process. + +```eval_rst +.. note:: + Any time the code or your settings change you'll need to restart gunicorn and celery. :: + + supervisorctl restart myauth: +``` + +## Webserver + +Once installed, decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md) and follow the respective guide. + ## Superuser -Before proceeding it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account. +Before using your auth site it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account. python /home/allianceserver/myauth/manage.py createsuperuser @@ -143,11 +183,6 @@ Before proceeding it is essential to create a superuser account. This account wi Be sure to add a main character to this account before attempting to activate services with it. ``` -## Webserver - -Once installed, move onto the [Gunicorn Guide](gunicorn.md) and decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md). You will also need to install [supervisor](supervisor.md) to run the background tasks. - - ## Updating Periodically [new releases](https://github.com/allianceauth/allianceauth/releases/) are issued with bug fixes and new features. To update your install, simply activate your virtual environment and update with `pip install --upgrade allianceauth`. Be sure to read the release notes which will highlight changes. diff --git a/docs/installation/auth/gunicorn.md b/docs/installation/auth/gunicorn.md index 60a6cdae..573ef0f4 100644 --- a/docs/installation/auth/gunicorn.md +++ b/docs/installation/auth/gunicorn.md @@ -15,30 +15,30 @@ Check out the full [Gunicorn docs](http://docs.gunicorn.org/en/latest/index.html Install Gunicorn using pip, `pip install gunicorn`. -In your `allianceauth` base directory, try running `gunicorn --bind 0.0.0.0:8000 alliance_auth.wsgi`. You should be able to browse to http://yourserver:8000 and see your Alliance Auth installation running. Images and styling will be missing, but dont worry, your web server will provide them. +In your `myauth` base directory, try running `gunicorn --bind 0.0.0.0:8000 myauth.wsgi`. You should be able to browse to http://yourserver:8000 and see your Alliance Auth installation running. Images and styling will be missing, but dont worry, your web server will provide them. Once you validate its running, you can kill the process with Ctrl+C and continue. ## Running Gunicorn with Supervisor -You should use [Supervisor](supervisor.md) to keep all of Alliance Auth components running (instead of using screen). You don't _have to_ but we will be using it to start and run Gunicorn so you might as well. +You should use [Supervisor](allianceauth.md#supervisor) to keep all of Alliance Auth components running (instead of using screen). You don't _have to_ but we will be using it to start and run Gunicorn so you might as well. ### Sample Supervisor config -You'll want to edit `/etc/supervisor/conf.d/aauth_gunicorn.conf` (or whatever you want to call the config file) +You'll want to edit `/etc/supervisor/conf.d/myauth_gunicorn.conf` (or whatever you want to call the config file) ``` -[program:aauth-gunicorn] -user = www-data -directory=/home/allianceserver/allianceauth/ -command=gunicorn alliance_auth.wsgi --workers=3 --timeout 120 +[program:myauth-gunicorn] +user = allianceserver +directory=/home/allianceserver/myauth/ +command=gunicorn myauth.wsgi --workers=3 --timeout 120 autostart=true autorestart=true stopsignal=INT ``` -- `[program:aauth-gunicorn]` - Change aauth-gunicorn to whatever you wish to call your process in Supervisor. -- `user = www-data` - Change to whatever user you wish Gunicorn to run as. You could even set this as allianceserver if you wished. I'll leave the question security of that up to you. -- `directory=/home/allianceserver/allianceauth/` - Needs to be the path to your Alliance Auth install. -- `command=gunicorn alliance_auth.wsgi --workers=3 --timeout 120` - Running Gunicorn and the options to launch with. This is where you have some decisions to make, we'll continue below. +- `[program:myauth-gunicorn]` - Change myauth-gunicorn to whatever you wish to call your process in Supervisor. +- `user = allianceserver` - Change to whatever user you wish Gunicorn to run as. You could even set this as allianceserver if you wished. I'll leave the question security of that up to you. +- `directory=/home/allianceserver/myauth/` - Needs to be the path to your Alliance Auth project. +- `command=gunicorn myauth.wsgi --workers=3 --timeout 120` - Running Gunicorn and the options to launch with. This is where you have some decisions to make, we'll continue below. #### Gunicorn Arguments @@ -118,4 +118,4 @@ Any web server capable of proxy passing should be able to sit in front of Gunico ## Restarting Gunicorn In the past when you made changes you restarted the entire Apache server. This is no longer required. When you update or make configuration changes that ask you to restart Apache, instead you can just restart Gunicorn: -`sudo supervisorctl restart aauth-gunicorn`, or the service name you chose for it. +`sudo supervisorctl restart myauth-gunicorn`, or the service name you chose for it. diff --git a/docs/installation/auth/index.md b/docs/installation/auth/index.md index d245828b..0a64cb11 100644 --- a/docs/installation/auth/index.md +++ b/docs/installation/auth/index.md @@ -7,5 +7,4 @@ gunicorn nginx apache - supervisor ``` diff --git a/docs/installation/auth/supervisor.md b/docs/installation/auth/supervisor.md deleted file mode 100644 index 276dfac0..00000000 --- a/docs/installation/auth/supervisor.md +++ /dev/null @@ -1,81 +0,0 @@ -# Supervisor - ->Supervisor is a client/server system that allows its users to control a number of processes on UNIX-like operating systems. - -What that means is supervisor will take care of ensuring the celery workers are running (and mumble authenticator) and start the automatically on reboot. Handy, eh? - -## Installation - -Most OSes have a supervisor package available in their distribution. - -Ubuntu: - - sudo apt-get install supervisor - -CentOS: - - sudo yum install supervisor - sudo systemctl enable supervisord.service - sudo systemctl start supervisord.service - -## Configuration - -Auth provides example config files for the celery workers, the periodic task scheduler (celery beat), and the mumble authenticator. All of these are available in `thirdparty/Supervisor`. - -For most users, all you have to do is copy the config files to `/etc/supervisor/conf.d` then restart the service. Copy `auth.conf` for the celery workers, and `auth-mumble.conf` for the mumble authenticator. For all three just use a wildcard: - - sudo cp thirdparty/Supervisor/* /etc/supervisor/conf.d - -Ubuntu: - - sudo service supervisor restart - -CentOS: - - sudo systemctl restart supervisor.service - -## Checking Status - -To ensure the processes are working, check their status: - - sudo supervisorctl status - -Processes will be `STARTING`, `RUNNING`, or `ERROR`. If an error has occurred, check their log files: - - celery workers: `log/worker.log` - - celery beat: `log/beat.log` - - authenticator: `log/authenticator.log` - -## Restarting Processes - -To restart the celery group: - - sudo supervisorctl restart auth:* - -To restart just celerybeat: - - sudo supervisorctl restart auth:celerybeat - -To restart just celeryd: - - sudo supervisorctl restart auth:celeryd - -To restart just mumble authenticator: - - sudo supervisorctl restart auth-mumble - -## Customizing Config Files - -The only real customization needed is if running in a virtual environment. The python path will have to be changed in order to start in the venv. - -Edit the config files and find the line saying `command`. Replace `python` with `/path/to/venv/bin/python`. For Celery replace `celery` with `/path/to/venv/bin/celery`. This can be relative to the `directory` specified in the config file. - -Note that for config changes to be loaded, the supervisor service must be restarted. - -## Troubleshooting - -### auth-celerybeat fails to start -Most often this is caused by a permissions issue on the allianceauth directory (the error will talk about `celerybeat.pid`). The easiest fix is to edit its config file and change the `user` from `allianceserver` to `root`. - -### Workers are using old settings - -Every time the codebase is updated or settings file changed, workers will have to be restarted. Easiest way is to restart the supervisor service (see configuration above for commands) From cfad4fa8a650c784fb042344e998d87ed6ee1ad3 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Tue, 10 Oct 2017 10:41:25 -0400 Subject: [PATCH 16/16] Pycharm refactor failed me. --- allianceauth/authentication/tasks.py | 2 +- allianceauth/corputils/tasks.py | 2 +- allianceauth/eveonline/tasks.py | 2 +- allianceauth/services/modules/discord/tasks.py | 2 +- allianceauth/services/modules/discourse/tasks.py | 2 +- allianceauth/services/modules/mumble/tasks.py | 2 +- allianceauth/services/modules/openfire/tasks.py | 2 +- allianceauth/services/modules/phpbb3/tasks.py | 2 +- allianceauth/services/modules/seat/tasks.py | 2 +- allianceauth/services/modules/smf/tasks.py | 2 +- allianceauth/services/modules/teamspeak3/tasks.py | 2 +- allianceauth/services/tasks.py | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/allianceauth/authentication/tasks.py b/allianceauth/authentication/tasks.py index d495e4c9..67620481 100644 --- a/allianceauth/authentication/tasks.py +++ b/allianceauth/authentication/tasks.py @@ -4,7 +4,7 @@ from esi.errors import TokenExpiredError, TokenInvalidError from esi.models import Token from allianceauth.authentication.models import CharacterOwnership -from allianceauth.celeryapp import app +from allianceauth.celery import app logger = logging.getLogger(__name__) diff --git a/allianceauth/corputils/tasks.py b/allianceauth/corputils/tasks.py index 584f930e..d78ac3d2 100644 --- a/allianceauth/corputils/tasks.py +++ b/allianceauth/corputils/tasks.py @@ -1,4 +1,4 @@ -from allianceauth.celeryapp import app +from allianceauth.celery import app from allianceauth.corputils import CorpStats diff --git a/allianceauth/eveonline/tasks.py b/allianceauth/eveonline/tasks.py index 399b9f78..115723f4 100644 --- a/allianceauth/eveonline/tasks.py +++ b/allianceauth/eveonline/tasks.py @@ -1,6 +1,6 @@ import logging -from allianceauth.celeryapp import app +from allianceauth.celery import app from .models import EveAllianceInfo from .models import EveCharacter from .models import EveCorporationInfo diff --git a/allianceauth/services/modules/discord/tasks.py b/allianceauth/services/modules/discord/tasks.py index f0076d25..c53d048c 100644 --- a/allianceauth/services/modules/discord/tasks.py +++ b/allianceauth/services/modules/discord/tasks.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from allianceauth.notifications import notify -from allianceauth.celeryapp import app +from allianceauth.celery import app from .manager import DiscordOAuthManager, DiscordApiBackoff from .models import DiscordUser diff --git a/allianceauth/services/modules/discourse/tasks.py b/allianceauth/services/modules/discourse/tasks.py index bdfa14ce..898dc27c 100644 --- a/allianceauth/services/modules/discourse/tasks.py +++ b/allianceauth/services/modules/discourse/tasks.py @@ -3,7 +3,7 @@ import logging from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from allianceauth.celeryapp import app +from allianceauth.celery import app from allianceauth.notifications import notify from .manager import DiscourseManager from .models import DiscourseUser diff --git a/allianceauth/services/modules/mumble/tasks.py b/allianceauth/services/modules/mumble/tasks.py index 7a665322..9fc5c65b 100644 --- a/allianceauth/services/modules/mumble/tasks.py +++ b/allianceauth/services/modules/mumble/tasks.py @@ -3,7 +3,7 @@ import logging from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from allianceauth.celeryapp import app +from allianceauth.celery import app from .manager import MumbleManager from .models import MumbleUser diff --git a/allianceauth/services/modules/openfire/tasks.py b/allianceauth/services/modules/openfire/tasks.py index acda91d3..b39c9e97 100644 --- a/allianceauth/services/modules/openfire/tasks.py +++ b/allianceauth/services/modules/openfire/tasks.py @@ -4,7 +4,7 @@ from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from allianceauth.notifications import notify -from allianceauth.celeryapp import app +from allianceauth.celery import app from allianceauth.services.modules.openfire.manager import OpenfireManager from .models import OpenfireUser diff --git a/allianceauth/services/modules/phpbb3/tasks.py b/allianceauth/services/modules/phpbb3/tasks.py index ee882c9a..b0d963f2 100644 --- a/allianceauth/services/modules/phpbb3/tasks.py +++ b/allianceauth/services/modules/phpbb3/tasks.py @@ -3,7 +3,7 @@ import logging from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from allianceauth.celeryapp import app +from allianceauth.celery import app from allianceauth.notifications import notify from .manager import Phpbb3Manager from .models import Phpbb3User diff --git a/allianceauth/services/modules/seat/tasks.py b/allianceauth/services/modules/seat/tasks.py index fe1390bb..185e2015 100644 --- a/allianceauth/services/modules/seat/tasks.py +++ b/allianceauth/services/modules/seat/tasks.py @@ -3,7 +3,7 @@ import logging from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from allianceauth.celeryapp import app +from allianceauth.celery import app from allianceauth.notifications import notify from .manager import SeatManager from .models import SeatUser diff --git a/allianceauth/services/modules/smf/tasks.py b/allianceauth/services/modules/smf/tasks.py index a164a3e4..be445e3a 100644 --- a/allianceauth/services/modules/smf/tasks.py +++ b/allianceauth/services/modules/smf/tasks.py @@ -3,7 +3,7 @@ import logging from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from allianceauth.celeryapp import app +from allianceauth.celery import app from allianceauth.notifications import notify from .manager import SmfManager from .models import SmfUser diff --git a/allianceauth/services/modules/teamspeak3/tasks.py b/allianceauth/services/modules/teamspeak3/tasks.py index 7528377a..7a0c2fab 100644 --- a/allianceauth/services/modules/teamspeak3/tasks.py +++ b/allianceauth/services/modules/teamspeak3/tasks.py @@ -3,7 +3,7 @@ import logging from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from allianceauth.celeryapp import app +from allianceauth.celery import app from allianceauth.notifications import notify from .manager import Teamspeak3Manager from .models import AuthTS, TSgroup, UserTSgroup, Teamspeak3User diff --git a/allianceauth/services/tasks.py b/allianceauth/services/tasks.py index 5ec0fcc8..19e4bc55 100644 --- a/allianceauth/services/tasks.py +++ b/allianceauth/services/tasks.py @@ -2,7 +2,7 @@ import logging import redis -from allianceauth.celeryapp import app +from allianceauth.celery import app from .hooks import ServicesHook REDIS_CLIENT = redis.Redis()