{% blocktrans %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}
{% blocktrans %}This part of the site requires us to verify that
13 | you are who you claim to be. For this purpose, we require that you
14 | verify ownership of your e-mail address. {% endblocktrans %}
15 |
16 |
{% blocktrans %}We have sent an e-mail to you for
17 | verification. Please click on the link inside this e-mail. Please
18 | contact us if you do not receive it within a few minutes.{% endblocktrans %}
{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}
9 |
10 | {% if token_fail %}
11 | {% url 'account_reset_password' as passwd_reset_url %}
12 |
{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %}
13 | {% else %}
14 | {% if form %}
15 |
20 | {% else %}
21 |
{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}
28 |
29 | {% endif %}
30 |
31 | {% endblock %}
32 |
33 |
--------------------------------------------------------------------------------
/compose/production/django/entrypoint:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -o errexit
4 | set -o pipefail
5 | set -o nounset
6 |
7 |
8 | # N.B. If only .env files supported variable expansion...
9 | export CELERY_BROKER_URL="${REDIS_URL}"
10 |
11 | if [ -z "${POSTGRES_USER}" ]; then
12 | base_postgres_image_default_user='postgres'
13 | export POSTGRES_USER="${base_postgres_image_default_user}"
14 | fi
15 | export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
16 |
17 | postgres_ready() {
18 | python << END
19 | import sys
20 |
21 | import psycopg2
22 |
23 | try:
24 | psycopg2.connect(
25 | dbname="${POSTGRES_DB}",
26 | user="${POSTGRES_USER}",
27 | password="${POSTGRES_PASSWORD}",
28 | host="${POSTGRES_HOST}",
29 | port="${POSTGRES_PORT}",
30 | )
31 | except psycopg2.OperationalError:
32 | sys.exit(-1)
33 | sys.exit(0)
34 |
35 | END
36 | }
37 | until postgres_ready; do
38 | >&2 echo 'Waiting for PostgreSQL to become available...'
39 | sleep 1
40 | done
41 | >&2 echo 'PostgreSQL is available'
42 |
43 | exec "$@"
44 |
--------------------------------------------------------------------------------
/requirements/base.txt:
--------------------------------------------------------------------------------
1 | pytz==2018.4 # https://github.com/stub42/pytz
2 | python-slugify==1.2.5 # https://github.com/un33k/python-slugify
3 | Pillow==5.1.0 # https://github.com/python-pillow/Pillow
4 | argon2-cffi==18.1.0 # https://github.com/hynek/argon2_cffi
5 | redis>=2.10.5 # https://github.com/antirez/redis
6 |
7 | # Django
8 | # ------------------------------------------------------------------------------
9 | django==2.0.6 # pyup: < 2.1 # https://www.djangoproject.com/
10 | django-environ==0.4.4 # https://github.com/joke2k/django-environ
11 | django-model-utils==3.1.2 # https://github.com/jazzband/django-model-utils
12 | django-allauth==0.36.0 # https://github.com/pennersr/django-allauth
13 | django-crispy-forms==1.7.2 # https://github.com/django-crispy-forms/django-crispy-forms
14 | django-redis==4.9.0 # https://github.com/niwinz/django-redis
15 |
16 | # Django REST Framework
17 | djangorestframework==3.8.2 # https://github.com/encode/django-rest-framework
18 | coreapi==2.3.3 # https://github.com/core-api/python-client
19 |
20 | # KeyCloak integration
21 | git+https://github.com/Peter-Slump/django-keycloak.git@7e325e8
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
7 |
8 | try:
9 | from django.core.management import execute_from_command_line
10 | except ImportError:
11 | # The above import may fail for some other reason. Ensure that the
12 | # issue is really that Django is missing to avoid masking other
13 | # exceptions on Python 2.
14 | try:
15 | import django # noqa
16 | except ImportError:
17 | raise ImportError(
18 | "Couldn't import Django. Are you sure it's installed and "
19 | "available on your PYTHONPATH environment variable? Did you "
20 | "forget to activate a virtual environment?"
21 | )
22 |
23 | raise
24 |
25 | # This allows easy placement of apps within the interior
26 | # demoapp directory.
27 | current_path = os.path.dirname(os.path.abspath(__file__))
28 | sys.path.append(os.path.join(current_path, "demoapp"))
29 |
30 | execute_from_command_line(sys.argv)
31 |
--------------------------------------------------------------------------------
/compose/production/django/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.6-alpine
2 |
3 | ENV PYTHONUNBUFFERED 1
4 |
5 | RUN apk update \
6 | # psycopg2 dependencies
7 | && apk add --virtual build-deps gcc python3-dev musl-dev \
8 | && apk add postgresql-dev \
9 | # Pillow dependencies
10 | && apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
11 | # CFFI dependencies
12 | && apk add libffi-dev py-cffi
13 |
14 | RUN addgroup -S django \
15 | && adduser -S -G django django
16 |
17 | # Requirements are installed here to ensure they will be cached.
18 | COPY ./requirements /requirements
19 | RUN pip install --no-cache-dir -r /requirements/production.txt \
20 | && rm -rf /requirements
21 |
22 | COPY ./compose/production/django/entrypoint /entrypoint
23 | RUN sed -i 's/\r//' /entrypoint
24 | RUN chmod +x /entrypoint
25 | RUN chown django /entrypoint
26 |
27 | COPY ./compose/production/django/start /start
28 | RUN sed -i 's/\r//' /start
29 | RUN chmod +x /start
30 | RUN chown django /start
31 |
32 | COPY . /app
33 |
34 | RUN chown -R django /app
35 |
36 | USER django
37 |
38 | WORKDIR /app
39 |
40 | ENTRYPOINT ["/entrypoint"]
41 |
--------------------------------------------------------------------------------
/demoapp/contrib/sites/migrations/0003_set_site_domain_and_name.py:
--------------------------------------------------------------------------------
1 | """
2 | To understand why this file is here, please read:
3 |
4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
5 | """
6 | from django.conf import settings
7 | from django.db import migrations
8 |
9 |
10 | def update_site_forward(apps, schema_editor):
11 | """Set site domain and name."""
12 | Site = apps.get_model("sites", "Site")
13 | Site.objects.update_or_create(
14 | id=settings.SITE_ID,
15 | defaults={
16 | "domain": "example.com",
17 | "name": "DemoApp",
18 | },
19 | )
20 |
21 |
22 | def update_site_backward(apps, schema_editor):
23 | """Revert site domain and name to default."""
24 | Site = apps.get_model("sites", "Site")
25 | Site.objects.update_or_create(
26 | id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"}
27 | )
28 |
29 |
30 | class Migration(migrations.Migration):
31 |
32 | dependencies = [("sites", "0002_alter_domain_unique")]
33 |
34 | operations = [migrations.RunPython(update_site_forward, update_site_backward)]
35 |
--------------------------------------------------------------------------------
/compose/production/postgres/maintenance/backup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 |
4 | ### Create a database backup.
5 | ###
6 | ### Usage:
7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backup
8 |
9 |
10 | set -o errexit
11 | set -o pipefail
12 | set -o nounset
13 |
14 |
15 | working_dir="$(dirname ${0})"
16 | source "${working_dir}/_sourced/constants.sh"
17 | source "${working_dir}/_sourced/messages.sh"
18 |
19 |
20 | message_welcome "Backing up the '${POSTGRES_DB}' database..."
21 |
22 |
23 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then
24 | message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
25 | exit 1
26 | fi
27 |
28 | export PGHOST="${POSTGRES_HOST}"
29 | export PGPORT="${POSTGRES_PORT}"
30 | export PGUSER="${POSTGRES_USER}"
31 | export PGPASSWORD="${POSTGRES_PASSWORD}"
32 | export PGDATABASE="${POSTGRES_DB}"
33 |
34 | backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz"
35 | pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}"
36 |
37 |
38 | message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'."
39 |
--------------------------------------------------------------------------------
/production.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | volumes:
4 | postgres_data: {}
5 | postgres_backup: {}
6 | caddy: {}
7 |
8 | services:
9 | django:
10 | build:
11 | context: .
12 | dockerfile: ./compose/production/django/Dockerfile
13 | image: demoapp_production_django
14 | depends_on:
15 | - postgres
16 | - redis
17 | env_file:
18 | - ./.envs/.production/.django
19 | - ./.envs/.production/.postgres
20 | command: /start
21 |
22 | postgres:
23 | build:
24 | context: .
25 | dockerfile: ./compose/production/postgres/Dockerfile
26 | image: demoapp_production_postgres
27 | volumes:
28 | - postgres_data:/var/lib/postgresql/data
29 | - postgres_backup:/backups
30 | env_file:
31 | - ./.envs/.production/.postgres
32 |
33 | caddy:
34 | build:
35 | context: .
36 | dockerfile: ./compose/production/caddy/Dockerfile
37 | image: demoapp_production_caddy
38 | depends_on:
39 | - django
40 | volumes:
41 | - caddy:/root/.caddy
42 | env_file:
43 | - ./.envs/.production/.caddy
44 | ports:
45 | - "0.0.0.0:80:80"
46 | - "0.0.0.0:443:443"
47 |
48 | redis:
49 | image: redis:3.2
50 |
--------------------------------------------------------------------------------
/demoapp/users/admin.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib import admin
3 | from django.contrib.auth.admin import UserAdmin as AuthUserAdmin
4 | from django.contrib.auth.forms import UserChangeForm, UserCreationForm
5 | from .models import User
6 |
7 |
8 | class MyUserChangeForm(UserChangeForm):
9 |
10 | class Meta(UserChangeForm.Meta):
11 | model = User
12 |
13 |
14 | class MyUserCreationForm(UserCreationForm):
15 |
16 | error_message = UserCreationForm.error_messages.update(
17 | {"duplicate_username": "This username has already been taken."}
18 | )
19 |
20 | class Meta(UserCreationForm.Meta):
21 | model = User
22 |
23 | def clean_username(self):
24 | username = self.cleaned_data["username"]
25 | try:
26 | User.objects.get(username=username)
27 | except User.DoesNotExist:
28 | return username
29 |
30 | raise forms.ValidationError(self.error_messages["duplicate_username"])
31 |
32 |
33 | @admin.register(User)
34 | class MyUserAdmin(AuthUserAdmin):
35 | form = MyUserChangeForm
36 | add_form = MyUserCreationForm
37 | fieldsets = (("User Profile", {"fields": ("name",)}),) + AuthUserAdmin.fieldsets
38 | list_display = ("username", "name", "is_superuser")
39 | search_fields = ["name"]
40 |
--------------------------------------------------------------------------------
/requirements/local.txt:
--------------------------------------------------------------------------------
1 | -r ./base.txt
2 |
3 | Werkzeug==0.14.1 # https://github.com/pallets/werkzeug
4 | ipdb==0.11 # https://github.com/gotcha/ipdb
5 | Sphinx==1.7.5 # https://github.com/sphinx-doc/sphinx
6 | psycopg2==2.7.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
7 |
8 | # Testing
9 | # ------------------------------------------------------------------------------
10 | pytest==3.6.0 # https://github.com/pytest-dev/pytest
11 | pytest-sugar==0.9.1 # https://github.com/Frozenball/pytest-sugar
12 |
13 | # Code quality
14 | # ------------------------------------------------------------------------------
15 | flake8==3.5.0 # https://github.com/PyCQA/flake8
16 | coverage==4.5.1 # https://github.com/nedbat/coveragepy
17 |
18 | # Django
19 | # ------------------------------------------------------------------------------
20 | factory-boy==2.11.1 # https://github.com/FactoryBoy/factory_boy
21 | django-test-plus==1.0.22 # https://github.com/revsys/django-test-plus
22 |
23 | django-debug-toolbar==1.9.1 # https://github.com/jazzband/django-debug-toolbar
24 | django-extensions==2.0.7 # https://github.com/django-extensions/django-extensions
25 | django-coverage-plugin==1.5.0 # https://github.com/nedbat/django_coverage_plugin
26 | pytest-django==3.2.1 # https://github.com/pytest-dev/pytest-django
27 |
--------------------------------------------------------------------------------
/superset-conf/config/security.py:
--------------------------------------------------------------------------------
1 | from flask import redirect, g, flash, request
2 | from flask_appbuilder.security.views import UserDBModelView,AuthDBView
3 | from superset.security import SupersetSecurityManager
4 | from flask_appbuilder.security.views import expose
5 | from flask_appbuilder.security.manager import BaseSecurityManager
6 | from flask_login import login_user, logout_user
7 |
8 |
9 | class CustomAuthDBView(AuthDBView):
10 | login_template = 'appbuilder/general/security/login_db.html'
11 |
12 | @expose('/login/', methods=['GET', 'POST'])
13 | def login(self):
14 | if request.args.get('username') is not None:
15 | user = self.appbuilder.sm.find_user(username=request.args.get('username'))
16 | flash('Admin auto logged in', 'success')
17 | login_user(user, remember=False)
18 | return redirect(self.appbuilder.get_url_for_index)
19 | elif g.user is not None and g.user.is_authenticated():
20 | return redirect(self.appbuilder.get_url_for_index)
21 | else:
22 | flash('Unable to auto login', 'warning')
23 | return super(CustomAuthDBView,self).login()
24 |
25 | class CustomSecurityManager(SupersetSecurityManager):
26 | authdbview = CustomAuthDBView
27 | def __init__(self, appbuilder):
28 | super(CustomSecurityManager, self).__init__(appbuilder)
29 |
30 |
--------------------------------------------------------------------------------
/superset-conf/config/superset_config.py:
--------------------------------------------------------------------------------
1 | #---------------------------------------------------------
2 | # Superset specific config
3 | #---------------------------------------------------------
4 | ROW_LIMIT = 5000
5 |
6 | SUPERSET_WEBSERVER_PORT = 8088
7 | #---------------------------------------------------------
8 |
9 | #---------------------------------------------------------
10 | # Flask App Builder configuration
11 | #---------------------------------------------------------
12 | # Your App secret key
13 | SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h'
14 |
15 | # The SQLAlchemy connection string to your database backend
16 | # This connection defines the path to the database that stores your
17 | # superset metadata (slices, connections, tables, dashboards, ...).
18 | # Note that the connection information to connect to the datasources
19 | # you want to explore are managed directly in the web UI
20 | SQLALCHEMY_DATABASE_URI = 'sqlite:////var/lib/superset/superset.db'
21 |
22 | # Flask-WTF flag for CSRF
23 | WTF_CSRF_ENABLED = False
24 | # Add endpoints that need to be exempt from CSRF protection
25 | WTF_CSRF_EXEMPT_LIST = []
26 | # A CSRF token that expires in 1 year
27 | WTF_CSRF_TIME_LIMIT = 60 * 60 * 24 * 365
28 |
29 | # Set this API key to enable Mapbox visualizations
30 | MAPBOX_API_KEY = ''
31 | ENABLE_PROXY_FIX = True
32 |
33 | from security import CustomSecurityManager
34 | CUSTOM_SECURITY_MANAGER = CustomSecurityManager
--------------------------------------------------------------------------------
/demoapp/users/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.mixins import LoginRequiredMixin
2 | from django.urls import reverse
3 | from django.views.generic import DetailView, ListView, RedirectView, UpdateView
4 |
5 | from .models import User
6 |
7 |
8 | class UserDetailView(LoginRequiredMixin, DetailView):
9 | model = User
10 | # These next two lines tell the view to index lookups by username
11 | slug_field = "username"
12 | slug_url_kwarg = "username"
13 |
14 |
15 | class UserRedirectView(LoginRequiredMixin, RedirectView):
16 | permanent = False
17 |
18 | def get_redirect_url(self):
19 | return reverse("users:detail", kwargs={"username": self.request.user.username})
20 |
21 |
22 | class UserUpdateView(LoginRequiredMixin, UpdateView):
23 |
24 | fields = ["name"]
25 |
26 | # we already imported User in the view code above, remember?
27 | model = User
28 |
29 | # send the user back to their own page after a successful update
30 |
31 | def get_success_url(self):
32 | return reverse("users:detail", kwargs={"username": self.request.user.username})
33 |
34 | def get_object(self):
35 | # Only get the User record for the user making the request
36 | return User.objects.get(username=self.request.user.username)
37 |
38 |
39 | class UserListView(LoginRequiredMixin, ListView):
40 | model = User
41 | # These next two lines tell the view to index lookups by username
42 | slug_field = "username"
43 | slug_url_kwarg = "username"
44 |
--------------------------------------------------------------------------------
/demoapp/contrib/sites/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | import django.contrib.sites.models
2 | from django.contrib.sites.models import _simple_domain_name_validator
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = []
9 |
10 | operations = [
11 | migrations.CreateModel(
12 | name="Site",
13 | fields=[
14 | (
15 | "id",
16 | models.AutoField(
17 | verbose_name="ID",
18 | serialize=False,
19 | auto_created=True,
20 | primary_key=True,
21 | ),
22 | ),
23 | (
24 | "domain",
25 | models.CharField(
26 | max_length=100,
27 | verbose_name="domain name",
28 | validators=[_simple_domain_name_validator],
29 | ),
30 | ),
31 | ("name", models.CharField(max_length=50, verbose_name="display name")),
32 | ],
33 | options={
34 | "ordering": ("domain",),
35 | "db_table": "django_site",
36 | "verbose_name": "site",
37 | "verbose_name_plural": "sites",
38 | },
39 | bases=(models.Model,),
40 | managers=[("objects", django.contrib.sites.models.SiteManager())],
41 | )
42 | ]
43 |
--------------------------------------------------------------------------------
/demoapp/users/tests/test_admin.py:
--------------------------------------------------------------------------------
1 | from test_plus.test import TestCase
2 |
3 | from ..admin import MyUserCreationForm
4 |
5 |
6 | class TestMyUserCreationForm(TestCase):
7 |
8 | def setUp(self):
9 | self.user = self.make_user("notalamode", "notalamodespassword")
10 |
11 | def test_clean_username_success(self):
12 | # Instantiate the form with a new username
13 | form = MyUserCreationForm(
14 | {
15 | "username": "alamode",
16 | "password1": "7jefB#f@Cc7YJB]2v",
17 | "password2": "7jefB#f@Cc7YJB]2v",
18 | }
19 | )
20 | # Run is_valid() to trigger the validation
21 | valid = form.is_valid()
22 | self.assertTrue(valid)
23 |
24 | # Run the actual clean_username method
25 | username = form.clean_username()
26 | self.assertEqual("alamode", username)
27 |
28 | def test_clean_username_false(self):
29 | # Instantiate the form with the same username as self.user
30 | form = MyUserCreationForm(
31 | {
32 | "username": self.user.username,
33 | "password1": "notalamodespassword",
34 | "password2": "notalamodespassword",
35 | }
36 | )
37 | # Run is_valid() to trigger the validation, which is going to fail
38 | # because the username is already taken
39 | valid = form.is_valid()
40 | self.assertFalse(valid)
41 |
42 | # The form.errors dict should contain a single error called 'username'
43 | self.assertTrue(len(form.errors) == 1)
44 | self.assertTrue("username" in form.errors)
45 |
--------------------------------------------------------------------------------
/demoapp/templates/account/login.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account socialaccount %}
5 | {% load crispy_forms_tags %}
6 |
7 | {% block head_title %}{% trans "Sign In" %}{% endblock %}
8 |
9 | {% block inner %}
10 |
11 |
{% trans "Sign In" %}
12 |
13 | {% get_providers as socialaccount_providers %}
14 |
15 | {% if socialaccount_providers %}
16 |
{% blocktrans with site.name as site_name %}Please sign in with one
17 | of your existing third party accounts. Or, sign up
18 | for a {{ site_name }} account and sign in below:{% endblocktrans %}
19 |
20 |
21 |
22 |
23 | {% include "socialaccount/snippets/provider_list.html" with process="login" %}
24 |
{% blocktrans %}If you have not created an account yet, then please
34 | sign up first.{% endblocktrans %}
35 | {% endif %}
36 |
37 |
46 |
47 | {% endblock %}
48 |
49 |
--------------------------------------------------------------------------------
/local.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 |
3 | volumes:
4 | postgres_data_local: {}
5 | postgres_backup_local: {}
6 |
7 | services:
8 | django:
9 | build:
10 | context: .
11 | dockerfile: ./compose/local/django/Dockerfile
12 | image: demoapp_local_django
13 | depends_on:
14 | - postgres
15 | - mailhog
16 | volumes:
17 | - .:/app
18 | env_file:
19 | - ./.envs/.local/.django
20 | - ./.envs/.local/.postgres
21 | ports:
22 | - "8000:8000"
23 | command: /start
24 | networks:
25 | - demo-app
26 |
27 |
28 | postgres:
29 | build:
30 | context: .
31 | dockerfile: ./compose/production/postgres/Dockerfile
32 | image: demoapp_production_postgres
33 | volumes:
34 | - postgres_data_local:/var/lib/postgresql/data
35 | - postgres_backup_local:/backups
36 | env_file:
37 | - ./.envs/.local/.postgres
38 | ports:
39 | - "5432:5432"
40 | networks:
41 | - demo-app
42 |
43 | mailhog:
44 | image: mailhog/mailhog:v1.0.0
45 | ports:
46 | - "8025:8025"
47 | environment:
48 | - MH_UI_WEB_PATH=mailhog
49 | networks:
50 | - demo-app
51 |
52 | superset:
53 | image: amancevice/superset:0.25.6
54 | ports:
55 | - "8088:8088"
56 | volumes:
57 | - ./superset-conf/config:/etc/superset
58 | - ./superset-conf/data:/var/lib/superset
59 | networks:
60 | - demo-app
61 |
62 | nginx:
63 | image: nginx:1.15
64 | volumes:
65 | - ./nginx-conf/nginx.conf:/etc/nginx/nginx.conf:ro
66 | ports:
67 | - "8080:8080"
68 | - "8081:8081"
69 | networks:
70 | - demo-app
71 | depends_on:
72 | - django
73 | - superset
74 |
75 | networks:
76 | demo-app:
77 |
--------------------------------------------------------------------------------
/demoapp/users/tests/test_urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import reverse, resolve
2 |
3 | from test_plus.test import TestCase
4 |
5 |
6 | class TestUserURLs(TestCase):
7 | """Test URL patterns for users app."""
8 |
9 | def setUp(self):
10 | self.user = self.make_user()
11 |
12 | def test_list_reverse(self):
13 | """users:list should reverse to /users/."""
14 | self.assertEqual(reverse("users:list"), "/users/")
15 |
16 | def test_list_resolve(self):
17 | """/users/ should resolve to users:list."""
18 | self.assertEqual(resolve("/users/").view_name, "users:list")
19 |
20 | def test_redirect_reverse(self):
21 | """users:redirect should reverse to /users/~redirect/."""
22 | self.assertEqual(reverse("users:redirect"), "/users/~redirect/")
23 |
24 | def test_redirect_resolve(self):
25 | """/users/~redirect/ should resolve to users:redirect."""
26 | self.assertEqual(resolve("/users/~redirect/").view_name, "users:redirect")
27 |
28 | def test_detail_reverse(self):
29 | """users:detail should reverse to /users/testuser/."""
30 | self.assertEqual(
31 | reverse("users:detail", kwargs={"username": "testuser"}), "/users/testuser/"
32 | )
33 |
34 | def test_detail_resolve(self):
35 | """/users/testuser/ should resolve to users:detail."""
36 | self.assertEqual(resolve("/users/testuser/").view_name, "users:detail")
37 |
38 | def test_update_reverse(self):
39 | """users:update should reverse to /users/~update/."""
40 | self.assertEqual(reverse("users:update"), "/users/~update/")
41 |
42 | def test_update_resolve(self):
43 | """/users/~update/ should resolve to users:update."""
44 | self.assertEqual(resolve("/users/~update/").view_name, "users:update")
45 |
--------------------------------------------------------------------------------
/config/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for DemoApp project.
3 |
4 | This module contains the WSGI application used by Django's development server
5 | and any production WSGI deployments. It should expose a module-level variable
6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
7 | this application via the ``WSGI_APPLICATION`` setting.
8 |
9 | Usually you will have the standard Django WSGI application here, but it also
10 | might make sense to replace the whole Django WSGI application with a custom one
11 | that later delegates to the Django one. For example, you could introduce WSGI
12 | middleware here, or combine a Django application with an application of another
13 | framework.
14 |
15 | """
16 | import os
17 | import sys
18 |
19 | from django.core.wsgi import get_wsgi_application
20 |
21 | # This allows easy placement of apps within the interior
22 | # demoapp directory.
23 | app_path = os.path.abspath(os.path.join(
24 | os.path.dirname(os.path.abspath(__file__)), os.pardir))
25 | sys.path.append(os.path.join(app_path, 'demoapp'))
26 |
27 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
28 | # if running multiple sites in the same mod_wsgi process. To fix this, use
29 | # mod_wsgi daemon mode with each site in its own daemon process, or use
30 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production"
31 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
32 |
33 | # This application object is used by any WSGI server configured to use this
34 | # file. This includes Django's development server, if the WSGI_APPLICATION
35 | # setting points here.
36 | application = get_wsgi_application()
37 |
38 | # Apply WSGI middleware here.
39 | # from helloworld.wsgi import HelloWorldApplication
40 | # application = HelloWorldApplication(application)
41 |
--------------------------------------------------------------------------------
/utility/install_python_dependencies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | WORK_DIR="$(dirname "$0")"
4 | PROJECT_DIR="$(dirname "$WORK_DIR")"
5 |
6 | pip --version >/dev/null 2>&1 || {
7 | echo >&2 -e "\npip is required but it's not installed."
8 | echo >&2 -e "You can install it by running the following command:\n"
9 | echo >&2 "wget https://bootstrap.pypa.io/get-pip.py --output-document=get-pip.py; chmod +x get-pip.py; sudo -H python3 get-pip.py"
10 | echo >&2 -e "\n"
11 | echo >&2 -e "\nFor more information, see pip documentation: https://pip.pypa.io/en/latest/"
12 | exit 1;
13 | }
14 |
15 | virtualenv --version >/dev/null 2>&1 || {
16 | echo >&2 -e "\nvirtualenv is required but it's not installed."
17 | echo >&2 -e "You can install it by running the following command:\n"
18 | echo >&2 "sudo -H pip3 install virtualenv"
19 | echo >&2 -e "\n"
20 | echo >&2 -e "\nFor more information, see virtualenv documentation: https://virtualenv.pypa.io/en/latest/"
21 | exit 1;
22 | }
23 |
24 | if [ -z "$VIRTUAL_ENV" ]; then
25 | echo >&2 -e "\nYou need activate a virtualenv first"
26 | echo >&2 -e 'If you do not have a virtualenv created, run the following command to create and automatically activate a new virtualenv named "venv" on current folder:\n'
27 | echo >&2 -e "virtualenv venv --python=\`which python3\`"
28 | echo >&2 -e "\nTo leave/disable the currently active virtualenv, run the following command:\n"
29 | echo >&2 "deactivate"
30 | echo >&2 -e "\nTo activate the virtualenv again, run the following command:\n"
31 | echo >&2 "source venv/bin/activate"
32 | echo >&2 -e "\nFor more information, see virtualenv documentation: https://virtualenv.pypa.io/en/latest/"
33 | echo >&2 -e "\n"
34 | exit 1;
35 | else
36 |
37 | pip install -r $PROJECT_DIR/requirements/local.txt
38 |
39 | fi
40 |
--------------------------------------------------------------------------------
/compose/production/postgres/maintenance/restore:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 |
4 | ### Restore database from a backup.
5 | ###
6 | ### Parameters:
7 | ### <1> filename of an existing backup.
8 | ###
9 | ### Usage:
10 | ### $ docker-compose -f .yml (exec |run --rm) postgres restore <1>
11 |
12 |
13 | set -o errexit
14 | set -o pipefail
15 | set -o nounset
16 |
17 |
18 | working_dir="$(dirname ${0})"
19 | source "${working_dir}/_sourced/constants.sh"
20 | source "${working_dir}/_sourced/messages.sh"
21 |
22 |
23 | if [[ -z ${1+x} ]]; then
24 | message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again."
25 | exit 1
26 | fi
27 | backup_filename="${BACKUP_DIR_PATH}/${1}"
28 | if [[ ! -f "${backup_filename}" ]]; then
29 | message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again."
30 | exit 1
31 | fi
32 |
33 | message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..."
34 |
35 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then
36 | message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
37 | exit 1
38 | fi
39 |
40 | export PGHOST="${POSTGRES_HOST}"
41 | export PGPORT="${POSTGRES_PORT}"
42 | export PGUSER="${POSTGRES_USER}"
43 | export PGPASSWORD="${POSTGRES_PASSWORD}"
44 | export PGDATABASE="${POSTGRES_DB}"
45 |
46 | message_info "Dropping the database..."
47 | dropdb "${PGDATABASE}"
48 |
49 | message_info "Creating a new database..."
50 | createdb --owner="${POSTGRES_USER}"
51 |
52 | message_info "Applying the backup to the new database..."
53 | gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}"
54 |
55 | message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup."
56 |
--------------------------------------------------------------------------------
/demoapp/users/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from django.test import RequestFactory
2 |
3 | from test_plus.test import TestCase
4 |
5 | from ..views import UserRedirectView, UserUpdateView
6 |
7 |
8 | class BaseUserTestCase(TestCase):
9 |
10 | def setUp(self):
11 | self.user = self.make_user()
12 | self.factory = RequestFactory()
13 |
14 |
15 | class TestUserRedirectView(BaseUserTestCase):
16 |
17 | def test_get_redirect_url(self):
18 | # Instantiate the view directly. Never do this outside a test!
19 | view = UserRedirectView()
20 | # Generate a fake request
21 | request = self.factory.get("/fake-url")
22 | # Attach the user to the request
23 | request.user = self.user
24 | # Attach the request to the view
25 | view.request = request
26 | # Expect: '/users/testuser/', as that is the default username for
27 | # self.make_user()
28 | self.assertEqual(view.get_redirect_url(), "/users/testuser/")
29 |
30 |
31 | class TestUserUpdateView(BaseUserTestCase):
32 |
33 | def setUp(self):
34 | # call BaseUserTestCase.setUp()
35 | super(TestUserUpdateView, self).setUp()
36 | # Instantiate the view directly. Never do this outside a test!
37 | self.view = UserUpdateView()
38 | # Generate a fake request
39 | request = self.factory.get("/fake-url")
40 | # Attach the user to the request
41 | request.user = self.user
42 | # Attach the request to the view
43 | self.view.request = request
44 |
45 | def test_get_success_url(self):
46 | # Expect: '/users/testuser/', as that is the default username for
47 | # self.make_user()
48 | self.assertEqual(self.view.get_success_url(), "/users/testuser/")
49 |
50 | def test_get_object(self):
51 | # Expect: self.user, as that is the request's user object
52 | self.assertEqual(self.view.get_object(), self.user)
53 |
--------------------------------------------------------------------------------
/nginx-conf/nginx.conf:
--------------------------------------------------------------------------------
1 |
2 | worker_processes 1;
3 |
4 |
5 | events {
6 | worker_connections 1024;
7 | }
8 |
9 |
10 | http {
11 | include mime.types;
12 | default_type application/octet-stream;
13 |
14 | sendfile on;
15 | keepalive_timeout 65;
16 | proxy_set_header X-Real-IP $remote_addr;
17 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
18 | proxy_set_header Host $host:$server_port;
19 | proxy_set_header X-Forwarded-Host $server_name;
20 | chunked_transfer_encoding on;
21 | proxy_set_header X-NginX-Proxy true;
22 | proxy_set_header Upgrade $http_upgrade;
23 | proxy_set_header Connection "upgrade";
24 | proxy_http_version 1.1;
25 | proxy_redirect off;
26 | proxy_buffering off;
27 |
28 | upstream superset-server {
29 | server superset:8088;
30 | }
31 |
32 | upstream django-server {
33 | server django:8000;
34 | }
35 |
36 | upstream mailhog {
37 | server mailhog:8025;
38 | }
39 |
40 | server {
41 | listen 8080;
42 | gzip_static on;
43 | gzip_disable "msie6";
44 | gzip_proxied any;
45 |
46 | location / {
47 | proxy_pass http://django-server/;
48 | proxy_redirect off;
49 | }
50 |
51 | location /ss {
52 | proxy_pass http://nginx:8081/;
53 | }
54 |
55 | location /mailhog {
56 | proxy_pass http://mailhog/mailhog;
57 | }
58 | }
59 |
60 | server {
61 | # Once we have a domain name, both django and superset will run on same 80 port
62 | # they will have different server name like viz.domain.com and www.domain.com
63 | listen 8081;
64 | gzip_static on;
65 | gzip_disable "msie6";
66 | gzip_proxied any;
67 |
68 | location / {
69 | proxy_pass http://superset:8088/;
70 | }
71 |
72 | }
73 | }
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/config/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.urls import include, path
3 | from django.conf.urls.static import static
4 | from django.contrib import admin
5 | from django.views.generic import TemplateView
6 | from django.views import defaults as default_views
7 | from django_keycloak import views as keycloak_views
8 |
9 | urlpatterns = [
10 | path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
11 | path(
12 | "about/",
13 | TemplateView.as_view(template_name="pages/about.html"),
14 | name="about",
15 | ),
16 | # Django Admin, use {% url 'admin:index' %}
17 | path(settings.ADMIN_URL, admin.site.urls),
18 | # User management
19 | path(
20 | "users/",
21 | include("demoapp.users.urls", namespace="users"),
22 | ),
23 | path("accounts/", include("allauth.urls")),
24 | path("login", keycloak_views.Login.as_view(), name="keycloak_login"),
25 | path("login-complete",keycloak_views.LoginComplete.as_view(),name="login-complete"),
26 | path("logout", keycloak_views.Logout.as_view(), name='keycloak_logout'),
27 |
28 | # Your stuff: custom urls includes go here
29 | ] + static(
30 | settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
31 | )
32 |
33 | if settings.DEBUG:
34 | # This allows the error pages to be debugged during development, just visit
35 | # these url in browser to see how these error pages look like.
36 | urlpatterns += [
37 | path(
38 | "400/",
39 | default_views.bad_request,
40 | kwargs={"exception": Exception("Bad Request!")},
41 | ),
42 | path(
43 | "403/",
44 | default_views.permission_denied,
45 | kwargs={"exception": Exception("Permission Denied")},
46 | ),
47 | path(
48 | "404/",
49 | default_views.page_not_found,
50 | kwargs={"exception": Exception("Page not Found")},
51 | ),
52 | path("500/", default_views.server_error),
53 | ]
54 | if "debug_toolbar" in settings.INSTALLED_APPS:
55 | import debug_toolbar
56 |
57 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns
58 |
--------------------------------------------------------------------------------
/config/settings/test.py:
--------------------------------------------------------------------------------
1 | """
2 | With these settings, tests run faster.
3 | """
4 |
5 | from .base import * # noqa
6 | from .base import env
7 |
8 | # GENERAL
9 | # ------------------------------------------------------------------------------
10 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug
11 | DEBUG = False
12 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
13 | SECRET_KEY = env("DJANGO_SECRET_KEY", default="dGeObIwNzjEB0hI5u307R2AgsmycDH6bAUQLuVy14hrq5veydZt9YT1CksDf0EyM")
14 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
15 | TEST_RUNNER = "django.test.runner.DiscoverRunner"
16 |
17 | # CACHES
18 | # ------------------------------------------------------------------------------
19 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches
20 | CACHES = {
21 | "default": {
22 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": ""
23 | }
24 | }
25 |
26 | # PASSWORDS
27 | # ------------------------------------------------------------------------------
28 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
29 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
30 |
31 | # TEMPLATES
32 | # ------------------------------------------------------------------------------
33 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates
34 | TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405
35 | TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405
36 | (
37 | "django.template.loaders.cached.Loader",
38 | [
39 | "django.template.loaders.filesystem.Loader",
40 | "django.template.loaders.app_directories.Loader",
41 | ],
42 | )
43 | ]
44 |
45 | # EMAIL
46 | # ------------------------------------------------------------------------------
47 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
48 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
49 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host
50 | EMAIL_HOST = "localhost"
51 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port
52 | EMAIL_PORT = 1025
53 |
54 | # Your stuff...
55 | # ------------------------------------------------------------------------------
56 |
--------------------------------------------------------------------------------
/merge_production_dotenvs_in_dotenv.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Sequence
3 |
4 | import pytest
5 |
6 | ROOT_DIR_PATH = os.path.dirname(os.path.realpath(__file__))
7 | PRODUCTION_DOTENVS_DIR_PATH = os.path.join(ROOT_DIR_PATH, ".envs", ".production")
8 | PRODUCTION_DOTENV_FILE_PATHS = [
9 | os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".django"),
10 | os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".postgres"),
11 | os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".caddy"),
12 | ]
13 | DOTENV_FILE_PATH = os.path.join(ROOT_DIR_PATH, ".env")
14 |
15 |
16 | def merge(
17 | output_file_path: str, merged_file_paths: Sequence[str], append_linesep: bool = True
18 | ) -> None:
19 | with open(output_file_path, "w") as output_file:
20 | for merged_file_path in merged_file_paths:
21 | with open(merged_file_path, "r") as merged_file:
22 | merged_file_content = merged_file.read()
23 | output_file.write(merged_file_content)
24 | if append_linesep:
25 | output_file.write(os.linesep)
26 |
27 |
28 | def main():
29 | merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS)
30 |
31 |
32 | @pytest.mark.parametrize("merged_file_count", range(3))
33 | @pytest.mark.parametrize("append_linesep", [True, False])
34 | def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool):
35 | tmp_dir_path = str(tmpdir_factory.getbasetemp())
36 |
37 | output_file_path = os.path.join(tmp_dir_path, ".env")
38 |
39 | expected_output_file_content = ""
40 | merged_file_paths = []
41 | for i in range(merged_file_count):
42 | merged_file_ord = i + 1
43 |
44 | merged_filename = ".service{}".format(merged_file_ord)
45 | merged_file_path = os.path.join(tmp_dir_path, merged_filename)
46 |
47 | merged_file_content = merged_filename * merged_file_ord
48 |
49 | with open(merged_file_path, "w+") as file:
50 | file.write(merged_file_content)
51 |
52 | expected_output_file_content += merged_file_content
53 | if append_linesep:
54 | expected_output_file_content += os.linesep
55 |
56 | merged_file_paths.append(merged_file_path)
57 |
58 | merge(output_file_path, merged_file_paths, append_linesep)
59 |
60 | with open(output_file_path, "r") as output_file:
61 | actual_output_file_content = output_file.read()
62 |
63 | assert actual_output_file_content == expected_output_file_content
64 |
65 |
66 | if __name__ == "__main__":
67 | main()
68 |
--------------------------------------------------------------------------------
/utility/install_os_dependencies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | WORK_DIR="$(dirname "$0")"
4 | DISTRO_NAME=$(lsb_release -sc)
5 | OS_REQUIREMENTS_FILENAME="requirements-$DISTRO_NAME.apt"
6 |
7 | cd $WORK_DIR
8 |
9 | # Check if a requirements file exist for the current distribution.
10 | if [ ! -r "$OS_REQUIREMENTS_FILENAME" ]; then
11 | cat <<-EOF >&2
12 | There is no requirements file for your distribution.
13 | You can see one of the files listed below to help search the equivalent package in your system:
14 | $(find ./ -name "requirements-*.apt" -printf " - %f\n")
15 | EOF
16 | exit 1;
17 | fi
18 |
19 | # Handle call with wrong command
20 | function wrong_command()
21 | {
22 | echo "${0##*/} - unknown command: '${1}'" >&2
23 | usage_message
24 | }
25 |
26 | # Print help / script usage
27 | function usage_message()
28 | {
29 | cat <<-EOF
30 | Usage: $WORK_DIR/${0##*/}
31 | Available commands are:
32 | list Print a list of all packages defined on ${OS_REQUIREMENTS_FILENAME} file
33 | help Print this help
34 |
35 | Commands that require superuser permission:
36 | install Install packages defined on ${OS_REQUIREMENTS_FILENAME} file. Note: This
37 | does not upgrade the packages already installed for new versions, even if
38 | new version is available in the repository.
39 | upgrade Same that install, but upgrade the already installed packages, if new
40 | version is available.
41 | EOF
42 | }
43 |
44 | # Read the requirements.apt file, and remove comments and blank lines
45 | function list_packages(){
46 | grep -v "#" "${OS_REQUIREMENTS_FILENAME}" | grep -v "^$";
47 | }
48 |
49 | function install_packages()
50 | {
51 | list_packages | xargs apt-get --no-upgrade install -y;
52 | }
53 |
54 | function upgrade_packages()
55 | {
56 | list_packages | xargs apt-get install -y;
57 | }
58 |
59 | function install_or_upgrade()
60 | {
61 | P=${1}
62 | PARAN=${P:-"install"}
63 |
64 | if [[ $EUID -ne 0 ]]; then
65 | cat <<-EOF >&2
66 | You must run this script with root privilege
67 | Please do:
68 | sudo $WORK_DIR/${0##*/} $PARAN
69 | EOF
70 | exit 1
71 | else
72 |
73 | apt-get update
74 |
75 | # Install the basic compilation dependencies and other required libraries of this project
76 | if [ "$PARAN" == "install" ]; then
77 | install_packages;
78 | else
79 | upgrade_packages;
80 | fi
81 |
82 | # cleaning downloaded packages from apt-get cache
83 | apt-get clean
84 |
85 | exit 0
86 | fi
87 | }
88 |
89 | # Handle command argument
90 | case "$1" in
91 | install) install_or_upgrade;;
92 | upgrade) install_or_upgrade "upgrade";;
93 | list) list_packages;;
94 | help|"") usage_message;;
95 | *) wrong_command "$1";;
96 | esac
97 |
--------------------------------------------------------------------------------
/demoapp/templates/account/email.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "account/base.html" %}
3 |
4 | {% load i18n %}
5 | {% load crispy_forms_tags %}
6 |
7 | {% block head_title %}{% trans "Account" %}{% endblock %}
8 |
9 | {% block inner %}
10 |
{% trans "E-mail Addresses" %}
11 |
12 | {% if user.emailaddress_set.all %}
13 |
{% trans 'The following e-mail addresses are associated with your account:' %}
14 |
15 |
44 |
45 | {% else %}
46 |
{% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
47 |
48 | {% endif %}
49 |
50 |
51 |
{% trans "Add E-mail Address" %}
52 |
53 |
58 |
59 | {% endblock %}
60 |
61 |
62 | {% block javascript %}
63 | {{ block.super }}
64 |
79 | {% endblock %}
80 |
81 |
--------------------------------------------------------------------------------
/config/settings/local.py:
--------------------------------------------------------------------------------
1 | from .base import * # noqa
2 | from .base import env
3 |
4 | # GENERAL
5 | # ------------------------------------------------------------------------------
6 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug
7 | DEBUG = True
8 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
9 | SECRET_KEY = env('DJANGO_SECRET_KEY', default='duqzzAHzNonIcKrtMeKlQjc5eZ1bF5iXRh8QIw2dz9dCLuO25qKSWqDmmrfUjHqN')
10 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
11 | # ALLOWED_HOSTS = [
12 | # "localhost",
13 | # "0.0.0.0",
14 | # "127.0.0.1",
15 | # "13.127.112.133",
16 | # "192.168.1.47",
17 | # "nginx",
18 | # ]
19 |
20 | ALLOWED_HOSTS = ['*']
21 | # CACHES
22 | # ------------------------------------------------------------------------------
23 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches
24 | CACHES = {
25 | 'default': {
26 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
27 | 'LOCATION': ''
28 | }
29 | }
30 |
31 | # TEMPLATES
32 | # ------------------------------------------------------------------------------
33 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates
34 | TEMPLATES[0]['OPTIONS']['debug'] = DEBUG # noqa F405
35 |
36 | # EMAIL
37 | # ------------------------------------------------------------------------------
38 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host
39 | EMAIL_HOST = env('EMAIL_HOST', default='mailhog')
40 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port
41 | EMAIL_PORT = 1025
42 |
43 | # django-debug-toolbar
44 | # ------------------------------------------------------------------------------
45 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
46 | INSTALLED_APPS += ['debug_toolbar'] # noqa F405
47 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
48 | MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] # noqa F405
49 | # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
50 | DEBUG_TOOLBAR_CONFIG = {
51 | 'DISABLE_PANELS': [
52 | 'debug_toolbar.panels.redirects.RedirectsPanel',
53 | ],
54 | 'SHOW_TEMPLATE_CONTEXT': True,
55 | }
56 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
57 | INTERNAL_IPS = ['127.0.0.1', '10.0.2.2']
58 | if env('USE_DOCKER') == 'yes':
59 | import socket
60 | hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
61 | INTERNAL_IPS += [ip[:-1] + '1' for ip in ips]
62 |
63 | # django-extensions
64 | # ------------------------------------------------------------------------------
65 | # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
66 | INSTALLED_APPS += ['django_extensions'] # noqa F405
67 |
68 | # Your stuff...
69 | # ------------------------------------------------------------------------------
70 |
--------------------------------------------------------------------------------
/superset-conf/custom-css-templates/blue-dashboard.css:
--------------------------------------------------------------------------------
1 | body{
2 | background: white;
3 | font-family: Courier, Monaco, monospace;
4 | }
5 |
6 | .btn-default{
7 | border-color: #3182bd;
8 | }
9 |
10 | .btn-default:active:hover, .btn-default.active:hover, .open > .dropdown-toggle.btn-default:hover, .btn-default:active:focus, .btn-default.active:focus, .open > .dropdown-toggle.btn-default:focus, .btn-default:active.focus, .btn-default.active.focus, .open > .dropdown-toggle.btn-default.focus {
11 | background-color: white;
12 | }
13 |
14 | .btn-default:hover{
15 | color: #3182bd;
16 | background-color: white;
17 | }
18 |
19 | /* navbar */
20 |
21 | .navbar {
22 | box-shadow: none;
23 | }
24 |
25 | .navbar {
26 | transition: opacity 0.5s ease;
27 | opacity: 1;
28 | }
29 |
30 | .navbar:hover {
31 | opacity: 1;
32 | }
33 |
34 | .navbar-inverse .navbar-nav > li > a{
35 | border-bottom-color: #3182bd !important;
36 | }
37 |
38 | .navbar-inverse .navbar-nav > li > a:hover{
39 | border-bottom-color: #3182bd;
40 | color:#3182bd;
41 | }
42 |
43 | .navbar-inverse .navbar-nav > .open > a:focus {
44 | background-color: white;
45 | }
46 |
47 | .dropdown-menu > li > a:hover{
48 | color:#3182bd;
49 | background: white;
50 | }
51 |
52 | /* chart */
53 | .chart-header .header{
54 | font-weight: normal;
55 | font-size: 12px;
56 | color: white !important;
57 | }
58 |
59 | .gridster div.widget {
60 | transition: background-color 0.5s ease;
61 | background-color: #3182bd;
62 | border: 2px solid white;
63 | border-radius: 15px;
64 | box-shadow: none;
65 | }
66 |
67 | .gridster div.widget:hover {
68 |
69 | }
70 |
71 | .pull-right a{
72 | color: white;
73 | }
74 |
75 | /* text */
76 |
77 | h2 {
78 | color: white;
79 | font-size: 52px;
80 | }
81 |
82 | h1{
83 | color:#3182bd;
84 | }
85 |
86 | .nvd3 text {
87 | font-size: 12px;
88 | font-family: inherit;
89 | }
90 |
91 | /* graph */
92 |
93 | /* tables */
94 |
95 | table {
96 | color: #3182bd !important;
97 | }
98 |
99 | .slice_container:hover{
100 | border-top-width:0px;
101 | border-right-width:1px;
102 | border-left-width:1px;
103 | border-bottom-width:1px;
104 | border-style: solid;
105 | border-radius: 0 0px 15px 15px;
106 | border-color: #3182bd;
107 | }
108 |
109 | /* world graph */
110 |
111 | .datamaps-bubble {
112 | fill:#3182bd !important;
113 | stroke:white !important;
114 | }
115 |
116 | .datamaps-hoverover {
117 | box-shadow: 1px 1px 5px #3182bd !important;
118 | color: #3182bd;
119 | }
120 |
121 | .hoverinfo {
122 | padding: 4px;
123 | border-radius: 1px;
124 | box-shadow: 1px 1px 5px #3182bd;
125 | font-size: 12px;
126 | border: 1px solid #CCC;
127 | }
128 |
129 | .datamaps-subunit:hover{
130 | fill: #3182bd !important;
131 | }
132 |
133 | .datamaps-subunit:not(:hover){
134 | fill: #ccc !important;
135 | }
136 |
137 | /**/
138 |
139 | text.big{
140 | stroke: white !important;
141 | fill: #3182bd !important;
142 | }
143 |
--------------------------------------------------------------------------------
/superset-conf/custom-css-templates/red-dashboard.css:
--------------------------------------------------------------------------------
1 | body{
2 | background: white;
3 | font-family: Courier, Monaco, monospace;
4 | }
5 |
6 | .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus {
7 | text-decoration: none;
8 | color: white !important;
9 | background-color: red !important;
10 | }
11 |
12 | .btn-default{
13 | border-color: red;
14 | }
15 |
16 | .btn-default:active:hover, .btn-default.active:hover, .open > .dropdown-toggle.btn-default:hover, .btn-default:active:focus, .btn-default.active:focus, .open > .dropdown-toggle.btn-default:focus, .btn-default:active.focus, .btn-default.active.focus, .open > .dropdown-toggle.btn-default.focus {
17 | background-color: red;
18 | color: white;
19 | border-color: red;
20 | }
21 |
22 | .btn-default:hover{
23 | color: white !important;
24 | background-color: red;
25 | border-color: red;
26 | }
27 |
28 | /* navbar */
29 |
30 | .navbar {
31 | box-shadow: none;
32 | }
33 |
34 | .navbar {
35 | transition: opacity 0.5s ease;
36 | opacity: 1;
37 | }
38 |
39 | .navbar:hover {
40 | opacity: 1;
41 | }
42 |
43 | .navbar-inverse .navbar-nav > li > a{
44 | border-bottom-color: red !important;
45 | }
46 |
47 | .navbar-inverse .navbar-nav > li > a:hover{
48 | border-bottom-color: red;
49 | color:red;
50 | }
51 |
52 | .navbar-inverse .navbar-nav > .open > a:focus {
53 | color:red;
54 | background-color: white;
55 | }
56 |
57 | .dropdown-menu > li > a:hover{
58 | color:red;
59 | background: white;
60 | }
61 |
62 |
63 | /* chart */
64 | .chart-header .header{
65 | font-weight: normal;
66 | font-size: 12px;
67 | color: white !important;
68 | }
69 |
70 | .gridster div.widget {
71 | transition: background-color 0.5s ease;
72 | background-color: red;
73 | border: 2px solid white;
74 | border-radius: 15px;
75 | box-shadow: none;
76 | }
77 |
78 | .gridster div.widget:hover {
79 |
80 | }
81 |
82 | .pull-right a{
83 | color: white;
84 | }
85 |
86 | /* text */
87 |
88 | h2 {
89 | color: white;
90 | font-size: 52px;
91 | }
92 |
93 | h1{
94 | color:red;
95 | }
96 |
97 | .nvd3 text {
98 | font-size: 12px;
99 | font-family: inherit;
100 | }
101 |
102 | /* graph */
103 |
104 | /* tables */
105 |
106 | table {
107 | color: red !important;
108 | }
109 |
110 | .slice_container:hover{
111 | border-top-width:0px;
112 | border-right-width:1px;
113 | border-left-width:1px;
114 | border-bottom-width:1px;
115 | border-style: solid;
116 | border-radius: 0 0px 15px 15px;
117 | border-color: red;
118 | }
119 |
120 | /* world graph */
121 |
122 | .datamaps-bubble {
123 | fill:red !important;
124 | stroke:white !important;
125 | }
126 |
127 | .datamaps-hoverover {
128 | box-shadow: 1px 1px 5px red !important;
129 | color: red;
130 | }
131 |
132 | .hoverinfo {
133 | padding: 4px;
134 | border-radius: 1px;
135 | box-shadow: 1px 1px 5px red;
136 | font-size: 12px;
137 | border: 1px solid #CCC;
138 | }
139 |
140 | .datamaps-subunit:hover{
141 | fill: red !important;
142 | }
143 |
144 | .datamaps-subunit:not(:hover){
145 | fill: #ccc !important;
146 | }
147 |
148 | /**/
149 |
150 | text.big{
151 | stroke: white !important;
152 | fill: red !important;
153 | }
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | DemoApp
2 | =======
3 |
4 | Demostrate django app which can integrate with Apache Superset
5 |
6 | Settings
7 | --------
8 |
9 | Moved to settings_.
10 |
11 | .. _settings: http://cookiecutter-django.readthedocs.io/en/latest/settings.html
12 |
13 | Getting started
14 | --------------
15 |
16 | This application development is done within Docker containers. It should be quick to get up and running with the application.
17 |
18 | `docker-compose -f local.yml up`
19 | DB migration is already taken care of., So no need to execute them.
20 |
21 | On a different terminal, execute management commands:
22 | ````
23 | docker-compose -f local.yml run --rm django python manage.py createsuperuser
24 | #django is the django app container name
25 | ````
26 |
27 | Setting Up Your Users
28 | ---
29 |
30 | * To create a **normal user account**, just go to Sign Up and fill out the form. Once you submit it, you'll see a "Verify Your E-mail Address" page. Go to your console to see a simulated email verification message. Copy the link into your browser. Now the user's email should be verified and ready to go.
31 |
32 | * To create an **superuser account**, use this command::
33 |
34 | $ python manage.py createsuperuser
35 |
36 | For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users.
37 |
38 | Superset database initialization
39 | ---
40 |
41 | On AWS EC2 or linux instance, need to grant write access to superset data directory
42 | `chmod 777 superset-conf/data`
43 |
44 | Superset needs to initialize the database and create admin user account. Please execute the following command
45 |
46 | `docker exec -it django-demo-project_superset_1 superset-init`
47 |
48 | django-demo-project_superset_1 - this is name generated automatically based on our local.yml
49 |
50 | If you like to load examples:
51 | `docker exec -it django-demo-project_superset_1 superset load_examples`
52 |
53 | #### Connecting superset with external postgres
54 |
55 |
56 | In datasources, add following SQLAlchemy URI :
57 | `postgresql://debug:debug@postgres:5432/demoapp`
58 |
59 | ##### Command line connection to docker postgres
60 | `psql -h localhost -U debug -d demoapp`
61 |
62 |
63 | Test coverage
64 | ---
65 |
66 | To run the tests, check your test coverage, and generate an HTML coverage report::
67 |
68 | $ coverage run manage.py test
69 | $ coverage html
70 | $ open htmlcov/index.html
71 |
72 | Running tests with py.test
73 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
74 |
75 | ::
76 |
77 | $ py.test
78 |
79 | Live reloading and Sass CSS compilation
80 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
81 |
82 | Moved to `Live reloading and SASS compilation`_.
83 |
84 | .. _`Live reloading and SASS compilation`: http://cookiecutter-django.readthedocs.io/en/latest/live-reloading-and-sass-compilation.html
85 |
86 |
87 |
88 |
89 | Email Server
90 | ^^^^^^^^^^^^
91 |
92 | In development, it is often nice to be able to see emails that are being sent from your application. For that reason local SMTP server `MailHog`_ with a web interface is available as docker container.
93 |
94 | Container mailhog will start automatically when you will run all docker containers.
95 | Please check `cookiecutter-django Docker documentation`_ for more details how to start all containers.
96 |
97 | With MailHog running, to view messages that are sent by your application, open your browser and go to ``http://127.0.0.1:8025``
98 |
99 | .. _mailhog: https://github.com/mailhog/MailHog
100 |
101 |
102 |
103 | Deployment
104 | ----------
105 |
106 | The following details how to deploy this application.
107 |
108 |
109 |
110 | Docker
111 | ^^^^^^
112 |
113 | See detailed `cookiecutter-django Docker documentation`_.
114 |
115 | .. _`cookiecutter-django Docker documentation`: http://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Python template
2 | # Byte-compiled / optimized / DLL files
3 | __pycache__/
4 | *.py[cod]
5 | *$py.class
6 |
7 | # C extensions
8 | *.so
9 |
10 | # Distribution / packaging
11 | .Python
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | staticfiles/
55 |
56 | # Sphinx documentation
57 | docs/_build/
58 |
59 | # PyBuilder
60 | target/
61 |
62 | # pyenv
63 | .python-version
64 |
65 |
66 |
67 | # Environments
68 | .venv
69 | venv/
70 | ENV/
71 |
72 | # Rope project settings
73 | .ropeproject
74 |
75 | # mkdocs documentation
76 | /site
77 |
78 | # mypy
79 | .mypy_cache/
80 |
81 |
82 | ### Node template
83 | # Logs
84 | logs
85 | *.log
86 | npm-debug.log*
87 | yarn-debug.log*
88 | yarn-error.log*
89 |
90 | # Runtime data
91 | pids
92 | *.pid
93 | *.seed
94 | *.pid.lock
95 |
96 | # Directory for instrumented libs generated by jscoverage/JSCover
97 | lib-cov
98 |
99 | # Coverage directory used by tools like istanbul
100 | coverage
101 |
102 | # nyc test coverage
103 | .nyc_output
104 |
105 | # Bower dependency directory (https://bower.io/)
106 | bower_components
107 |
108 | # node-waf configuration
109 | .lock-wscript
110 |
111 | # Compiled binary addons (http://nodejs.org/api/addons.html)
112 | build/Release
113 |
114 | # Dependency directories
115 | node_modules/
116 | jspm_packages/
117 |
118 | # Typescript v1 declaration files
119 | typings/
120 |
121 | # Optional npm cache directory
122 | .npm
123 |
124 | # Optional eslint cache
125 | .eslintcache
126 |
127 | # Optional REPL history
128 | .node_repl_history
129 |
130 | # Output of 'npm pack'
131 | *.tgz
132 |
133 | # Yarn Integrity file
134 | .yarn-integrity
135 |
136 |
137 | ### Linux template
138 | *~
139 |
140 | # temporary files which can be created if a process still has a handle open of a deleted file
141 | .fuse_hidden*
142 |
143 | # KDE directory preferences
144 | .directory
145 |
146 | # Linux trash folder which might appear on any partition or disk
147 | .Trash-*
148 |
149 | # .nfs files are created when an open file is removed but is still being accessed
150 | .nfs*
151 |
152 |
153 | ### VisualStudioCode template
154 | .vscode/*
155 | !.vscode/settings.json
156 | !.vscode/tasks.json
157 | !.vscode/launch.json
158 | !.vscode/extensions.json
159 |
160 |
161 |
162 |
163 |
164 | ### Windows template
165 | # Windows thumbnail cache files
166 | Thumbs.db
167 | ehthumbs.db
168 | ehthumbs_vista.db
169 |
170 | # Dump file
171 | *.stackdump
172 |
173 | # Folder config file
174 | Desktop.ini
175 |
176 | # Recycle Bin used on file shares
177 | $RECYCLE.BIN/
178 |
179 | # Windows Installer files
180 | *.cab
181 | *.msi
182 | *.msm
183 | *.msp
184 |
185 | # Windows shortcuts
186 | *.lnk
187 |
188 |
189 | ### macOS template
190 | # General
191 | *.DS_Store
192 | .AppleDouble
193 | .LSOverride
194 |
195 | # Icon must end with two \r
196 | Icon
197 |
198 | # Thumbnails
199 | ._*
200 |
201 | # Files that might appear in the root of a volume
202 | .DocumentRevisions-V100
203 | .fseventsd
204 | .Spotlight-V100
205 | .TemporaryItems
206 | .Trashes
207 | .VolumeIcon.icns
208 | .com.apple.timemachine.donotpresent
209 |
210 | # Directories potentially created on remote AFP share
211 | .AppleDB
212 | .AppleDesktop
213 | Network Trash Folder
214 | Temporary Items
215 | .apdisk
216 |
217 |
218 | ### SublimeText template
219 | # Cache files for Sublime Text
220 | *.tmlanguage.cache
221 | *.tmPreferences.cache
222 | *.stTheme.cache
223 |
224 | # Workspace files are user-specific
225 | *.sublime-workspace
226 |
227 | # Project files should be checked into the repository, unless a significant
228 | # proportion of contributors will probably not be using Sublime Text
229 | # *.sublime-project
230 |
231 | # SFTP configuration file
232 | sftp-config.json
233 |
234 | # Package control specific files
235 | Package Control.last-run
236 | Package Control.ca-list
237 | Package Control.ca-bundle
238 | Package Control.system-ca-bundle
239 | Package Control.cache/
240 | Package Control.ca-certs/
241 | Package Control.merged-ca-bundle
242 | Package Control.user-ca-bundle
243 | oscrypto-ca-bundle.crt
244 | bh_unicode_properties.cache
245 |
246 | # Sublime-github package stores a github token in this file
247 | # https://packagecontrol.io/packages/sublime-github
248 | GitHub.sublime-settings
249 |
250 |
251 | ### Vim template
252 | # Swap
253 | [._]*.s[a-v][a-z]
254 | [._]*.sw[a-p]
255 | [._]s[a-v][a-z]
256 | [._]sw[a-p]
257 |
258 | # Session
259 | Session.vim
260 |
261 | # Temporary
262 | .netrwhist
263 |
264 | # Auto-generated tag files
265 | tags
266 |
267 |
268 | ### Project template
269 |
270 | demoapp/media/
271 | .env
272 | .envs/*
273 | !.envs/.local/
274 |
--------------------------------------------------------------------------------
/demoapp/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static i18n %}
2 |
3 |
4 |
5 |
6 | {% block title %}DemoApp{% endblock title %}
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 | {% block css %}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {% endblock %}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
77 |
78 |
79 |
80 |
81 |
82 | {% if messages %}
83 | {% for message in messages %}
84 |