From cbcf9dddc36125852482b28c43f0fd0b850caa44 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 22 Mar 2014 15:49:05 +0100 Subject: [PATCH] Allow add domain aliases. This implies some minor refactor of domains app code. --- taiga/base/domains/api.py | 2 +- taiga/base/domains/base.py | 75 ++++++++++++++++ taiga/base/domains/middleware.py | 25 ++++-- .../0006_auto__add_field_domain_alias_of.py | 89 +++++++++++++++++++ taiga/base/domains/models.py | 5 +- 5 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 taiga/base/domains/base.py create mode 100644 taiga/base/domains/migrations/0006_auto__add_field_domain_alias_of.py diff --git a/taiga/base/domains/api.py b/taiga/base/domains/api.py index d9eb06d2..3a2853b2 100644 --- a/taiga/base/domains/api.py +++ b/taiga/base/domains/api.py @@ -7,8 +7,8 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from django.http import Http404 from taiga.base.api import ModelCrudViewSet, UpdateModelMixin -from taiga.base.domains import get_active_domain +from .base import get_active_domain from .serializers import DomainSerializer, DomainMemberSerializer from .permissions import DomainMembersPermission, DomainPermission from .models import DomainMember, Domain diff --git a/taiga/base/domains/base.py b/taiga/base/domains/base.py new file mode 100644 index 00000000..2b2d4243 --- /dev/null +++ b/taiga/base/domains/base.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +import logging +import functools +import threading + +from django.db.models import get_model +from django.core.exceptions import ImproperlyConfigured +from django.utils.translation import ugettext_lazy as _ + +from taiga.base import exceptions as exc +log = logging.getLogger("taiga.domains") + +_local = threading.local() + + +class DomainNotFound(exc.BaseException): + pass + + +@functools.lru_cache(maxsize=1) +def get_default_domain(): + from django.conf import settings + try: + sid = settings.DOMAIN_ID + except AttributeError: + raise ImproperlyConfigured("You're using the \"domains framework\" without having " + "set the DOMAIN_ID setting. Create a domain in your database " + "and set the DOMAIN_ID setting to fix this error.") + + model_cls = get_model("domains", "Domain") + try: + return model_cls.objects.get(pk=sid) + except model_cls.DoesNotExist: + raise ImproperlyConfigured("default domain not found on database.") + +@functools.lru_cache(maxsize=100, typed=True) +def get_domain_for_domain_name(domain:str, follow_alias:bool=True): + log.debug("Trying activate domain for domain name: {}".format(domain)) + + model_cls = get_model("domains", "Domain") + + try: + domain = model_cls.objects.get(domain=domain) + except model_cls.DoesNotExist: + log.warning("Domain does not exist for domain: {}".format(domain)) + raise DomainNotFound(_("domain not found")) + + # Use `alias_of_id` instead of simple `alias_of` for performace reasons. + if domain.alias_of_id is None or not follow_alias: + return domain + + return domain.alias_of + +def activate(domain): + log.debug("Activating domain: {}".format(domain)) + _local.active_domain = domain + + +def deactivate(): + if hasattr(_local, "active_domain"): + log.debug("Deactivating domain: {}".format(_local.active_domain)) + del _local.active_domain + + +def get_active_domain(): + active_domain = getattr(_local, "active_domain", None) + if active_domain is None: + return get_default_domain() + return active_domain + + +def clear_domain_cache(**kwargs): + get_default_domain.cache_clear() + get_domain_for_domain_name.cache_clear() diff --git a/taiga/base/domains/middleware.py b/taiga/base/domains/middleware.py index 36931857..d6f3b197 100644 --- a/taiga/base/domains/middleware.py +++ b/taiga/base/domains/middleware.py @@ -1,29 +1,38 @@ -# -*- coding: utf-8 -*- - import json from django import http -from taiga.base import domains from taiga.base.exceptions import format_exception +from .base import get_domain_for_domain_name +from .base import activate as activate_domain +from .base import deactivate as deactivate_domain +from .base import get_default_domain +from .base import DomainNotFound + class DomainsMiddleware(object): + """ + Domain middlewate: process request and try resolve domain + from HTTP_X_HOST header. If no header is specified, one + default is used. + """ + def process_request(self, request): domain = request.META.get("HTTP_X_HOST", None) if domain is not None: try: - domain = domains.get_domain_for_domain_name(domain) - except domains.DomainNotFound as e: + domain = get_domain_for_domain_name(domain, follow_alias=True) + except DomainNotFound as e: detail = format_exception(e) return http.HttpResponseBadRequest(json.dumps(detail)) else: - domain = domains.get_default_domain() + domain = get_default_domain() request.domain = domain - domains.activate(domain) + activate_domain(domain) def process_response(self, request, response): - domains.deactivate() + deactivate_domain() if hasattr(request, "domain"): response["X-Site-Host"] = request.domain.domain diff --git a/taiga/base/domains/migrations/0006_auto__add_field_domain_alias_of.py b/taiga/base/domains/migrations/0006_auto__add_field_domain_alias_of.py new file mode 100644 index 00000000..b78cb4c2 --- /dev/null +++ b/taiga/base/domains/migrations/0006_auto__add_field_domain_alias_of.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Domain.alias_of' + db.add_column('domains_domain', 'alias_of', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['domains.Domain'], default=None, blank=True, null=True, related_name='+'), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Domain.alias_of' + db.delete_column('domains_domain', 'alias_of_id') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'object_name': 'Permission', 'unique_together': "(('content_type', 'codename'),)", 'ordering': "('content_type__app_label', 'content_type__model', 'codename')"}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'object_name': 'ContentType', 'unique_together': "(('app_label', 'model'),)", 'ordering': "('name',)", 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'domains.domain': { + 'Meta': {'object_name': 'Domain', 'ordering': "('domain',)"}, + 'alias_of': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['domains.Domain']", 'default': 'None', 'blank': 'True', 'null': 'True', 'related_name': "'+'"}), + 'default_language': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '20'}), + 'domain': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'public_register': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scheme': ('django.db.models.fields.CharField', [], {'default': 'None', 'null': 'True', 'max_length': '60'}) + }, + 'domains.domainmember': { + 'Meta': {'object_name': 'DomainMember', 'unique_together': "(('domain', 'user'),)", 'ordering': "['email']"}, + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['domains.Domain']", 'null': 'True', 'related_name': "'members'"}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.User']", 'null': 'True', 'related_name': "'+'"}) + }, + 'users.user': { + 'Meta': {'object_name': 'User', 'ordering': "['username']"}, + 'color': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "'#28261c'", 'max_length': '9'}), + 'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'default_language': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '20'}), + 'default_timezone': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '20'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '75'}), + 'first_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '30'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True', 'symmetrical': 'False', 'related_name': "'user_set'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '30'}), + 'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'notify_level': ('django.db.models.fields.CharField', [], {'default': "'all_owned_projects'", 'max_length': '32'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'photo': ('django.db.models.fields.files.FileField', [], {'blank': 'True', 'null': 'True', 'max_length': '500'}), + 'token': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': 'None', 'null': 'True', 'max_length': '200'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False', 'related_name': "'user_set'"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + } + } + + complete_apps = ['domains'] \ No newline at end of file diff --git a/taiga/base/domains/models.py b/taiga/base/domains/models.py index 1f5ab093..eabce8d4 100644 --- a/taiga/base/domains/models.py +++ b/taiga/base/domains/models.py @@ -8,7 +8,7 @@ from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError -from . import clear_domain_cache +from .base import clear_domain_cache def _simple_domain_name_validator(value): @@ -38,6 +38,9 @@ class Domain(models.Model): default_language = models.CharField(max_length=20, null=False, blank=True, default="", verbose_name=_("default language")) + alias_of = models.ForeignKey("self", null=True, default=None, blank=True, + verbose_name=_("Mark as alias of"), related_name="+") + class Meta: verbose_name = _('domain') verbose_name_plural = _('domain')