django-multisite2 2.0.0__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.0.0.dist-info/LICENSE +28 -0
- django_multisite2-2.0.0.dist-info/METADATA +246 -0
- django_multisite2-2.0.0.dist-info/RECORD +51 -0
- django_multisite2-2.0.0.dist-info/WHEEL +5 -0
- django_multisite2-2.0.0.dist-info/top_level.txt +2 -0
- multisite/__init__.py +1 -0
- multisite/admin/__init__.py +3 -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/commands/__init__.py +0 -0
- multisite/management/commands/update_public_suffix_list.py +33 -0
- multisite/middleware/__init__.py +4 -0
- multisite/middleware/cookie_domain_middleware.py +72 -0
- multisite/middleware/dynamic_site_middleware.py +198 -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/0004_alter_alias_managers.py +22 -0
- multisite/migrations/__init__.py +0 -0
- multisite/models/__init__.py +3 -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/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/request_factory.py +12 -0
- multisite/tests/tests/test_alias.py +271 -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 +136 -0
- multisite/utils.py +172 -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,41 @@
|
|
|
1
|
+
# Generated by Django 5.0.3 on 2024-03-22 03:52
|
|
2
|
+
|
|
3
|
+
import multisite.models
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
("multisite", "0002_alter_alias_id_alter_alias_is_canonical"),
|
|
11
|
+
("sites", "0002_alter_domain_unique"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.AlterModelOptions(
|
|
16
|
+
name="alias",
|
|
17
|
+
options={"verbose_name": "Alias", "verbose_name_plural": "Aliases"},
|
|
18
|
+
),
|
|
19
|
+
migrations.AlterUniqueTogether(
|
|
20
|
+
name="alias",
|
|
21
|
+
unique_together=set(),
|
|
22
|
+
),
|
|
23
|
+
migrations.AlterField(
|
|
24
|
+
model_name="alias",
|
|
25
|
+
name="is_canonical",
|
|
26
|
+
field=models.IntegerField(
|
|
27
|
+
default=None,
|
|
28
|
+
editable=False,
|
|
29
|
+
help_text="Does this domain name match the one in site?",
|
|
30
|
+
null=True,
|
|
31
|
+
validators=[multisite.models.validate_1_or_none],
|
|
32
|
+
verbose_name="is canonical?",
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
migrations.AddConstraint(
|
|
36
|
+
model_name="alias",
|
|
37
|
+
constraint=models.UniqueConstraint(
|
|
38
|
+
fields=("is_canonical", "site"), name="unique_is_canonical_site"
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated by Django 4.2.11 on 2024-03-27 04:14
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
import multisite.models.managers
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
("multisite", "0003_alter_alias_options_alter_alias_unique_together_and_more"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AlterModelManagers(
|
|
15
|
+
name="alias",
|
|
16
|
+
managers=[
|
|
17
|
+
("objects", multisite.models.managers.AliasManager()),
|
|
18
|
+
("canonical", multisite.models.managers.CanonicalAliasManager()),
|
|
19
|
+
("aliases", multisite.models.managers.NotCanonicalAliasManager()),
|
|
20
|
+
],
|
|
21
|
+
),
|
|
22
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.contrib.sites.models import Site
|
|
4
|
+
from django.core.exceptions import ValidationError
|
|
5
|
+
from django.db import models
|
|
6
|
+
from django.db.models import UniqueConstraint
|
|
7
|
+
from django.utils.translation import gettext_lazy as _
|
|
8
|
+
|
|
9
|
+
from .managers import AliasManager, CanonicalAliasManager, NotCanonicalAliasManager
|
|
10
|
+
from .validators import validate_1_or_none
|
|
11
|
+
|
|
12
|
+
_site_domain = Site._meta.get_field("domain")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["Alias"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Alias(models.Model):
|
|
19
|
+
"""
|
|
20
|
+
Model for domain-name aliases for Site objects.
|
|
21
|
+
|
|
22
|
+
Domain names must be unique in the format of: 'hostname[:port].'
|
|
23
|
+
Each Site object that has a domain must have an ``is_canonical``
|
|
24
|
+
Alias.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
domain = type(_site_domain)(
|
|
28
|
+
_("domain name"),
|
|
29
|
+
max_length=_site_domain.max_length,
|
|
30
|
+
unique=True,
|
|
31
|
+
help_text=_('Either "domain" or "domain:port"'),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
site = models.ForeignKey(Site, related_name="aliases", on_delete=models.CASCADE)
|
|
35
|
+
|
|
36
|
+
is_canonical = models.IntegerField(
|
|
37
|
+
_("is canonical?"),
|
|
38
|
+
default=None,
|
|
39
|
+
null=True,
|
|
40
|
+
editable=False,
|
|
41
|
+
validators=[validate_1_or_none],
|
|
42
|
+
help_text=_("Does this domain name match the one in site?"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
redirect_to_canonical = models.BooleanField(
|
|
46
|
+
_("redirect to canonical?"),
|
|
47
|
+
default=True,
|
|
48
|
+
help_text=_("Should this domain name redirect to the one in site?"),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def __str__(self):
|
|
52
|
+
return f"{self.domain} -> {self.site.domain}"
|
|
53
|
+
|
|
54
|
+
def __repr__(self):
|
|
55
|
+
return f"<Alias: {self}>"
|
|
56
|
+
|
|
57
|
+
objects = AliasManager()
|
|
58
|
+
canonical = CanonicalAliasManager()
|
|
59
|
+
aliases = NotCanonicalAliasManager()
|
|
60
|
+
|
|
61
|
+
class Meta:
|
|
62
|
+
verbose_name = _("Alias")
|
|
63
|
+
verbose_name_plural = _("Aliases")
|
|
64
|
+
constraints = [
|
|
65
|
+
UniqueConstraint(
|
|
66
|
+
name="unique_is_canonical_site",
|
|
67
|
+
fields=["is_canonical", "site"],
|
|
68
|
+
# nulls_distinct=False, DJ5
|
|
69
|
+
)
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
def save_base(self, *args, **kwargs):
|
|
73
|
+
"""For canonical Alias, domains must match Site domains.
|
|
74
|
+
|
|
75
|
+
This needs to be validated here so that it is executed *after* the
|
|
76
|
+
Site pre-save signal updates the domain (an AliasInline modelform
|
|
77
|
+
on SiteAdmin will be saved (and it's clean methods run before
|
|
78
|
+
the Site is saved)
|
|
79
|
+
"""
|
|
80
|
+
self.full_clean()
|
|
81
|
+
if self.is_canonical and self.domain != self.site.domain:
|
|
82
|
+
raise ValidationError({"domain": ["Does not match %r" % self.site]})
|
|
83
|
+
super().save_base(*args, **kwargs)
|
|
84
|
+
|
|
85
|
+
def validate_unique(self, exclude=None) -> None:
|
|
86
|
+
errors = {}
|
|
87
|
+
try:
|
|
88
|
+
super().validate_unique(exclude=exclude)
|
|
89
|
+
except ValidationError as e:
|
|
90
|
+
errors = e.update_error_dict(errors)
|
|
91
|
+
if exclude is not None and "domain" not in exclude:
|
|
92
|
+
errors = self._validate_domain_is_unique(errors)
|
|
93
|
+
if errors:
|
|
94
|
+
raise ValidationError(errors)
|
|
95
|
+
|
|
96
|
+
def _validate_domain_is_unique(self, errors: dict) -> dict:
|
|
97
|
+
"""Ensure domain is unique, insensitive to case"""
|
|
98
|
+
field_name = "domain"
|
|
99
|
+
field_error = self.unique_error_message(self.__class__, (field_name,))
|
|
100
|
+
if field_name not in errors or str(field_error) not in [
|
|
101
|
+
str(err) for err in errors[field_name]
|
|
102
|
+
]:
|
|
103
|
+
queryset = self.__class__.objects.filter(
|
|
104
|
+
**{field_name + "__iexact": getattr(self, field_name)}
|
|
105
|
+
)
|
|
106
|
+
if self.pk is not None:
|
|
107
|
+
queryset = queryset.exclude(pk=self.pk)
|
|
108
|
+
if queryset.exists():
|
|
109
|
+
errors.setdefault(field_name, []).append(field_error)
|
|
110
|
+
return errors
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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)
|