├── polls ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── views.py ├── apps.py ├── models.py └── admin.py ├── domains ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ └── createdomain.py ├── migrations │ ├── __init__.py │ ├── 0001_initial.py │ └── 0002_auto_20181002_0149.py ├── tests.py ├── views.py ├── apps.py ├── admin.py ├── middleware.py └── models.py ├── dynamicdomains ├── __init__.py ├── wsgi.py ├── urls.py └── settings.py ├── .gitignore └── manage.py /polls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /domains/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dynamicdomains/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polls/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /domains/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /domains/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /domains/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /polls/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /domains/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /polls/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /polls/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PollsConfig(AppConfig): 5 | name = 'polls' 6 | -------------------------------------------------------------------------------- /domains/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DomainsConfig(AppConfig): 5 | name = 'domains' 6 | -------------------------------------------------------------------------------- /domains/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Domain 4 | 5 | 6 | class DomainAdmin(admin.ModelAdmin): 7 | fields = ('domain', 'name') 8 | ordering = ('domain',) 9 | 10 | 11 | admin.site.register(Domain, DomainAdmin) 12 | -------------------------------------------------------------------------------- /domains/middleware.py: -------------------------------------------------------------------------------- 1 | from django.utils.deprecation import MiddlewareMixin 2 | 3 | 4 | class CurrentDomainMiddleware(MiddlewareMixin): 5 | def process_request(self, request): 6 | from .models import Domain 7 | request.domain = Domain.objects.get_current(request) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # Installer logs 9 | pip-log.txt 10 | pip-delete-this-directory.txt 11 | 12 | 13 | # Django stuff: 14 | *.log 15 | local_settings.py 16 | db.sqlite3 17 | 18 | .env 19 | /venv 20 | /uploads/ 21 | /staticfiles/ 22 | -------------------------------------------------------------------------------- /dynamicdomains/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for dynamicdomains project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dynamicdomains.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /polls/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from domains.models import Domain 4 | 5 | 6 | class Poll(models.Model): 7 | content = models.CharField(max_length=256) 8 | domain = models.ForeignKey(Domain, on_delete=models.CASCADE) 9 | 10 | def __str__(self): 11 | return self.content 12 | 13 | 14 | class PollOption(models.Model): 15 | poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name='poll_options') 16 | value = models.CharField(max_length=256) 17 | 18 | def __str__(self): 19 | return self.value 20 | -------------------------------------------------------------------------------- /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', 'dynamicdomains.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /domains/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-01 20:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Domain', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('domain', models.CharField(max_length=128, unique=True)), 19 | ('name', models.CharField(max_length=128)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /domains/migrations/0002_auto_20181002_0149.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-01 20:19 2 | 3 | from django.db import migrations, models 4 | import domains.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('domains', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelManagers( 15 | name='domain', 16 | managers=[ 17 | ('objects', domains.models.DomainManager()), 18 | ], 19 | ), 20 | migrations.AlterField( 21 | model_name='domain', 22 | name='domain', 23 | field=models.CharField(max_length=128, unique=True, validators=[domains.models._simple_domain_name_validator]), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /dynamicdomains/urls.py: -------------------------------------------------------------------------------- 1 | """dynamicdomains URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /polls/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from polls.models import PollOption, Poll 4 | 5 | 6 | class PollOptionInline(admin.TabularInline): 7 | model = PollOption 8 | fields = ('value',) 9 | extra = 1 10 | 11 | 12 | class PollAdmin(admin.ModelAdmin): 13 | inlines = (PollOptionInline,) 14 | fields = ('content', 'domain',) 15 | readonly_fields = ('domain',) 16 | 17 | def get_queryset(self, request): 18 | qs = Poll.objects.filter(domain=request.domain) 19 | ordering = self.get_ordering(request) 20 | if ordering: 21 | qs = qs.order_by(*ordering) 22 | return qs 23 | 24 | def save_model(self, request, obj, form, change): 25 | obj.domain = request.domain 26 | return super().save_model(request, obj, form, change) 27 | 28 | 29 | admin.site.register(Poll, PollAdmin) 30 | -------------------------------------------------------------------------------- /polls/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-01 20:19 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('domains', '0002_auto_20181002_0149'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Poll', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('content', models.CharField(max_length=256)), 21 | ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='domains.Domain')), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='PollOption', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('value', models.CharField(max_length=256)), 29 | ('poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='poll_options', to='polls.Poll')), 30 | ], 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /domains/models.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from django.core.exceptions import ValidationError 4 | from django.db import models 5 | from django.http.request import split_domain_port 6 | 7 | DOMAINS_CACHE = {} 8 | 9 | 10 | def _simple_domain_name_validator(value): 11 | if not value: 12 | return 13 | checks = ((s in value) for s in string.whitespace) 14 | if any(checks): 15 | raise ValidationError("The domain name cannot contain any spaces or tabs.", 16 | code='invalid') 17 | 18 | 19 | class DomainManager(models.Manager): 20 | use_in_migrations = True 21 | 22 | def _get_domain_by_id(self, domain_id): 23 | if domain_id not in DOMAINS_CACHE: 24 | domain = self.get(pk=domain_id) 25 | DOMAINS_CACHE[domain_id] = domain 26 | return DOMAINS_CACHE[domain_id] 27 | 28 | def _get_domain_by_request(self, request): 29 | host = request.get_host() 30 | try: 31 | if host not in DOMAINS_CACHE: 32 | DOMAINS_CACHE[host] = self.get(domain__iexact=host) 33 | return DOMAINS_CACHE[host] 34 | except Domain.DoesNotExist: 35 | domain, port = split_domain_port(host) 36 | if domain not in DOMAINS_CACHE: 37 | DOMAINS_CACHE[domain] = self.get(domain__iexact=domain) 38 | return DOMAINS_CACHE[domain] 39 | 40 | def get_current(self, request=None, domain_id=None): 41 | if domain_id: 42 | return self._get_domain_by_id(domain_id) 43 | elif request: 44 | return self._get_domain_by_request(request) 45 | 46 | def clear_cache(self): 47 | global DOMAINS_CACHE 48 | DOMAINS_CACHE = {} 49 | 50 | def get_by_natural_key(self, domain): 51 | return self.get(domain=domain) 52 | 53 | 54 | class Domain(models.Model): 55 | domain = models.CharField(max_length=128, unique=True, validators=[_simple_domain_name_validator]) 56 | name = models.CharField(max_length=128) 57 | 58 | objects = DomainManager() 59 | 60 | def __str__(self): 61 | return self.domain 62 | -------------------------------------------------------------------------------- /dynamicdomains/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for dynamicdomains project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'k^at-1%b=-r(y$z11xn(uzvf0$-*xqp1&i^68hzt(7u2n8s*5o' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'domains', 42 | 43 | 'polls', 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'domains.middleware.CurrentDomainMiddleware', 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'dynamicdomains.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'dynamicdomains.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.sqlite3', 84 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 104 | }, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'en-us' 112 | 113 | TIME_ZONE = 'UTC' 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 124 | 125 | STATIC_URL = '/static/' 126 | -------------------------------------------------------------------------------- /domains/management/commands/createdomain.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.core import exceptions 4 | from django.core.management.base import BaseCommand, CommandError 5 | from django.db import DEFAULT_DB_ALIAS 6 | from django.utils.text import capfirst 7 | 8 | from domains.models import Domain 9 | 10 | 11 | class NotRunningInTTYException(Exception): 12 | pass 13 | 14 | 15 | class Command(BaseCommand): 16 | help = 'Used to create a domain.' 17 | requires_migrations_checks = True 18 | stealth_options = ('stdin',) 19 | 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | self.DomainModel = Domain 23 | self.domain_field = self.DomainModel._meta.get_field('domain') 24 | self.name_field = self.DomainModel._meta.get_field('name') 25 | 26 | def add_arguments(self, parser): 27 | parser.add_argument( 28 | '--%s' % 'domain', 29 | dest='domain', default=None, 30 | help='Specifies the domain.', 31 | ) 32 | parser.add_argument( 33 | '--database', action='store', dest='database', 34 | default=DEFAULT_DB_ALIAS, 35 | help='Specifies the database to use. Default is "default".', 36 | ) 37 | parser.add_argument( 38 | '--%s' % 'name', dest='name', default=None, 39 | help='Specifies the name', 40 | ) 41 | 42 | def execute(self, *args, **options): 43 | self.stdin = options.get('stdin', sys.stdin) # Used for testing 44 | return super().execute(*args, **options) 45 | 46 | def handle(self, *args, **options): 47 | domain = options['domain'] 48 | name = options['name'] 49 | database = options['database'] 50 | 51 | verbose_domain_field_name = self.domain_field.verbose_name 52 | verbose_name_field_name = self.name_field.verbose_name 53 | 54 | # Enclose this whole thing in a try/except to catch 55 | # KeyboardInterrupt and exit gracefully. 56 | try: 57 | 58 | if hasattr(self.stdin, 'isatty') and not self.stdin.isatty(): 59 | raise NotRunningInTTYException("Not running in a TTY") 60 | 61 | while domain is None: 62 | input_msg = '%s: ' % capfirst(verbose_domain_field_name) 63 | domain = self.get_input_data(self.domain_field, input_msg) 64 | if not domain: 65 | continue 66 | if self.domain_field.unique: 67 | try: 68 | self.DomainModel._default_manager.db_manager(database).get_by_natural_key(domain) 69 | except self.DomainModel.DoesNotExist: 70 | pass 71 | else: 72 | self.stderr.write("Error: That %s is already in use." % verbose_domain_field_name) 73 | domain = None 74 | 75 | if not domain: 76 | raise CommandError('%s cannot be blank.' % capfirst(verbose_domain_field_name)) 77 | 78 | while name is None: 79 | input_msg = '%s: ' % capfirst(verbose_name_field_name) 80 | name = self.get_input_data(self.name_field, input_msg) 81 | if not name: 82 | continue 83 | 84 | if not name: 85 | raise CommandError('%s cannot be blank.' % capfirst(verbose_domain_field_name)) 86 | 87 | except KeyboardInterrupt: 88 | self.stderr.write("\nOperation cancelled.") 89 | sys.exit(1) 90 | 91 | except NotRunningInTTYException: 92 | self.stdout.write( 93 | "Domain creation skipped due to not running in a TTY. " 94 | ) 95 | 96 | if domain and name: 97 | domain_data = { 98 | 'domain': domain, 99 | 'name': name, 100 | } 101 | self.DomainModel(**domain_data).save() 102 | if options['verbosity'] >= 1: 103 | self.stdout.write("Domain created successfully.") 104 | 105 | def get_input_data(self, field, message, default=None): 106 | """ 107 | Override this method if you want to customize data inputs or 108 | validation exceptions. 109 | """ 110 | raw_value = input(message) 111 | if default and raw_value == '': 112 | raw_value = default 113 | try: 114 | val = field.clean(raw_value, None) 115 | except exceptions.ValidationError as e: 116 | self.stderr.write("Error: %s" % '; '.join(e.messages)) 117 | val = None 118 | 119 | return val 120 | --------------------------------------------------------------------------------