django-multisite2 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_multisite2-2.1.1.dist-info/LICENSE +28 -0
- django_multisite2-2.1.1.dist-info/METADATA +244 -0
- django_multisite2-2.1.1.dist-info/RECORD +138 -0
- django_multisite2-2.1.1.dist-info/WHEEL +5 -0
- django_multisite2-2.1.1.dist-info/top_level.txt +2 -0
- multisite/__init__.py +2 -0
- multisite/__pycache__/__init__.cpython-310.pyc +0 -0
- multisite/__pycache__/__init__.cpython-311.pyc +0 -0
- multisite/__pycache__/__init__.cpython-312.pyc +0 -0
- multisite/__pycache__/__init__.cpython-39.pyc +0 -0
- multisite/__pycache__/__version__.cpython-310.pyc +0 -0
- multisite/__pycache__/__version__.cpython-311.pyc +0 -0
- multisite/__pycache__/__version__.cpython-312.pyc +0 -0
- multisite/__pycache__/__version__.cpython-39.pyc +0 -0
- multisite/__pycache__/apps.cpython-312.pyc +0 -0
- multisite/__pycache__/exceptions.cpython-312.pyc +0 -0
- multisite/__pycache__/forms.cpython-310.pyc +0 -0
- multisite/__pycache__/forms.cpython-311.pyc +0 -0
- multisite/__pycache__/forms.cpython-312.pyc +0 -0
- multisite/__pycache__/forms.cpython-39.pyc +0 -0
- multisite/__pycache__/hacks.cpython-310.pyc +0 -0
- multisite/__pycache__/hacks.cpython-311.pyc +0 -0
- multisite/__pycache__/hacks.cpython-312.pyc +0 -0
- multisite/__pycache__/hacks.cpython-39.pyc +0 -0
- multisite/__pycache__/test_settings.cpython-310-pytest-6.2.5.pyc +0 -0
- multisite/__pycache__/test_settings.cpython-311-pytest-8.1.1.pyc +0 -0
- multisite/__pycache__/test_settings.cpython-312-pytest-7.4.4.pyc +0 -0
- multisite/__pycache__/test_settings.cpython-312-pytest-8.1.1.pyc +0 -0
- multisite/__pycache__/test_settings.cpython-39-pytest-6.2.5.pyc +0 -0
- multisite/__pycache__/tests.cpython-310-pytest-6.2.5.pyc +0 -0
- multisite/__pycache__/tests.cpython-311-pytest-8.1.1.pyc +0 -0
- multisite/__pycache__/tests.cpython-312-pytest-7.4.4.pyc +0 -0
- multisite/__pycache__/tests.cpython-312-pytest-8.1.1.pyc +0 -0
- multisite/__pycache__/tests.cpython-39-pytest-6.2.5.pyc +0 -0
- multisite/__pycache__/threadlocals.cpython-310.pyc +0 -0
- multisite/__pycache__/threadlocals.cpython-311.pyc +0 -0
- multisite/__pycache__/threadlocals.cpython-312.pyc +0 -0
- multisite/__pycache__/threadlocals.cpython-39.pyc +0 -0
- multisite/__pycache__/utils.cpython-312.pyc +0 -0
- multisite/__version__.py +1 -0
- multisite/admin/__init__.py +3 -0
- multisite/admin/__pycache__/__init__.cpython-312.pyc +0 -0
- multisite/admin/__pycache__/alias_admin.cpython-312.pyc +0 -0
- multisite/admin/__pycache__/multisite_changelist.cpython-312.pyc +0 -0
- multisite/admin/__pycache__/multisite_model_admin.cpython-312.pyc +0 -0
- multisite/admin/alias_admin.py +58 -0
- multisite/admin/multisite_changelist.py +64 -0
- multisite/admin/multisite_model_admin.py +161 -0
- multisite/apps.py +29 -0
- multisite/exceptions.py +57 -0
- multisite/forms.py +22 -0
- multisite/hacks.py +163 -0
- multisite/management/__init__.py +0 -0
- multisite/management/__pycache__/__init__.cpython-310.pyc +0 -0
- multisite/management/__pycache__/__init__.cpython-311.pyc +0 -0
- multisite/management/__pycache__/__init__.cpython-312.pyc +0 -0
- multisite/management/__pycache__/__init__.cpython-39.pyc +0 -0
- multisite/management/commands/__init__.py +0 -0
- multisite/management/commands/__pycache__/__init__.cpython-310.pyc +0 -0
- multisite/management/commands/__pycache__/__init__.cpython-311.pyc +0 -0
- multisite/management/commands/__pycache__/__init__.cpython-312.pyc +0 -0
- multisite/management/commands/__pycache__/__init__.cpython-39.pyc +0 -0
- multisite/management/commands/__pycache__/update_public_suffix_list.cpython-310.pyc +0 -0
- multisite/management/commands/__pycache__/update_public_suffix_list.cpython-311.pyc +0 -0
- multisite/management/commands/__pycache__/update_public_suffix_list.cpython-312.pyc +0 -0
- multisite/management/commands/__pycache__/update_public_suffix_list.cpython-39.pyc +0 -0
- multisite/management/commands/update_public_suffix_list.py +33 -0
- multisite/middleware/__init__.py +4 -0
- multisite/middleware/__pycache__/__init__.cpython-312.pyc +0 -0
- multisite/middleware/__pycache__/cookie_domain_middleware.cpython-312.pyc +0 -0
- multisite/middleware/__pycache__/dynamic_site_middleware.cpython-312.pyc +0 -0
- multisite/middleware/cookie_domain_middleware.py +72 -0
- multisite/middleware/dynamic_site_middleware.py +195 -0
- multisite/migrations/0001_initial.py +71 -0
- multisite/migrations/0002_alter_alias_id_alter_alias_is_canonical.py +32 -0
- multisite/migrations/0003_alter_alias_options_alter_alias_unique_together_and_more.py +41 -0
- multisite/migrations/__init__.py +0 -0
- multisite/migrations/__pycache__/0001_initial.cpython-310.pyc +0 -0
- multisite/migrations/__pycache__/0001_initial.cpython-311.pyc +0 -0
- multisite/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- multisite/migrations/__pycache__/0001_initial.cpython-39.pyc +0 -0
- multisite/migrations/__pycache__/0002_alter_alias_id_alter_alias_is_canonical.cpython-310.pyc +0 -0
- multisite/migrations/__pycache__/0002_alter_alias_id_alter_alias_is_canonical.cpython-311.pyc +0 -0
- multisite/migrations/__pycache__/0002_alter_alias_id_alter_alias_is_canonical.cpython-312.pyc +0 -0
- multisite/migrations/__pycache__/0002_alter_alias_id_alter_alias_is_canonical.cpython-39.pyc +0 -0
- multisite/migrations/__pycache__/0003_alias_multisite_alias_canon_site_uniq.cpython-311.pyc +0 -0
- multisite/migrations/__pycache__/0003_alter_alias_options_alter_alias_unique_together_and_more.cpython-312.pyc +0 -0
- multisite/migrations/__pycache__/__init__.cpython-310.pyc +0 -0
- multisite/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- multisite/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- multisite/migrations/__pycache__/__init__.cpython-39.pyc +0 -0
- multisite/models/__init__.py +3 -0
- multisite/models/__pycache__/__init__.cpython-312.pyc +0 -0
- multisite/models/__pycache__/alias.cpython-312.pyc +0 -0
- multisite/models/__pycache__/managers.cpython-312.pyc +0 -0
- multisite/models/__pycache__/signals.cpython-312.pyc +0 -0
- multisite/models/__pycache__/validators.cpython-312.pyc +0 -0
- multisite/models/alias.py +110 -0
- multisite/models/managers.py +196 -0
- multisite/models/signals.py +36 -0
- multisite/models/validators.py +14 -0
- multisite/tests/__init__.py +0 -0
- multisite/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- multisite/tests/__pycache__/get_test_allowed_hosts.cpython-312.pyc +0 -0
- multisite/tests/__pycache__/get_test_http_response.cpython-312.pyc +0 -0
- multisite/tests/__pycache__/test_settings.cpython-312-pytest-8.1.1.pyc +0 -0
- multisite/tests/__pycache__/test_settings.cpython-312.pyc +0 -0
- multisite/tests/__pycache__/tests.cpython-312-pytest-8.1.1.pyc +0 -0
- multisite/tests/get_test_allowed_hosts.py +14 -0
- multisite/tests/get_test_http_response.py +13 -0
- multisite/tests/hosts.py +46 -0
- multisite/tests/test_settings.py +23 -0
- multisite/tests/tests/__init__.py +0 -0
- multisite/tests/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- multisite/tests/tests/__pycache__/request_factory.cpython-312.pyc +0 -0
- multisite/tests/tests/__pycache__/test_alias.cpython-312.pyc +0 -0
- multisite/tests/tests/__pycache__/test_contrib_site.cpython-312.pyc +0 -0
- multisite/tests/tests/__pycache__/test_cookie_domain_middleware.cpython-312.pyc +0 -0
- multisite/tests/tests/__pycache__/test_dynamic_site_middleware.cpython-312.pyc +0 -0
- multisite/tests/tests/__pycache__/test_site_cache.cpython-312.pyc +0 -0
- multisite/tests/tests/__pycache__/test_site_domain.cpython-312.pyc +0 -0
- multisite/tests/tests/__pycache__/test_site_id.cpython-312.pyc +0 -0
- multisite/tests/tests/__pycache__/test_template_loader.cpython-312.pyc +0 -0
- multisite/tests/tests/request_factory.py +12 -0
- multisite/tests/tests/test_alias.py +272 -0
- multisite/tests/tests/test_contrib_site.py +21 -0
- multisite/tests/tests/test_cookie_domain_middleware.py +196 -0
- multisite/tests/tests/test_dynamic_site_middleware.py +262 -0
- multisite/tests/tests/test_site_cache.py +127 -0
- multisite/tests/tests/test_site_domain.py +24 -0
- multisite/tests/tests/test_site_id.py +76 -0
- multisite/tests/tests/test_template_loader.py +49 -0
- multisite/threadlocals.py +149 -0
- multisite/utils.py +165 -0
- multisite_app/__init__.py +0 -0
- multisite_app/apps.py +6 -0
- multisite_app/urls.py +5 -0
- multisite_app/views.py +8 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import operator
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from django.contrib.sites import managers
|
|
8
|
+
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
|
9
|
+
from django.core.validators import validate_ipv4_address
|
|
10
|
+
from django.db import models
|
|
11
|
+
from django.db.models import Q
|
|
12
|
+
from django.db.models.constants import LOOKUP_SEP
|
|
13
|
+
|
|
14
|
+
from ..exceptions import MultisiteInvalidHostError
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from django.db.models import QuerySet
|
|
18
|
+
|
|
19
|
+
from .alias import Alias
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AliasManager",
|
|
23
|
+
"CanonicalAliasManager",
|
|
24
|
+
"NotCanonicalAliasManager",
|
|
25
|
+
"SpanningCurrentSiteManager",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AliasManager(models.Manager):
|
|
30
|
+
"""Manager for all Aliases."""
|
|
31
|
+
|
|
32
|
+
use_in_migrations = True
|
|
33
|
+
|
|
34
|
+
def get_queryset(self) -> QuerySet[Alias]:
|
|
35
|
+
return super().get_queryset().select_related("site")
|
|
36
|
+
|
|
37
|
+
def resolve(self, host: str, port: str | None = None) -> list[str] | None:
|
|
38
|
+
"""Returns the Alias that best matches ``host`` and
|
|
39
|
+
``port``, or None.
|
|
40
|
+
|
|
41
|
+
``host`` is a hostname like ``'example.com'``.
|
|
42
|
+
``port`` is a port number like 8000, or None.
|
|
43
|
+
|
|
44
|
+
Attempts to first match by 'host:port' against
|
|
45
|
+
Alias.domain. If that fails, it will try to match the bare
|
|
46
|
+
'host' with no port number.
|
|
47
|
+
|
|
48
|
+
All comparisons are done case-insensitively.
|
|
49
|
+
"""
|
|
50
|
+
domains = self._expand_netloc(host=host, port=port)
|
|
51
|
+
q: Q = reduce(operator.or_, (Q(domain__iexact=d) for d in domains))
|
|
52
|
+
aliases = dict((a.domain, a) for a in self.get_queryset().filter(q))
|
|
53
|
+
for domain in domains:
|
|
54
|
+
try:
|
|
55
|
+
return aliases[domain]
|
|
56
|
+
except KeyError:
|
|
57
|
+
pass
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def _expand_netloc(cls, host: str, port: str | None = None) -> list[str]:
|
|
62
|
+
"""Returns a list of possible domain expansions for ``host``
|
|
63
|
+
and ``port``.
|
|
64
|
+
|
|
65
|
+
``host`` is a hostname like ``'example.com'``.
|
|
66
|
+
``port`` is a port number like 8000, or None.
|
|
67
|
+
|
|
68
|
+
Expansions are ordered from highest to lowest preference and may
|
|
69
|
+
include wildcards. Examples::
|
|
70
|
+
|
|
71
|
+
>>> AliasManager._expand_netloc('www.example.com')
|
|
72
|
+
['www.example.com', '*.example.com', '*.com', '*']
|
|
73
|
+
|
|
74
|
+
>>> AliasManager._expand_netloc('www.example.com', 80)
|
|
75
|
+
['www.example.com:80', 'www.example.com',
|
|
76
|
+
'*.example.com:80', '*.example.com',
|
|
77
|
+
'*.com:80', '*.com',
|
|
78
|
+
'*:80', '*']
|
|
79
|
+
"""
|
|
80
|
+
if not host:
|
|
81
|
+
raise MultisiteInvalidHostError("Invalid host: %s" % host)
|
|
82
|
+
try:
|
|
83
|
+
validate_ipv4_address(host)
|
|
84
|
+
except ValidationError:
|
|
85
|
+
# Not an IP address
|
|
86
|
+
bits = host.split(".")
|
|
87
|
+
else:
|
|
88
|
+
bits = [host]
|
|
89
|
+
result = []
|
|
90
|
+
for i in range(0, (len(bits) + 1)):
|
|
91
|
+
if i == 0:
|
|
92
|
+
host = ".".join(bits[i:])
|
|
93
|
+
else:
|
|
94
|
+
host = ".".join(["*"] + bits[i:])
|
|
95
|
+
if port:
|
|
96
|
+
result.append("%s:%s" % (host, port))
|
|
97
|
+
result.append(host)
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class CanonicalAliasManager(models.Manager):
|
|
102
|
+
"""Manager for Alias objects where is_canonical == 1."""
|
|
103
|
+
|
|
104
|
+
use_in_migrations = True
|
|
105
|
+
|
|
106
|
+
def get_queryset(self) -> QuerySet[Alias]:
|
|
107
|
+
queryset = super().get_queryset()
|
|
108
|
+
return queryset.filter(is_canonical=1)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class NotCanonicalAliasManager(models.Manager):
|
|
112
|
+
"""Manager for Aliases where is_canonical != 1."""
|
|
113
|
+
|
|
114
|
+
use_in_migrations = True
|
|
115
|
+
|
|
116
|
+
def get_queryset(self) -> QuerySet[Alias]:
|
|
117
|
+
queryset = super().get_queryset()
|
|
118
|
+
return queryset.exclude(is_canonical=1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class SpanningCurrentSiteManager(managers.CurrentSiteManager):
|
|
122
|
+
"""Unlike ``django.contrib.sites.managers.CurrentSiteManager``,
|
|
123
|
+
this CurrentSiteManager can span multiple related models by using
|
|
124
|
+
the django filtering syntax, namely foo__bar__baz__site.
|
|
125
|
+
|
|
126
|
+
For example, let's say you have a model called Layer, which has
|
|
127
|
+
a field called family, which points to a model called LayerFamily,
|
|
128
|
+
which in turn has a field called site pointing to a
|
|
129
|
+
django.contrib.sites Site model. On Layer, add the following
|
|
130
|
+
manager:
|
|
131
|
+
|
|
132
|
+
on_site = SpanningCurrentSiteManager("family__site")
|
|
133
|
+
|
|
134
|
+
and it will do the proper thing.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def _validate_field_name(self):
|
|
138
|
+
"""Given the field identifier, goes down the chain to check that
|
|
139
|
+
each specified field
|
|
140
|
+
a) exists,
|
|
141
|
+
b) is of type ForeignKey or ManyToManyField
|
|
142
|
+
|
|
143
|
+
If no field name is specified when instantiating
|
|
144
|
+
SpanningCurrentSiteManager, it tries to find either 'site' or
|
|
145
|
+
'sites' as the site link, much like CurrentSiteManager does.
|
|
146
|
+
"""
|
|
147
|
+
if self._CurrentSiteManager__field_name is None:
|
|
148
|
+
# Guess at field name
|
|
149
|
+
field_names = self.model._meta.get_all_field_names()
|
|
150
|
+
for potential_name in ["site", "sites"]:
|
|
151
|
+
if potential_name in field_names:
|
|
152
|
+
self._CurrentSiteManager__field_name = potential_name
|
|
153
|
+
break
|
|
154
|
+
else:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"{self.__class__.__name__} couldn't find a field named either 'site' or 'sites' "
|
|
157
|
+
f"in {self.model._meta.object_name}."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
field_name_chain = self._CurrentSiteManager__field_name.split(LOOKUP_SEP)
|
|
161
|
+
model = self.model
|
|
162
|
+
|
|
163
|
+
for field_name in field_name_chain:
|
|
164
|
+
# Throws an exception if anything goes bad
|
|
165
|
+
self.validate_single_field_name(model, field_name)
|
|
166
|
+
model = self.get_related_model(model, field_name)
|
|
167
|
+
|
|
168
|
+
# If we get this far without an exception, everything is good
|
|
169
|
+
self._CurrentSiteManager__is_validated = True
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def validate_single_field_name(model, field_name) -> None:
|
|
173
|
+
"""Checks if the given field_name can be used to make a link
|
|
174
|
+
between a model and a site.
|
|
175
|
+
|
|
176
|
+
If anything is wrong, will raise an appropriate exception,
|
|
177
|
+
because that is what CurrentSiteManager expects.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
field = model._meta.get_field(field_name)
|
|
181
|
+
if not isinstance(field, (models.ForeignKey, models.ManyToManyField)):
|
|
182
|
+
raise TypeError(f"Field {field_name} must be a ForeignKey or ManyToManyField.")
|
|
183
|
+
except FieldDoesNotExist:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"Couldn't find a field named {field_name} in {model._meta.object_name}."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def get_related_model(model, field_name) -> models.Model:
|
|
190
|
+
"""Given a model and the name of a ForeignKey or ManyToManyField column
|
|
191
|
+
as a string, returns the associated model.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
return model._meta.get_field(field_name).remote_field.model
|
|
195
|
+
except AttributeError:
|
|
196
|
+
return model._meta.get_field(field_name).related_model
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from django.contrib.sites.models import Site
|
|
2
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
3
|
+
from django.db import connections, router
|
|
4
|
+
from django.db.models.signals import post_save, pre_save
|
|
5
|
+
from django.dispatch import receiver
|
|
6
|
+
|
|
7
|
+
from multisite.utils import create_or_sync_alias_from_site
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@receiver(pre_save, sender=Site, weak=False, dispatch_uid="site_domain_changed_hook")
|
|
11
|
+
def pre_save_site_domain_changed(sender, instance, raw, *args, **kwargs):
|
|
12
|
+
"""Updates canonical Alias object if Site.domain has changed."""
|
|
13
|
+
if not raw and instance.pk is not None:
|
|
14
|
+
try:
|
|
15
|
+
original = sender.objects.get(pk=instance.pk)
|
|
16
|
+
except ObjectDoesNotExist:
|
|
17
|
+
pass
|
|
18
|
+
else:
|
|
19
|
+
# Update Alias.domain to match site
|
|
20
|
+
if original.domain != instance.domain:
|
|
21
|
+
create_or_sync_alias_from_site(site=instance)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@receiver(post_save, sender=Site, weak=False, dispatch_uid="site_created_hook")
|
|
25
|
+
def post_save_site_created(sender, instance, raw, created, *args, **kwargs):
|
|
26
|
+
"""Creates canonical Alias object for a new Site."""
|
|
27
|
+
if not raw and created:
|
|
28
|
+
# When running create_default_site() because of post_syncdb,
|
|
29
|
+
# don't try to sync before the db_table has been created.
|
|
30
|
+
using = router.db_for_write(sender)
|
|
31
|
+
tables = connections[using].introspection.table_names()
|
|
32
|
+
if sender._meta.db_table not in tables:
|
|
33
|
+
pass
|
|
34
|
+
else:
|
|
35
|
+
# Update Alias.domain to match site
|
|
36
|
+
create_or_sync_alias_from_site(site=instance)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.core.exceptions import ValidationError
|
|
2
|
+
|
|
3
|
+
__all__ = ["validate_1_or_none"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_1_or_none(value: bool) -> None:
|
|
7
|
+
"""Raises ValidationError if value is not int(1) or None."""
|
|
8
|
+
if value not in [1, None]:
|
|
9
|
+
raise ValidationError("%r must be 1 or None" % value)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_true_or_none(value: bool) -> None:
|
|
13
|
+
# leave for old migrations
|
|
14
|
+
pass
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
def get_test_allowed_hosts(*extra, replace=None):
|
|
2
|
+
if replace:
|
|
3
|
+
allowed_hosts = []
|
|
4
|
+
else:
|
|
5
|
+
allowed_hosts = [
|
|
6
|
+
".extrahost.com",
|
|
7
|
+
"app.test1.example.com",
|
|
8
|
+
"app.test2.example.com",
|
|
9
|
+
"example.com",
|
|
10
|
+
"new.app.test3.example.com",
|
|
11
|
+
"test.example.com",
|
|
12
|
+
]
|
|
13
|
+
allowed_hosts.extend(extra)
|
|
14
|
+
return allowed_hosts
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Callable, Type
|
|
2
|
+
|
|
3
|
+
from django.http import HttpResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_test_http_response(*cookies) -> Callable:
|
|
7
|
+
def http_response(request) -> HttpResponse:
|
|
8
|
+
response = HttpResponse()
|
|
9
|
+
for cookie in cookies:
|
|
10
|
+
response.set_cookie(**cookie)
|
|
11
|
+
return response
|
|
12
|
+
|
|
13
|
+
return http_response
|
multisite/tests/hosts.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.utils.functional import SimpleLazyObject, empty
|
|
4
|
+
|
|
5
|
+
__all__ = ["ALLOWED_HOSTS", "AllowedHosts"]
|
|
6
|
+
|
|
7
|
+
_wrapped_default = empty
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_multisite_extra_hosts():
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
|
|
13
|
+
return getattr(settings, "MULTISITE_EXTRA_HOSTS", [])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IterableLazyObject(SimpleLazyObject):
|
|
17
|
+
_wrapped_default = globals()["_wrapped_default"]
|
|
18
|
+
|
|
19
|
+
def __iter__(self):
|
|
20
|
+
if self._wrapped is self._wrapped_default:
|
|
21
|
+
self._setup()
|
|
22
|
+
return self._wrapped.__iter__()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AllowedHosts(object):
|
|
26
|
+
alias_model = None
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.extra_hosts = get_multisite_extra_hosts()
|
|
30
|
+
if self.alias_model is None:
|
|
31
|
+
from multisite.models import Alias
|
|
32
|
+
|
|
33
|
+
self.alias_model = Alias
|
|
34
|
+
|
|
35
|
+
def __iter__(self):
|
|
36
|
+
# Yielding extra hosts before actual hosts because there might be
|
|
37
|
+
# wild cards in there that would prevent us from doing a database
|
|
38
|
+
# query every time.
|
|
39
|
+
for host in self.extra_hosts:
|
|
40
|
+
yield host
|
|
41
|
+
|
|
42
|
+
for host in self.alias_model.objects.values_list("domain"):
|
|
43
|
+
yield host[0]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
ALLOWED_HOSTS = IterableLazyObject(lambda: AllowedHosts())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from multisite import SiteID
|
|
2
|
+
|
|
3
|
+
SILENCED_SYSTEM_CHECKS = ["sites.E101"]
|
|
4
|
+
SECRET_KEY = "iufoj=mibkpdz*%bob952x(%49rqgv8gg45k36kjcg76&-y5=!" # nosec B105
|
|
5
|
+
SITE_ID = SiteID(default=1)
|
|
6
|
+
DATABASES = {
|
|
7
|
+
"default": {
|
|
8
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
9
|
+
"NAME": ":memory:",
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
INSTALLED_APPS = [
|
|
14
|
+
# "django.contrib.auth",
|
|
15
|
+
# "django.contrib.contenttypes",
|
|
16
|
+
"django.contrib.sites",
|
|
17
|
+
"multisite",
|
|
18
|
+
"multisite_app",
|
|
19
|
+
]
|
|
20
|
+
MIDDLEWARE = [
|
|
21
|
+
"multisite.middleware.DynamicSiteMiddleware",
|
|
22
|
+
"django.contrib.sites.middleware.CurrentSiteMiddleware",
|
|
23
|
+
]
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from django.test.client import RequestFactory as DjangoRequestFactory
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RequestFactory(DjangoRequestFactory):
|
|
5
|
+
def __init__(self, host):
|
|
6
|
+
super().__init__()
|
|
7
|
+
self.host = host
|
|
8
|
+
|
|
9
|
+
def get(self, path, data={}, host=None, **extra):
|
|
10
|
+
if host is None:
|
|
11
|
+
host = self.host
|
|
12
|
+
return super().get(path=path, data=data, HTTP_HOST=host, **extra)
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
from django.contrib.sites.models import Site
|
|
2
|
+
from django.core.exceptions import MultipleObjectsReturned, ValidationError
|
|
3
|
+
from django.test import TestCase, tag
|
|
4
|
+
|
|
5
|
+
from multisite.exceptions import MultisiteInvalidHostError
|
|
6
|
+
from multisite.models import Alias
|
|
7
|
+
from multisite.utils import (
|
|
8
|
+
create_or_sync_alias_from_site,
|
|
9
|
+
create_or_sync_canonical_from_all_sites,
|
|
10
|
+
create_or_sync_missing_canonical_from_site_domain,
|
|
11
|
+
sync_blank_domain,
|
|
12
|
+
sync_canonical_from_site_domain,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AliasTest(TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
Alias.objects.all().delete()
|
|
19
|
+
Site.objects.all().delete()
|
|
20
|
+
|
|
21
|
+
def test_create(self):
|
|
22
|
+
Site.objects.create() # site0
|
|
23
|
+
site1 = Site.objects.create(domain="1.example")
|
|
24
|
+
site2 = Site.objects.create(domain="2.example")
|
|
25
|
+
# Missing site
|
|
26
|
+
self.assertRaises(ValidationError, Alias.objects.create)
|
|
27
|
+
self.assertRaises(ValidationError, Alias.objects.create, domain="0.example")
|
|
28
|
+
# Valid
|
|
29
|
+
self.assertTrue(Alias.objects.create(domain="1a.example", site=site1))
|
|
30
|
+
# Duplicate domain
|
|
31
|
+
self.assertRaises(
|
|
32
|
+
ValidationError, Alias.objects.create, domain=site1.domain, site=site1
|
|
33
|
+
)
|
|
34
|
+
self.assertRaises(
|
|
35
|
+
ValidationError, Alias.objects.create, domain=site2.domain, site=site1
|
|
36
|
+
)
|
|
37
|
+
self.assertRaises(
|
|
38
|
+
ValidationError, Alias.objects.create, domain="1a.example", site=site1
|
|
39
|
+
)
|
|
40
|
+
# Duplicate domains, case-sensitivity
|
|
41
|
+
self.assertRaises(
|
|
42
|
+
ValidationError, Alias.objects.create, domain="1A.EXAMPLE", site=site2
|
|
43
|
+
)
|
|
44
|
+
self.assertRaises(
|
|
45
|
+
ValidationError, Alias.objects.create, domain="2.EXAMPLE", site=site2
|
|
46
|
+
)
|
|
47
|
+
# Duplicate is_canonical
|
|
48
|
+
site1.domain = "1b.example"
|
|
49
|
+
self.assertRaises(
|
|
50
|
+
ValidationError,
|
|
51
|
+
Alias.objects.create,
|
|
52
|
+
domain=site1.domain,
|
|
53
|
+
site=site1,
|
|
54
|
+
is_canonical=1,
|
|
55
|
+
)
|
|
56
|
+
# Invalid is_canonical
|
|
57
|
+
self.assertRaises(
|
|
58
|
+
ValidationError,
|
|
59
|
+
Alias.objects.create,
|
|
60
|
+
domain=site1.domain,
|
|
61
|
+
site=site1,
|
|
62
|
+
is_canonical=0,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def test_repr(self):
|
|
66
|
+
site = Site.objects.create(domain="example.com")
|
|
67
|
+
self.assertEqual(
|
|
68
|
+
repr(Alias.objects.get(site=site)),
|
|
69
|
+
"<Alias: %(domain)s -> %(domain)s>" % site.__dict__,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@tag("1")
|
|
73
|
+
def test_managers(self):
|
|
74
|
+
site = Site.objects.create(domain="example.com")
|
|
75
|
+
Alias.objects.create(site=site, domain="example.org")
|
|
76
|
+
self.assertEqual(
|
|
77
|
+
set(Alias.objects.values_list("domain", flat=True)),
|
|
78
|
+
set(["example.com", "example.org"]),
|
|
79
|
+
)
|
|
80
|
+
self.assertEqual(
|
|
81
|
+
set(Alias.canonical.values_list("domain", flat=True)), set(["example.com"])
|
|
82
|
+
)
|
|
83
|
+
self.assertEqual(
|
|
84
|
+
set(Alias.aliases.values_list("domain", flat=True)), set(["example.org"])
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def test_sync_canonical_from_site_domain(self):
|
|
88
|
+
# Create Sites with Aliases
|
|
89
|
+
Site.objects.create()
|
|
90
|
+
site1 = Site.objects.create(domain="1.example.com")
|
|
91
|
+
site2 = Site.objects.create(domain="2.example.com")
|
|
92
|
+
# Create Site without triggering signals
|
|
93
|
+
site3 = Site(domain="3.example.com")
|
|
94
|
+
site3.save_base(raw=True)
|
|
95
|
+
self.assertEqual(
|
|
96
|
+
set(Alias.objects.values_list("domain", flat=True)),
|
|
97
|
+
set([site1.domain, site2.domain]),
|
|
98
|
+
)
|
|
99
|
+
# Sync existing
|
|
100
|
+
site1.domain = "1.example.org"
|
|
101
|
+
site1.save_base(raw=True)
|
|
102
|
+
site2.domain = "2.example.org"
|
|
103
|
+
site2.save_base(raw=True)
|
|
104
|
+
sync_canonical_from_site_domain()
|
|
105
|
+
self.assertEqual(
|
|
106
|
+
set(Alias.objects.values_list("domain", flat=True)),
|
|
107
|
+
set([site1.domain, site2.domain]),
|
|
108
|
+
)
|
|
109
|
+
# Sync with filter
|
|
110
|
+
site1.domain = "1.example.net"
|
|
111
|
+
site1.save_base(raw=True)
|
|
112
|
+
site2.domain = "2.example.net"
|
|
113
|
+
site2.save_base(raw=True)
|
|
114
|
+
sync_canonical_from_site_domain(site__domain=site1.domain)
|
|
115
|
+
self.assertEqual(
|
|
116
|
+
set(Alias.objects.values_list("domain", flat=True)),
|
|
117
|
+
set([site1.domain, "2.example.org"]),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def test_sync_missing(self):
|
|
121
|
+
Site.objects.create()
|
|
122
|
+
site1 = Site.objects.create(domain="1.example.com")
|
|
123
|
+
# Update site1 without triggering signals
|
|
124
|
+
site1.domain = "1.example.org"
|
|
125
|
+
site1.save_base(raw=True)
|
|
126
|
+
# Create site2 without triggering signals
|
|
127
|
+
site2 = Site(domain="2.example.org")
|
|
128
|
+
site2.save_base(raw=True)
|
|
129
|
+
# Only site2 should be updated
|
|
130
|
+
create_or_sync_missing_canonical_from_site_domain()
|
|
131
|
+
self.assertEqual(
|
|
132
|
+
set(Alias.objects.values_list("domain", flat=True)),
|
|
133
|
+
set(["1.example.com", site2.domain]),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def test_sync_all(self):
|
|
137
|
+
Site.objects.create()
|
|
138
|
+
site1 = Site.objects.create(domain="1.example.com")
|
|
139
|
+
# Update site1 without triggering signals
|
|
140
|
+
site1.domain = "1.example.org"
|
|
141
|
+
site1.save_base(raw=True)
|
|
142
|
+
# Create site2 without triggering signals
|
|
143
|
+
site2 = Site(domain="2.example.org")
|
|
144
|
+
site2.save_base(raw=True)
|
|
145
|
+
# Sync all
|
|
146
|
+
create_or_sync_canonical_from_all_sites()
|
|
147
|
+
self.assertEqual(
|
|
148
|
+
set(Alias.objects.values_list("domain", flat=True)),
|
|
149
|
+
set([site1.domain, site2.domain]),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def test_sync(self):
|
|
153
|
+
# Create Site without triggering signals
|
|
154
|
+
site = Site(domain="example.com")
|
|
155
|
+
site.save_base(raw=True)
|
|
156
|
+
# Insert Alias
|
|
157
|
+
self.assertFalse(Alias.objects.filter(site=site).exists())
|
|
158
|
+
create_or_sync_alias_from_site(site=site)
|
|
159
|
+
self.assertEqual(Alias.objects.get(site=site).domain, site.domain)
|
|
160
|
+
# Idempotent sync_alias
|
|
161
|
+
create_or_sync_alias_from_site(site=site)
|
|
162
|
+
self.assertEqual(Alias.objects.get(site=site).domain, site.domain)
|
|
163
|
+
# Duplicate force_insert
|
|
164
|
+
self.assertRaises(
|
|
165
|
+
ValidationError, create_or_sync_alias_from_site, site=site, force_insert=True
|
|
166
|
+
)
|
|
167
|
+
# Update Alias
|
|
168
|
+
site.domain = "example.org"
|
|
169
|
+
create_or_sync_alias_from_site(site=site)
|
|
170
|
+
self.assertEqual(Alias.objects.get(site=site).domain, site.domain)
|
|
171
|
+
# Clear domain
|
|
172
|
+
site.domain = ""
|
|
173
|
+
create_or_sync_alias_from_site(site=site)
|
|
174
|
+
self.assertFalse(Alias.objects.filter(site=site).exists())
|
|
175
|
+
|
|
176
|
+
def test_sync_blank_domain(self):
|
|
177
|
+
# Create Site
|
|
178
|
+
site = Site.objects.create(domain="example.com")
|
|
179
|
+
# Without clearing domain
|
|
180
|
+
self.assertRaises(ValueError, sync_blank_domain, site=site)
|
|
181
|
+
# With an extra Alias
|
|
182
|
+
site.domain = ""
|
|
183
|
+
alias = Alias.objects.create(site=site, domain="example.org")
|
|
184
|
+
self.assertRaises(MultipleObjectsReturned, sync_blank_domain, site=site)
|
|
185
|
+
# With a blank site
|
|
186
|
+
alias.delete()
|
|
187
|
+
sync_blank_domain(site=site)
|
|
188
|
+
self.assertFalse(Alias.objects.filter(site=site).exists())
|
|
189
|
+
|
|
190
|
+
def test_hooks(self):
|
|
191
|
+
# Create empty Site
|
|
192
|
+
Site.objects.create()
|
|
193
|
+
self.assertFalse(Alias.objects.filter(domain="").exists())
|
|
194
|
+
# Create Site
|
|
195
|
+
site = Site.objects.create(domain="example.com")
|
|
196
|
+
alias = Alias.objects.get(site=site)
|
|
197
|
+
self.assertEqual(alias.domain, site.domain)
|
|
198
|
+
self.assertTrue(alias.is_canonical)
|
|
199
|
+
# Create a non-canonical alias
|
|
200
|
+
Alias.objects.create(site=site, domain="example.info")
|
|
201
|
+
# Change Site to another domain name
|
|
202
|
+
site.domain = "example.org"
|
|
203
|
+
site.save()
|
|
204
|
+
self.assertEqual(Alias.canonical.get(site=site).domain, site.domain)
|
|
205
|
+
self.assertEqual(Alias.aliases.get(site=site).domain, "example.info")
|
|
206
|
+
# Change Site to an empty domain name
|
|
207
|
+
site.domain = ""
|
|
208
|
+
self.assertRaises(MultipleObjectsReturned, site.save)
|
|
209
|
+
|
|
210
|
+
Alias.aliases.all().delete()
|
|
211
|
+
Site.objects.get(domain="").delete() # domain is unique in Django1.9
|
|
212
|
+
site.save()
|
|
213
|
+
self.assertFalse(Alias.objects.filter(site=site).exists())
|
|
214
|
+
|
|
215
|
+
# Change Site from an empty domain name
|
|
216
|
+
site.domain = "example.net"
|
|
217
|
+
site.save()
|
|
218
|
+
site.refresh_from_db()
|
|
219
|
+
self.assertEqual(Alias.canonical.get(site=site).domain, site.domain)
|
|
220
|
+
|
|
221
|
+
# Delete Site, which deletes Alias
|
|
222
|
+
site.delete()
|
|
223
|
+
self.assertFalse(Alias.objects.filter(site__domain="example.net").exists())
|
|
224
|
+
|
|
225
|
+
def test_expand_netloc(self):
|
|
226
|
+
_expand_netloc = Alias.objects._expand_netloc
|
|
227
|
+
self.assertRaises(MultisiteInvalidHostError, _expand_netloc, "")
|
|
228
|
+
self.assertRaises(MultisiteInvalidHostError, _expand_netloc, "", 8000)
|
|
229
|
+
self.assertEqual(
|
|
230
|
+
_expand_netloc("testserver", 8000),
|
|
231
|
+
["testserver:8000", "testserver", "*:8000", "*"],
|
|
232
|
+
)
|
|
233
|
+
self.assertEqual(_expand_netloc("testserver"), ["testserver", "*"])
|
|
234
|
+
self.assertEqual(
|
|
235
|
+
_expand_netloc("example.com", 8000),
|
|
236
|
+
["example.com:8000", "example.com", "*.com:8000", "*.com", "*:8000", "*"],
|
|
237
|
+
)
|
|
238
|
+
self.assertEqual(_expand_netloc("example.com"), ["example.com", "*.com", "*"])
|
|
239
|
+
self.assertEqual(
|
|
240
|
+
_expand_netloc("www.example.com", 8000),
|
|
241
|
+
[
|
|
242
|
+
"www.example.com:8000",
|
|
243
|
+
"www.example.com",
|
|
244
|
+
"*.example.com:8000",
|
|
245
|
+
"*.example.com",
|
|
246
|
+
"*.com:8000",
|
|
247
|
+
"*.com",
|
|
248
|
+
"*:8000",
|
|
249
|
+
"*",
|
|
250
|
+
],
|
|
251
|
+
)
|
|
252
|
+
self.assertEqual(
|
|
253
|
+
_expand_netloc("www.example.com"),
|
|
254
|
+
["www.example.com", "*.example.com", "*.com", "*"],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def test_resolve(self):
|
|
258
|
+
site = Site.objects.create(domain="example.com")
|
|
259
|
+
alias = Alias.objects.get(domain="example.com")
|
|
260
|
+
# *.example.com
|
|
261
|
+
self.assertEqual(Alias.objects.resolve("www.example.com"), None)
|
|
262
|
+
self.assertEqual(Alias.objects.resolve("www.dev.example.com"), None)
|
|
263
|
+
|
|
264
|
+
alias = Alias.objects.create(site=site, domain="*.example.com")
|
|
265
|
+
self.assertEqual(Alias.objects.resolve("www.example.com"), alias)
|
|
266
|
+
self.assertEqual(Alias.objects.resolve("www.dev.example.com"), alias)
|
|
267
|
+
# *
|
|
268
|
+
self.assertEqual(Alias.objects.resolve("example.net"), None)
|
|
269
|
+
|
|
270
|
+
alias = Alias.objects.create(site=site, domain="*")
|
|
271
|
+
self.assertEqual(Alias.objects.resolve("example.net"), alias)
|
|
272
|
+
self.assertEqual(Alias.objects.resolve("blah"), alias)
|