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.
Files changed (138) hide show
  1. django_multisite2-2.1.1.dist-info/LICENSE +28 -0
  2. django_multisite2-2.1.1.dist-info/METADATA +244 -0
  3. django_multisite2-2.1.1.dist-info/RECORD +138 -0
  4. django_multisite2-2.1.1.dist-info/WHEEL +5 -0
  5. django_multisite2-2.1.1.dist-info/top_level.txt +2 -0
  6. multisite/__init__.py +2 -0
  7. multisite/__pycache__/__init__.cpython-310.pyc +0 -0
  8. multisite/__pycache__/__init__.cpython-311.pyc +0 -0
  9. multisite/__pycache__/__init__.cpython-312.pyc +0 -0
  10. multisite/__pycache__/__init__.cpython-39.pyc +0 -0
  11. multisite/__pycache__/__version__.cpython-310.pyc +0 -0
  12. multisite/__pycache__/__version__.cpython-311.pyc +0 -0
  13. multisite/__pycache__/__version__.cpython-312.pyc +0 -0
  14. multisite/__pycache__/__version__.cpython-39.pyc +0 -0
  15. multisite/__pycache__/apps.cpython-312.pyc +0 -0
  16. multisite/__pycache__/exceptions.cpython-312.pyc +0 -0
  17. multisite/__pycache__/forms.cpython-310.pyc +0 -0
  18. multisite/__pycache__/forms.cpython-311.pyc +0 -0
  19. multisite/__pycache__/forms.cpython-312.pyc +0 -0
  20. multisite/__pycache__/forms.cpython-39.pyc +0 -0
  21. multisite/__pycache__/hacks.cpython-310.pyc +0 -0
  22. multisite/__pycache__/hacks.cpython-311.pyc +0 -0
  23. multisite/__pycache__/hacks.cpython-312.pyc +0 -0
  24. multisite/__pycache__/hacks.cpython-39.pyc +0 -0
  25. multisite/__pycache__/test_settings.cpython-310-pytest-6.2.5.pyc +0 -0
  26. multisite/__pycache__/test_settings.cpython-311-pytest-8.1.1.pyc +0 -0
  27. multisite/__pycache__/test_settings.cpython-312-pytest-7.4.4.pyc +0 -0
  28. multisite/__pycache__/test_settings.cpython-312-pytest-8.1.1.pyc +0 -0
  29. multisite/__pycache__/test_settings.cpython-39-pytest-6.2.5.pyc +0 -0
  30. multisite/__pycache__/tests.cpython-310-pytest-6.2.5.pyc +0 -0
  31. multisite/__pycache__/tests.cpython-311-pytest-8.1.1.pyc +0 -0
  32. multisite/__pycache__/tests.cpython-312-pytest-7.4.4.pyc +0 -0
  33. multisite/__pycache__/tests.cpython-312-pytest-8.1.1.pyc +0 -0
  34. multisite/__pycache__/tests.cpython-39-pytest-6.2.5.pyc +0 -0
  35. multisite/__pycache__/threadlocals.cpython-310.pyc +0 -0
  36. multisite/__pycache__/threadlocals.cpython-311.pyc +0 -0
  37. multisite/__pycache__/threadlocals.cpython-312.pyc +0 -0
  38. multisite/__pycache__/threadlocals.cpython-39.pyc +0 -0
  39. multisite/__pycache__/utils.cpython-312.pyc +0 -0
  40. multisite/__version__.py +1 -0
  41. multisite/admin/__init__.py +3 -0
  42. multisite/admin/__pycache__/__init__.cpython-312.pyc +0 -0
  43. multisite/admin/__pycache__/alias_admin.cpython-312.pyc +0 -0
  44. multisite/admin/__pycache__/multisite_changelist.cpython-312.pyc +0 -0
  45. multisite/admin/__pycache__/multisite_model_admin.cpython-312.pyc +0 -0
  46. multisite/admin/alias_admin.py +58 -0
  47. multisite/admin/multisite_changelist.py +64 -0
  48. multisite/admin/multisite_model_admin.py +161 -0
  49. multisite/apps.py +29 -0
  50. multisite/exceptions.py +57 -0
  51. multisite/forms.py +22 -0
  52. multisite/hacks.py +163 -0
  53. multisite/management/__init__.py +0 -0
  54. multisite/management/__pycache__/__init__.cpython-310.pyc +0 -0
  55. multisite/management/__pycache__/__init__.cpython-311.pyc +0 -0
  56. multisite/management/__pycache__/__init__.cpython-312.pyc +0 -0
  57. multisite/management/__pycache__/__init__.cpython-39.pyc +0 -0
  58. multisite/management/commands/__init__.py +0 -0
  59. multisite/management/commands/__pycache__/__init__.cpython-310.pyc +0 -0
  60. multisite/management/commands/__pycache__/__init__.cpython-311.pyc +0 -0
  61. multisite/management/commands/__pycache__/__init__.cpython-312.pyc +0 -0
  62. multisite/management/commands/__pycache__/__init__.cpython-39.pyc +0 -0
  63. multisite/management/commands/__pycache__/update_public_suffix_list.cpython-310.pyc +0 -0
  64. multisite/management/commands/__pycache__/update_public_suffix_list.cpython-311.pyc +0 -0
  65. multisite/management/commands/__pycache__/update_public_suffix_list.cpython-312.pyc +0 -0
  66. multisite/management/commands/__pycache__/update_public_suffix_list.cpython-39.pyc +0 -0
  67. multisite/management/commands/update_public_suffix_list.py +33 -0
  68. multisite/middleware/__init__.py +4 -0
  69. multisite/middleware/__pycache__/__init__.cpython-312.pyc +0 -0
  70. multisite/middleware/__pycache__/cookie_domain_middleware.cpython-312.pyc +0 -0
  71. multisite/middleware/__pycache__/dynamic_site_middleware.cpython-312.pyc +0 -0
  72. multisite/middleware/cookie_domain_middleware.py +72 -0
  73. multisite/middleware/dynamic_site_middleware.py +195 -0
  74. multisite/migrations/0001_initial.py +71 -0
  75. multisite/migrations/0002_alter_alias_id_alter_alias_is_canonical.py +32 -0
  76. multisite/migrations/0003_alter_alias_options_alter_alias_unique_together_and_more.py +41 -0
  77. multisite/migrations/__init__.py +0 -0
  78. multisite/migrations/__pycache__/0001_initial.cpython-310.pyc +0 -0
  79. multisite/migrations/__pycache__/0001_initial.cpython-311.pyc +0 -0
  80. multisite/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  81. multisite/migrations/__pycache__/0001_initial.cpython-39.pyc +0 -0
  82. multisite/migrations/__pycache__/0002_alter_alias_id_alter_alias_is_canonical.cpython-310.pyc +0 -0
  83. multisite/migrations/__pycache__/0002_alter_alias_id_alter_alias_is_canonical.cpython-311.pyc +0 -0
  84. multisite/migrations/__pycache__/0002_alter_alias_id_alter_alias_is_canonical.cpython-312.pyc +0 -0
  85. multisite/migrations/__pycache__/0002_alter_alias_id_alter_alias_is_canonical.cpython-39.pyc +0 -0
  86. multisite/migrations/__pycache__/0003_alias_multisite_alias_canon_site_uniq.cpython-311.pyc +0 -0
  87. multisite/migrations/__pycache__/0003_alter_alias_options_alter_alias_unique_together_and_more.cpython-312.pyc +0 -0
  88. multisite/migrations/__pycache__/__init__.cpython-310.pyc +0 -0
  89. multisite/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
  90. multisite/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  91. multisite/migrations/__pycache__/__init__.cpython-39.pyc +0 -0
  92. multisite/models/__init__.py +3 -0
  93. multisite/models/__pycache__/__init__.cpython-312.pyc +0 -0
  94. multisite/models/__pycache__/alias.cpython-312.pyc +0 -0
  95. multisite/models/__pycache__/managers.cpython-312.pyc +0 -0
  96. multisite/models/__pycache__/signals.cpython-312.pyc +0 -0
  97. multisite/models/__pycache__/validators.cpython-312.pyc +0 -0
  98. multisite/models/alias.py +110 -0
  99. multisite/models/managers.py +196 -0
  100. multisite/models/signals.py +36 -0
  101. multisite/models/validators.py +14 -0
  102. multisite/tests/__init__.py +0 -0
  103. multisite/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  104. multisite/tests/__pycache__/get_test_allowed_hosts.cpython-312.pyc +0 -0
  105. multisite/tests/__pycache__/get_test_http_response.cpython-312.pyc +0 -0
  106. multisite/tests/__pycache__/test_settings.cpython-312-pytest-8.1.1.pyc +0 -0
  107. multisite/tests/__pycache__/test_settings.cpython-312.pyc +0 -0
  108. multisite/tests/__pycache__/tests.cpython-312-pytest-8.1.1.pyc +0 -0
  109. multisite/tests/get_test_allowed_hosts.py +14 -0
  110. multisite/tests/get_test_http_response.py +13 -0
  111. multisite/tests/hosts.py +46 -0
  112. multisite/tests/test_settings.py +23 -0
  113. multisite/tests/tests/__init__.py +0 -0
  114. multisite/tests/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  115. multisite/tests/tests/__pycache__/request_factory.cpython-312.pyc +0 -0
  116. multisite/tests/tests/__pycache__/test_alias.cpython-312.pyc +0 -0
  117. multisite/tests/tests/__pycache__/test_contrib_site.cpython-312.pyc +0 -0
  118. multisite/tests/tests/__pycache__/test_cookie_domain_middleware.cpython-312.pyc +0 -0
  119. multisite/tests/tests/__pycache__/test_dynamic_site_middleware.cpython-312.pyc +0 -0
  120. multisite/tests/tests/__pycache__/test_site_cache.cpython-312.pyc +0 -0
  121. multisite/tests/tests/__pycache__/test_site_domain.cpython-312.pyc +0 -0
  122. multisite/tests/tests/__pycache__/test_site_id.cpython-312.pyc +0 -0
  123. multisite/tests/tests/__pycache__/test_template_loader.cpython-312.pyc +0 -0
  124. multisite/tests/tests/request_factory.py +12 -0
  125. multisite/tests/tests/test_alias.py +272 -0
  126. multisite/tests/tests/test_contrib_site.py +21 -0
  127. multisite/tests/tests/test_cookie_domain_middleware.py +196 -0
  128. multisite/tests/tests/test_dynamic_site_middleware.py +262 -0
  129. multisite/tests/tests/test_site_cache.py +127 -0
  130. multisite/tests/tests/test_site_domain.py +24 -0
  131. multisite/tests/tests/test_site_id.py +76 -0
  132. multisite/tests/tests/test_template_loader.py +49 -0
  133. multisite/threadlocals.py +149 -0
  134. multisite/utils.py +165 -0
  135. multisite_app/__init__.py +0 -0
  136. multisite_app/apps.py +6 -0
  137. multisite_app/urls.py +5 -0
  138. 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
@@ -0,0 +1,3 @@
1
+ from .alias import Alias
2
+ from .signals import post_save_site_created, pre_save_site_domain_changed
3
+ from .validators import validate_1_or_none, validate_true_or_none
@@ -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