diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..f2a5aece --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +# .dockerignore + +# Ignore Python bytecode files +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Ignore virtual environment directories +venv/ +*.virtualenv/ +.env/ + +# Ignore Django migration files +*/migrations/*.pyc +*/migrations/__pycache__/ + +# Ignore logs +logs/ +*.log + +# Ignore configuration files +*.ini + +# Ignore user-specific files (e.g., editor settings) +*.swp +*.swo +*.swn +*.bak +*.tmp +*.sublime* +*.vscode/ + +# Ignore local media files +media/ + +# Ignore local database files (SQLite) +*.sqlite3 +*.sqlite3-journal + +# Ignore test coverage reports +.coverage +htmlcov/ + +# Ignore build artifacts and distribution files +build/ +dist/ +*.egg-info/ +*.egg +*.wheel diff --git a/.env.sample b/.env.sample new file mode 100644 index 00000000..b2c9da52 --- /dev/null +++ b/.env.sample @@ -0,0 +1,24 @@ +DEBUG=TRUE +POSTGRES_USER=postgres +POSTGRES_PASSWORD=junction +POSTGRES_DB=junction +HOST_NAME=db +DB_PORT=5432 +BROKER_URL=redis://redis:6379/0 +CELERY_RESULT_BACKEND=redis://redis:6379/0 +SITE_NAME=junction +SERVER_PORT=8888 +GOOGLE_ANALYTICS_ID=google_analytics_id +FACEBOOK_APP_ID=fb_app_id +EMAIL_HOST_USER=email_host_user +EMAIL_HOST_PASSWORD=email_host_pass +SECRET_KEY=secret_key +GITHUB_CLIENT_ID=github_client_id +GITHUB_CLIENT_SECRET=github_client_secret +GOOGLE_CLIENT_ID=google_oauth_client_id +GOOGLE_CLIENT_SECRET=google_oauth_client_secret +TWITTER_CONSUMER_KEY=twitter_consume_key +TWITTER_CONSUMER_SECRET=twitter_consume_secret +TWITTER_ACCESS_TOKEN_KEY=twitter_access_token +TWITTER_ACCESS_TOKEN_SECRET=twitter_access_token_secret +USE_ASYNC_FOR_EMAIL=boolean diff --git a/.gitignore b/.gitignore index 740ade66..2484b3fe 100644 --- a/.gitignore +++ b/.gitignore @@ -90,4 +90,7 @@ qr_files/ #VSCode .vscode/ -tmp/ \ No newline at end of file +tmp/ + +# Env +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..42e6a179 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.10-slim-buster + +WORKDIR /code + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc \ + postgresql-client \ + build-essential \ + nodejs \ + npm \ + libpq-dev && \ + rm -rf /var/lib/apt/lists/* + +COPY requirements.txt /code/ +RUN pip install --no-cache-dir -r requirements.txt + +# Install requirements for running tests +COPY ./tools/requirements-test.txt /code/ +RUN pip install --no-cache-dir -r requirements-test.txt + +RUN npm install -g yarn +RUN npm install -g grunt-cli + +COPY . /code/ + +RUN chmod +x bin/install-static.sh +RUN bin/install-static.sh +# not getting used at this moment +RUN chmod +x bin/wait-for-it.sh + +ENV PYTHONUNBUFFERED=1 diff --git a/bin/install-static.sh b/bin/install-static.sh new file mode 100644 index 00000000..1294e028 --- /dev/null +++ b/bin/install-static.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd junction/static +yarn install +grunt less +cd ../.. diff --git a/bin/wait-for-it.sh b/bin/wait-for-it.sh new file mode 100755 index 00000000..cd9e315f --- /dev/null +++ b/bin/wait-for-it.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# wait-for-it.sh: Wait for a service to be ready. + +set -e + +host="$1" +port="$2" +shift 2 +cmd="$@" + +until PGPASSWORD="$POSTGRES_PASSWORD" psql -h "$host" -U "$POSTGRES_USER" -c '\q'; do + >&2 echo "Postgres is unavailable - sleeping" + sleep 1 +done + +>&2 echo "Postgres is up - executing command" +exec $cmd diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..489d84f2 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,41 @@ +version: '3.8' + +services: + db: + image: postgres:15-alpine + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data/ + env_file: + - .env + + redis: + image: redis:latest + ports: + - "6379:6379" + + web: + image: ananyo2012/junction:1.1 + volumes: + - .:/code + ports: + - "${SERVER_PORT}:${SERVER_PORT}" + depends_on: + - db + env_file: + - .env + command: sh -c 'python manage.py migrate && python manage.py collectstatic --noinput --clear && gunicorn -c gunicorn.conf.py' + + celery: + image: ananyo2012/junction:1.1 + depends_on: + - db + - redis + - web + env_file: + - .env + command: sh -c 'celery -A junction worker -l info -E' + +volumes: + postgres_data: diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 00000000..59c0f48c --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,8 @@ +version: '3.8' + +services: + test: + build: + context: . + dockerfile: Dockerfile + command: sh -c pytest --cov=unit --cov=integrations --cov-report=html -v diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..efe055ff --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +version: '3.8' + +services: + db: + image: postgres:15-alpine + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data/ + env_file: + - .env + + redis: + image: redis:latest + ports: + - "6379:6379" + + web: + build: + context: . + dockerfile: Dockerfile + image: junction_local + volumes: + - .:/code + ports: + - "${SERVER_PORT}:${SERVER_PORT}" + depends_on: + - db + env_file: + - .env + command: sh -c 'python manage.py migrate && python manage.py runsslserver 0.0.0.0:${SERVER_PORT}' + + celery: + image: junction_local + depends_on: + - db + - redis + - web + env_file: + - .env + command: sh -c 'celery -A junction worker -l info -E' + +volumes: + postgres_data: diff --git a/gunicorn.conf.py b/gunicorn.conf.py index a8b6b91f..5e469558 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -1,4 +1,7 @@ +import os +port = os.environ.get("SERVER_PORT", "8888") + wsgi_app = "wsgi" -bind = "0.0.0.0:8001" +bind = f"0.0.0.0:{port}" workers = 2 loglevel = "debug" diff --git a/junction/conferences/models.py b/junction/conferences/models.py index 91e8cc8a..a104cd2d 100644 --- a/junction/conferences/models.py +++ b/junction/conferences/models.py @@ -7,7 +7,7 @@ from django.db import models from six import python_2_unicode_compatible from django.utils.timezone import now -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django_extensions.db.fields import AutoSlugField from simple_history.models import HistoricalRecords from slugify import slugify diff --git a/junction/conferences/urls.py b/junction/conferences/urls.py index e4f048e2..158ce1fb 100644 --- a/junction/conferences/urls.py +++ b/junction/conferences/urls.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from django.conf.urls import url +from django.urls import re_path from . import views -urlpatterns = [url(r"^$", views.get_conference, name="get-conference")] +urlpatterns = [re_path(r"^$", views.get_conference, name="get-conference")] diff --git a/junction/profiles/urls.py b/junction/profiles/urls.py index f7f1ab7e..feb0135b 100644 --- a/junction/profiles/urls.py +++ b/junction/profiles/urls.py @@ -1,10 +1,10 @@ -from django.conf.urls import url +from django.urls import re_path from . import views app_name = "junction.profiles" urlpatterns = [ - url(r"^$", views.dashboard, name="dashboard"), - url(r"^edit/$", views.profile, name="profile"), + re_path(r"^$", views.dashboard, name="dashboard"), + re_path(r"^edit/$", views.profile, name="profile"), ] diff --git a/junction/proposals/urls.py b/junction/proposals/urls.py index 4b5ab6fa..80004598 100644 --- a/junction/proposals/urls.py +++ b/junction/proposals/urls.py @@ -2,31 +2,32 @@ from __future__ import absolute_import, unicode_literals from django.conf.urls import include, url +from django.urls import re_path from . import comments_views, dashboard, views, votes_views comment_urls = [ - url( + re_path( r"^(?P[\w-]+)/create/$", comments_views.create_proposal_comment, name="proposal-comment-create", ), - url( + re_path( r"^(?P[\w-]+)/comments/(?P\d+)/up-vote/$", votes_views.proposal_comment_up_vote, name="proposal-comment-up-vote", ), - url( + re_path( r"^(?P[\w-]+)/comments/(?P\d+)/down-vote/$", votes_views.proposal_comment_down_vote, name="proposal-comment-down-vote", ), - url( + re_path( r"^(?P[\w-]+)/comments/(?P\d+)/mark_spam/$", comments_views.mark_comment_as_spam, name="comment_mark_spam", ), - url( + re_path( r"^(?P[\w-]+)/comments/(?P\d+)/unmark_spam/$", comments_views.unmark_comment_as_spam, name="comment_unmark_spam", @@ -35,56 +36,56 @@ urlpatterns = [ # proposal urls - url(r"^$", views.list_proposals, name="proposals-list"), - url(r"^create/$", views.create_proposal, name="proposal-create"), - url(r"^to_review/$", views.proposals_to_review, name="proposals-to-review"), - url( + re_path(r"^$", views.list_proposals, name="proposals-list"), + re_path(r"^create/$", views.create_proposal, name="proposal-create"), + re_path(r"^to_review/$", views.proposals_to_review, name="proposals-to-review"), + re_path( r"^second_phase_voting/$", dashboard.second_phase_voting, name="second-phase-voting", ), - url(r"^(?P[\w-]+)/$", views.detail_proposal, name="proposal-detail"), - url( + re_path(r"^(?P[\w-]+)/$", views.detail_proposal, name="proposal-detail"), + re_path( r"^(?P[\w-]+)~(?P.*)/$", views.detail_proposal, name="proposal-detail", ), - url(r"^(?P[\w-]+)/delete/$", views.delete_proposal, name="proposal-delete"), - url(r"^(?P[\w-]+)/update/$", views.update_proposal, name="proposal-update"), - url( + re_path(r"^(?P[\w-]+)/delete/$", views.delete_proposal, name="proposal-delete"), + re_path(r"^(?P[\w-]+)/update/$", views.update_proposal, name="proposal-update"), + re_path( r"^(?P[\w-]+)/upload-content/$", views.proposal_upload_content, name="proposal-upload-content", ), - url( + re_path( r"^(?P[\w-]+)/change-proposal-review-state/$", views.review_proposal, name="proposal-review", ), # comment urls - url(r"^comment/", include(comment_urls)), + re_path(r"^comment/", include(comment_urls)), # Voting - url( + re_path( r"^(?P[\w-]+)/down-vote/$", votes_views.proposal_vote_down, name="proposal-vote-down", ), - url( + re_path( r"^(?P[\w-]+)/up-vote/$", votes_views.proposal_vote_up, name="proposal-vote-up", ), - url( + re_path( r"^(?P[\w-]+)/remove-vote/$", votes_views.proposal_vote_remove, name="proposal-vote-remove", ), - url( + re_path( r"^(?P[\w-]+)/vote/$", votes_views.proposal_reviewer_vote, name="proposal-reviewer-vote", ), - url( + re_path( r"^(?P[\w-]+)/second-vote/$", votes_views.proposal_reviewer_secondary_vote, name="proposal-reviewer-secondary-vote", diff --git a/junction/schedule/urls.py b/junction/schedule/urls.py index 520bf150..34589b78 100644 --- a/junction/schedule/urls.py +++ b/junction/schedule/urls.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals -from django.conf.urls import url +from django.urls import re_path from . import views urlpatterns = [ # schedule urls - url(r"^dummy_schedule/$", views.dummy_schedule, name="dummy_schedule"), + re_path(r"^dummy_schedule/$", views.dummy_schedule, name="dummy_schedule"), ] diff --git a/junction/tickets/urls.py b/junction/tickets/urls.py index 85bd246c..139d8c45 100644 --- a/junction/tickets/urls.py +++ b/junction/tickets/urls.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals -from django.conf.urls import url +from django.urls import re_path from . import views urlpatterns = [ - url(r"^sync_data/$", views.sync_data, name="sync_data"), + re_path(r"^sync_data/$", views.sync_data, name="sync_data"), ] diff --git a/junction/urls.py b/junction/urls.py index 614305c6..c60746eb 100644 --- a/junction/urls.py +++ b/junction/urls.py @@ -4,6 +4,7 @@ import django.views.defaults from django.conf import settings from django.conf.urls import include, url +from django.urls import re_path from django.conf.urls.static import static from django.contrib import admin from django.views.generic.base import RedirectView, TemplateView @@ -42,108 +43,108 @@ urlpatterns = [ - url(r"^$", HomePageView.as_view(), name="page-home"), + re_path(r"^$", HomePageView.as_view(), name="page-home"), # Django Admin - url(r"^nimda/", admin.site.urls), + re_path(r"^nimda/", admin.site.urls), # Third Party Stuff - url(r"^accounts/", include("allauth.urls")), + re_path(r"^accounts/", include("allauth.urls")), # Tickets - url(r"^tickets/", include("junction.tickets.urls")), - url( + re_path(r"^tickets/", include("junction.tickets.urls")), + re_path( r"^feedback/(?P\d+)/$", view_feedback, name="feedback-detail" ), - url( + re_path( r"^schedule_item/(?P\d+)/$", non_proposal_schedule_item_view, name="schedule-item", ), - url(r"^api/v1/", include(router.urls)), + re_path(r"^api/v1/", include(router.urls)), # Device - url(r"^api/v1/devices/$", DeviceListApiView.as_view(), name="device-list"), - url( + re_path(r"^api/v1/devices/$", DeviceListApiView.as_view(), name="device-list"), + re_path( r"^api/v1/devices/(?P<_uuid>[\w-]+)/$", DeviceDetailApiView.as_view(), name="device-detail", ), # Feedback - url( + re_path( "^api/v1/feedback_questions/$", FeedbackQuestionListApiView.as_view(), name="feedback-questions-list", ), - url("^api/v1/feedback/$", FeedbackListApiView.as_view(), name="feedback-list"), + re_path("^api/v1/feedback/$", FeedbackListApiView.as_view(), name="feedback-list"), # User Dashboard - url(r"^profiles/", include("junction.profiles.urls", namespace="profiles")), + re_path(r"^profiles/", include("junction.profiles.urls", namespace="profiles")), # Static Pages. TODO: to be refactored - url( + re_path( r"^speakers/$", TemplateView.as_view(template_name="static-content/speakers.html",), name="speakers-static", ), - url( + re_path( r"^schedule/$", TemplateView.as_view(template_name="static-content/schedule.html",), name="schedule-static", ), - url( + re_path( r"^venue/$", TemplateView.as_view(template_name="static-content/venue.html",), name="venue-static", ), - url( + re_path( r"^sponsors/$", TemplateView.as_view(template_name="static-content/sponsors.html",), name="sponsors-static", ), - url( + re_path( r"^blog/$", TemplateView.as_view(template_name="static-content/blog-archive.html",), name="blog-archive", ), - url( + re_path( r"^coc/$", TemplateView.as_view(template_name="static-content/coc.html",), name="coc-static", ), - url( + re_path( r"^faq/$", TemplateView.as_view(template_name="static-content/faq.html",), name="faq-static", ), # Conference Pages - url(r"^(?P[\w-]+)/", include("junction.conferences.urls")), + re_path(r"^(?P[\w-]+)/", include("junction.conferences.urls")), # Proposals related - url(r"^(?P[\w-]+)/proposals/", include("junction.proposals.urls")), - url( + re_path(r"^(?P[\w-]+)/proposals/", include("junction.proposals.urls")), + re_path( r"^(?P[\w-]+)/dashboard/reviewers/", junction.proposals.dashboard.reviewer_comments_dashboard, name="proposal-reviewers-dashboard", ), - url( + re_path( r"^(?P[\w-]+)/dashboard/proposal_state/$", junction.proposals.dashboard.proposal_state, name="proposal-state", ), - url( + re_path( r"^(?P[\w-]+)/dashboard/$", junction.proposals.dashboard.proposals_dashboard, name="proposal-dashboard", ), - url( + re_path( r"^(?P[\w-]+)/dashboard/votes/$", junction.proposals.dashboard.reviewer_votes_dashboard, name="proposal-reviewer-votes-dashboard", ), - url( + re_path( r"^(?P[\w-]+)/dashboard/votes/export/$", junction.proposals.dashboard.export_reviewer_votes, name="export-reviewer-votes", ), # Schedule related - url(r"^(?P[\w-]+)/schedule/", include("junction.schedule.urls")), + re_path(r"^(?P[\w-]+)/schedule/", include("junction.schedule.urls")), # Proposals as conference home page. TODO: Needs to be enhanced - url( + re_path( r"^(?P[\w-]+)/", RedirectView.as_view(pattern_name="proposals-list"), name="conference-detail", @@ -153,8 +154,8 @@ if settings.DEBUG: urlpatterns += [ - url(r"^400/$", django.views.defaults.bad_request), # noqa - url(r"^403/$", django.views.defaults.permission_denied), - url(r"^404/$", django.views.defaults.page_not_found), - url(r"^500/$", django.views.defaults.server_error), + re_path(r"^400/$", django.views.defaults.bad_request), # noqa + re_path(r"^403/$", django.views.defaults.permission_denied), + re_path(r"^404/$", django.views.defaults.page_not_found), + re_path(r"^500/$", django.views.defaults.server_error), ] diff --git a/settings/common.py b/settings/common.py index 97265cff..1f6945b3 100644 --- a/settings/common.py +++ b/settings/common.py @@ -5,10 +5,12 @@ import os from os.path import dirname, join -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ +DEBUG = False +if os.environ.get("DEBUG", False) == "TRUE": + DEBUG = True -DEBUG = True # Build paths inside the project like this: os.path.join(ROOT_DIR, ...) ROOT_DIR = dirname(dirname(__file__)) APP_DIR = join(ROOT_DIR, "junction") @@ -57,7 +59,7 @@ "django.contrib.staticfiles", "django.contrib.sites", ) - + THIRD_PARTY_APPS = ( "allauth", "allauth.account", @@ -68,12 +70,15 @@ "pagedown", "markdown_deux", "django_bootstrap_breadcrumbs", + "django_extensions", "rest_framework", "django_filters", "simple_history", - #"sslserver", used in development server only ) +if DEBUG: + THIRD_PARTY_APPS += ("sslserver",) + OUR_APPS = ( "junction.base", "junction.conferences", @@ -86,7 +91,7 @@ ) INSTALLED_APPS = CORE_APPS + THIRD_PARTY_APPS + OUR_APPS -SITE_ID = 1 +SITE_ID = 1 # Provider specific settings SOCIALACCOUNT_PROVIDERS = { @@ -96,8 +101,8 @@ # interpreted as verified. 'VERIFIED_EMAIL': True, 'APP': { - 'client_id': 'enter your github client_id', - 'secret': 'enter your github secret key', + 'client_id': os.environ.get("GITHUB_CLIENT_ID", ""), + 'secret': os.environ.get("GITHUB_CLIENT_SECRET", ""), 'key': '', }, }, @@ -107,11 +112,11 @@ # credentials, or list them here: 'SCOPE': { 'profile', - 'email', + 'email', }, 'APP': { - 'client_id': 'enter your google client_id', - 'secret': 'enter your google secret key', + 'client_id': os.environ.get("GOOGLE_CLIENT_ID", ""), + 'secret': os.environ.get("GOOGLE_CLIENT_SECRET", ""), 'key': '', }, } @@ -157,7 +162,8 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_HOST = "smtp.gmail.com" EMAIL_HOST_USER = DEFAULT_FROM_EMAIL = (os.environ.get("EMAIL_HOST_USER", "enter gmail id"),) -EMAIL_HOST_PASSWORD = (os.environ.get("EMAIL_HOST_PASSWORD", "enter App password"),) #turn on 2-step verification in your gmail account and add App password +# turn on 2-step verification in your gmail account and add App password +EMAIL_HOST_PASSWORD = (os.environ.get("EMAIL_HOST_PASSWORD", "enter App password"),) EMAIL_PORT = 587 EMAIL_USE_TLS = True # DEFAULT_FROM_EMAIL = SITE_VARIABLES["site_name"] + " " @@ -176,7 +182,7 @@ "filters": ["require_debug_false"], "class": "django.utils.log.AdminEmailHandler", }, - "console": {"level": "DEBUG", "class": "logging.StreamHandler",}, + "console": {"level": "DEBUG", "class": "logging.StreamHandler", }, "file": { "level": "DEBUG", "class": "logging.FileHandler", @@ -185,11 +191,11 @@ }, "loggers": { "django.request": { - "handlers": ["mail_admins",], + "handlers": ["mail_admins", ], "level": "ERROR", "propagate": True, }, - "django.db.backends": {"level": "DEBUG", "handlers": ["file",],}, + "django.db.backends": {"level": "DEBUG", "handlers": ["file", ], }, }, } @@ -215,18 +221,14 @@ DATABASES = { - "default": { + "default": { "ENGINE": "django.db.backends.postgresql_psycopg2", - "NAME": os.environ.get("DB_NAME", "enter postgres db name"), - "USER": os.environ.get("DB_USER", "postgres"), - "PASSWORD": os.environ.get("DB_PASSWORD", "enter postgres password"), - "HOST": os.environ.get("DB_HOST", "localhost"), + "NAME": os.environ.get("POSTGRES_DB", "junction"), + "USER": os.environ.get("POSTGRES_USER", "postgres"), + "PASSWORD": os.environ.get("POSTGRES_PASSWORD", "junction"), + "HOST": os.environ.get("HOST_NAME", "db"), "PORT": os.environ.get("DB_PORT", "5432"), } - # "default": { - # "ENGINE": "django.db.backends.sqlite3", - # "NAME": os.path.join(ROOT_DIR, "test.sqlite3"), - # } } SECRET_KEY = os.environ.get( @@ -234,7 +236,7 @@ ) -ALLOWED_HOSTS = ["*"] +ALLOWED_HOSTS = ["*"] SITE_PROTOCOL = "http" @@ -267,4 +269,4 @@ ENABLE_SECOND_PHASE_VOTING = False -ENABLE_UPLOAD_CONTENT = False \ No newline at end of file +ENABLE_UPLOAD_CONTENT = False diff --git a/settings/dev.py.sample b/settings/dev.py.sample index d41d0829..72784104 100644 --- a/settings/dev.py.sample +++ b/settings/dev.py.sample @@ -23,8 +23,6 @@ TEMPLATES[0]['OPTIONS']['context_processors'].extend([ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' -INSTALLED_APPS += ('django_extensions',) - # settings for celery BROKER_URL = os.environ.get("BROKER_URL", "redis://127.0.0.1:6379/0") CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", 'redis://127.0.0.1:6379/0') diff --git a/settings/test_settings.py b/settings/test_settings.py index 78e09c47..22aabe8d 100644 --- a/settings/test_settings.py +++ b/settings/test_settings.py @@ -21,6 +21,4 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" -INSTALLED_APPS += ("django_extensions",) - DEVICE_VERIFICATION_CODE = 11111