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,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from hashlib import md5 as md5_constructor
|
|
4
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
5
|
+
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.contrib.sites.models import SITE_CACHE, Site
|
|
8
|
+
from django.core import mail
|
|
9
|
+
from django.core.cache import caches
|
|
10
|
+
from django.core.exceptions import DisallowedHost
|
|
11
|
+
from django.db.models.signals import post_delete, post_init, pre_save
|
|
12
|
+
from django.http import HttpResponse, HttpResponsePermanentRedirect
|
|
13
|
+
|
|
14
|
+
from ..exceptions import (
|
|
15
|
+
MultisiteError,
|
|
16
|
+
MultisiteInvalidHostError,
|
|
17
|
+
debug_check_status_code,
|
|
18
|
+
debug_raise_cache_missed_exception,
|
|
19
|
+
debug_raise_disallowed_host_exception,
|
|
20
|
+
)
|
|
21
|
+
from ..models import Alias
|
|
22
|
+
|
|
23
|
+
__all__ = ["DynamicSiteMiddleware"]
|
|
24
|
+
|
|
25
|
+
from ..utils import (
|
|
26
|
+
get_cache_multisite_alias,
|
|
27
|
+
get_cache_multisite_prefix,
|
|
28
|
+
get_fallback_kwargs,
|
|
29
|
+
get_fallback_view_or_404,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DynamicSiteMiddleware:
|
|
34
|
+
def __init__(self, get_response):
|
|
35
|
+
self.get_response = get_response
|
|
36
|
+
if not hasattr(settings.SITE_ID, "set"):
|
|
37
|
+
raise MultisiteError(
|
|
38
|
+
"Invalid type for settings.SITE_ID: %s" % type(settings.SITE_ID).__name__
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.cache_alias = get_cache_multisite_alias()
|
|
42
|
+
self.key_prefix = get_cache_multisite_prefix() or settings.CACHES[
|
|
43
|
+
self.cache_alias
|
|
44
|
+
].get("KEY_PREFIX", "")
|
|
45
|
+
|
|
46
|
+
self.cache = caches[self.cache_alias]
|
|
47
|
+
post_init.connect(
|
|
48
|
+
self.site_domain_cache_hook, sender=Site, dispatch_uid="multisite_post_init"
|
|
49
|
+
)
|
|
50
|
+
pre_save.connect(self.site_domain_changed_hook, sender=Site)
|
|
51
|
+
post_delete.connect(self.site_deleted_hook, sender=Site)
|
|
52
|
+
|
|
53
|
+
def __call__(self, request):
|
|
54
|
+
response = self.get_response(request)
|
|
55
|
+
# if 500 here -> Site ObjectDoesNotExist was raised by get_current_site()
|
|
56
|
+
debug_check_status_code(response, request=request)
|
|
57
|
+
try:
|
|
58
|
+
netloc = request.get_host().lower()
|
|
59
|
+
except DisallowedHost as e:
|
|
60
|
+
debug_raise_disallowed_host_exception(e)
|
|
61
|
+
settings.SITE_ID.reset()
|
|
62
|
+
response = self.fallback_view(request)
|
|
63
|
+
else:
|
|
64
|
+
cache_key = self.get_cache_key(netloc)
|
|
65
|
+
if (alias := self.cache.get(cache_key)) is not None:
|
|
66
|
+
# found Alias
|
|
67
|
+
self.cache.set(cache_key, alias)
|
|
68
|
+
settings.SITE_ID.set(alias.site_id)
|
|
69
|
+
response = self.redirect_to_canonical(request, response, alias)
|
|
70
|
+
elif (alias := self.get_alias(netloc)) is None:
|
|
71
|
+
# Cache missed, fallback using settings.MULTISITE_FALLBACK
|
|
72
|
+
debug_raise_cache_missed_exception(netloc, alias)
|
|
73
|
+
settings.SITE_ID.reset()
|
|
74
|
+
response = self.fallback_view(request)
|
|
75
|
+
else:
|
|
76
|
+
# found Site
|
|
77
|
+
self.cache.set(cache_key, alias)
|
|
78
|
+
settings.SITE_ID.set(alias.site_id)
|
|
79
|
+
SITE_CACHE[settings.SITE_ID] = alias.site # Pre-populate SITE_CACHE
|
|
80
|
+
response = self.redirect_to_canonical(request, response, alias)
|
|
81
|
+
debug_check_status_code(response, netloc=netloc, alias=alias, site=alias.site)
|
|
82
|
+
return response
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def redirect_to_canonical(request, response, alias: Alias) -> HttpResponse:
|
|
86
|
+
if not alias.redirect_to_canonical or alias.is_canonical:
|
|
87
|
+
return response
|
|
88
|
+
url = urlsplit(request.build_absolute_uri(request.get_full_path()))
|
|
89
|
+
url = urlunsplit((url.scheme, alias.site.domain, url.path, url.query, url.fragment))
|
|
90
|
+
return HttpResponsePermanentRedirect(url)
|
|
91
|
+
|
|
92
|
+
def get_cache_key(self, netloc):
|
|
93
|
+
"""Returns a cache key based on ``netloc``."""
|
|
94
|
+
netloc = md5_constructor(netloc.encode("utf-8"), usedforsecurity=False)
|
|
95
|
+
return "multisite.alias.%s.%s" % (self.key_prefix, netloc.hexdigest())
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def netloc_parse(netloc):
|
|
99
|
+
"""Returns ``(host, port)`` for ``netloc`` of the form
|
|
100
|
+
``'host:port'``.
|
|
101
|
+
|
|
102
|
+
If netloc does not have a port number, ``port`` will be None.
|
|
103
|
+
"""
|
|
104
|
+
if ":" in netloc:
|
|
105
|
+
return netloc.rsplit(":", 1)
|
|
106
|
+
else:
|
|
107
|
+
return netloc, None
|
|
108
|
+
|
|
109
|
+
def get_alias(self, netloc) -> Alias | None:
|
|
110
|
+
"""Returns Alias matching ``netloc``. Otherwise,
|
|
111
|
+
returns None.
|
|
112
|
+
"""
|
|
113
|
+
host, port = self.netloc_parse(netloc)
|
|
114
|
+
try:
|
|
115
|
+
alias = Alias.objects.resolve(host=host, port=port)
|
|
116
|
+
except MultisiteInvalidHostError:
|
|
117
|
+
alias = None
|
|
118
|
+
if alias is None:
|
|
119
|
+
# Running under TestCase or runserver?
|
|
120
|
+
alias = self.get_development_alias(netloc)
|
|
121
|
+
return alias
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def fallback_view(request) -> HttpResponse:
|
|
125
|
+
"""Runs the fallback view function in
|
|
126
|
+
``settings.MULTISITE_FALLBACK``.
|
|
127
|
+
|
|
128
|
+
If ``MULTISITE_FALLBACK`` is None, raises an Http404 error.
|
|
129
|
+
|
|
130
|
+
If ``MULTISITE_FALLBACK`` is callable, will treat that
|
|
131
|
+
callable as a view that returns an HttpResponse.
|
|
132
|
+
|
|
133
|
+
If ``MULTISITE_FALLBACK`` is a string, will resolve it to a
|
|
134
|
+
view that returns an HttpResponse.
|
|
135
|
+
|
|
136
|
+
In order to use a generic view that takes additional
|
|
137
|
+
parameters, ``settings.MULTISITE_FALLBACK_KWARGS`` may be a
|
|
138
|
+
dictionary of additional keyword arguments.
|
|
139
|
+
"""
|
|
140
|
+
view = get_fallback_view_or_404()
|
|
141
|
+
kwargs = get_fallback_kwargs()
|
|
142
|
+
if hasattr(view, "as_view"):
|
|
143
|
+
return view.as_view(**kwargs)(request)
|
|
144
|
+
return view(request, **kwargs)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def get_development_alias(netloc) -> Alias:
|
|
148
|
+
"""Returns valid Alias when in development mode. Otherwise,
|
|
149
|
+
returns None.
|
|
150
|
+
|
|
151
|
+
Development mode is either:
|
|
152
|
+
- Running tests, i.e. manage.py test
|
|
153
|
+
- Running locally in `settings.DEBUG` = True, where the
|
|
154
|
+
hostname is a top-level name, i.e. localhost
|
|
155
|
+
"""
|
|
156
|
+
alias = None
|
|
157
|
+
# When running tests, django.core.mail.outbox exists and
|
|
158
|
+
# netloc == 'testserver'
|
|
159
|
+
is_testserver = hasattr(mail, "outbox") and netloc in (
|
|
160
|
+
"testserver",
|
|
161
|
+
"adminsite.com",
|
|
162
|
+
)
|
|
163
|
+
# When using runserver, assume that host will only have one path
|
|
164
|
+
# component. This covers 'localhost' and your machine name.
|
|
165
|
+
is_local_debug = settings.DEBUG and len(netloc.split(".")) == 1
|
|
166
|
+
if is_testserver or is_local_debug:
|
|
167
|
+
try:
|
|
168
|
+
# Prefer the default SITE_ID
|
|
169
|
+
site_id = settings.SITE_ID.get_default()
|
|
170
|
+
except ValueError:
|
|
171
|
+
# Fallback to the first Site object
|
|
172
|
+
alias = Alias.canonical.order_by("site")[0]
|
|
173
|
+
else:
|
|
174
|
+
alias = Alias.canonical.get(site=site_id)
|
|
175
|
+
return alias
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def site_domain_cache_hook(cls, sender, instance, *args, **kwargs):
|
|
179
|
+
"""Caches `Site.domain` in the object for
|
|
180
|
+
site_domain_changed_hook.
|
|
181
|
+
"""
|
|
182
|
+
instance._domain_cache = instance.domain
|
|
183
|
+
|
|
184
|
+
def site_domain_changed_hook(self, sender, instance, raw, *args, **kwargs):
|
|
185
|
+
"""Clears the cache if `Site.domain` has changed."""
|
|
186
|
+
if raw or instance.pk is None:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
original = getattr(instance, "_domain_cache", None)
|
|
190
|
+
if original != instance.domain:
|
|
191
|
+
self.cache.clear()
|
|
192
|
+
|
|
193
|
+
def site_deleted_hook(self, *args, **kwargs):
|
|
194
|
+
"""Clears the cache if Site was deleted."""
|
|
195
|
+
self.cache.clear()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from __future__ import absolute_import, unicode_literals
|
|
3
|
+
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
import multisite.models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
dependencies = [
|
|
11
|
+
("sites", "0001_initial"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name="Alias",
|
|
17
|
+
fields=[
|
|
18
|
+
(
|
|
19
|
+
"id",
|
|
20
|
+
models.AutoField(
|
|
21
|
+
verbose_name="ID",
|
|
22
|
+
serialize=False,
|
|
23
|
+
auto_created=True,
|
|
24
|
+
primary_key=True,
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
"domain",
|
|
29
|
+
models.CharField(
|
|
30
|
+
help_text='Either "domain" or "domain:port"',
|
|
31
|
+
unique=True,
|
|
32
|
+
max_length=100,
|
|
33
|
+
verbose_name="domain name",
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
(
|
|
37
|
+
"is_canonical",
|
|
38
|
+
models.NullBooleanField(
|
|
39
|
+
default=None,
|
|
40
|
+
validators=[multisite.models.validators.validate_true_or_none],
|
|
41
|
+
editable=False,
|
|
42
|
+
help_text="Does this domain name match the one in site?",
|
|
43
|
+
verbose_name="is canonical?",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"redirect_to_canonical",
|
|
48
|
+
models.BooleanField(
|
|
49
|
+
default=True,
|
|
50
|
+
help_text="Should this domain name redirect to the one in site?",
|
|
51
|
+
verbose_name="redirect to canonical?",
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"site",
|
|
56
|
+
models.ForeignKey(
|
|
57
|
+
related_name="aliases",
|
|
58
|
+
to="sites.Site",
|
|
59
|
+
on_delete=models.CASCADE,
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
options={
|
|
64
|
+
"verbose_name_plural": "aliases",
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
migrations.AlterUniqueTogether(
|
|
68
|
+
name="alias",
|
|
69
|
+
unique_together=set([("is_canonical", "site")]),
|
|
70
|
+
),
|
|
71
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Generated by Django 4.0.4 on 2022-05-12 23:39
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
import multisite.models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
dependencies = [
|
|
10
|
+
("multisite", "0001_initial"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AlterField(
|
|
15
|
+
model_name="alias",
|
|
16
|
+
name="id",
|
|
17
|
+
field=models.BigAutoField(
|
|
18
|
+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
migrations.AlterField(
|
|
22
|
+
model_name="alias",
|
|
23
|
+
name="is_canonical",
|
|
24
|
+
field=models.BooleanField(
|
|
25
|
+
default=False,
|
|
26
|
+
editable=False,
|
|
27
|
+
help_text="Does this domain name match the one in site?",
|
|
28
|
+
validators=[multisite.models.validators.validate_true_or_none],
|
|
29
|
+
verbose_name="is canonical?",
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
]
|
|
@@ -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
|
+
]
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|