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,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from django.contrib.admin.views.main import ChangeList
|
|
6
|
+
from django.contrib.sites.models import Site
|
|
7
|
+
from django.db import models
|
|
8
|
+
|
|
9
|
+
from ..utils import get_user_sites
|
|
10
|
+
|
|
11
|
+
__all__ = ["MultisiteChangeList"]
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from django.contrib.auth.models import User
|
|
15
|
+
from django.core.handlers.wsgi import WSGIRequest as BaseWSGIRequest
|
|
16
|
+
|
|
17
|
+
class UserProfile(models.Model):
|
|
18
|
+
user = models.ForeignKey(User, on_delete=models.PROTECT)
|
|
19
|
+
sites = models.ManyToManyField(Site, blank=True)
|
|
20
|
+
|
|
21
|
+
class WSGIRequest(BaseWSGIRequest):
|
|
22
|
+
user: User
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MultisiteChangeList(ChangeList):
|
|
26
|
+
"""
|
|
27
|
+
A ChangeList like the built-in admin one, but it excludes site filters for
|
|
28
|
+
sites you're not associated with, unless you're a super-user.
|
|
29
|
+
|
|
30
|
+
At this point, it's probably fragile, given its reliance on Django
|
|
31
|
+
internals.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def get_filters(self, request: WSGIRequest, *args, **kwargs) -> tuple[list, bool]:
|
|
35
|
+
"""
|
|
36
|
+
This might be considered a fragile function, since it relies on a
|
|
37
|
+
fair bit of Django's internals.
|
|
38
|
+
"""
|
|
39
|
+
get_filters = super().get_filters
|
|
40
|
+
filter_specs, has_filter_specs = get_filters(request)
|
|
41
|
+
if request.user.is_superuser or not has_filter_specs:
|
|
42
|
+
return filter_specs, has_filter_specs
|
|
43
|
+
new_filter_specs = []
|
|
44
|
+
user_sites = frozenset(get_user_sites(request).values_list("pk", "domain"))
|
|
45
|
+
for filter_spec in filter_specs:
|
|
46
|
+
try:
|
|
47
|
+
try:
|
|
48
|
+
remote_model = filter_spec.field.remote_field.model
|
|
49
|
+
except AttributeError:
|
|
50
|
+
remote_model = filter_spec.field.related_model
|
|
51
|
+
except AttributeError:
|
|
52
|
+
new_filter_specs.append(filter_spec)
|
|
53
|
+
continue
|
|
54
|
+
if remote_model is not Site:
|
|
55
|
+
new_filter_specs.append(filter_spec)
|
|
56
|
+
continue
|
|
57
|
+
lookup_choices = frozenset(filter_spec.lookup_choices) & user_sites
|
|
58
|
+
if len(lookup_choices) > 1:
|
|
59
|
+
# put the choices back into the form they came in
|
|
60
|
+
filter_spec.lookup_choices = list(lookup_choices)
|
|
61
|
+
filter_spec.lookup_choices.sort()
|
|
62
|
+
new_filter_specs.append(filter_spec)
|
|
63
|
+
|
|
64
|
+
return new_filter_specs, bool(new_filter_specs)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from django.contrib import admin
|
|
6
|
+
from django.contrib.sites.models import Site
|
|
7
|
+
from django.db import models
|
|
8
|
+
|
|
9
|
+
from ..utils import get_user_sites
|
|
10
|
+
from .multisite_changelist import MultisiteChangeList
|
|
11
|
+
|
|
12
|
+
__all__ = ["MultisiteModelAdmin"]
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from django.contrib.auth.models import User
|
|
16
|
+
from django.core.handlers.wsgi import WSGIRequest as BaseWSGIRequest
|
|
17
|
+
from django.db.models import QuerySet
|
|
18
|
+
|
|
19
|
+
class UserProfile(models.Model):
|
|
20
|
+
user = models.ForeignKey(User, on_delete=models.PROTECT)
|
|
21
|
+
sites = models.ManyToManyField(Site, blank=True)
|
|
22
|
+
|
|
23
|
+
class WSGIRequest(BaseWSGIRequest):
|
|
24
|
+
user: User
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MultisiteModelAdmin(admin.ModelAdmin):
|
|
28
|
+
"""
|
|
29
|
+
A very helpful ModelAdmin class for handling multi-site django
|
|
30
|
+
applications.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
filter_sites_by_current_object = False
|
|
34
|
+
|
|
35
|
+
def __init__(self, model, admin_site):
|
|
36
|
+
self.object_sites: tuple | None = None
|
|
37
|
+
super().__init__(model, admin_site)
|
|
38
|
+
|
|
39
|
+
def get_queryset(self, request: WSGIRequest) -> QuerySet:
|
|
40
|
+
"""
|
|
41
|
+
Filters lists of items to items belonging to sites assigned to the
|
|
42
|
+
current member.
|
|
43
|
+
|
|
44
|
+
Additionally, for cases where the field containing a reference
|
|
45
|
+
to 'site' or 'sites' isn't immediate -- one can supply the
|
|
46
|
+
ModelAdmin class with a list of fields to check the site of:
|
|
47
|
+
|
|
48
|
+
- multisite_filter_fields
|
|
49
|
+
A list of paths to a 'site' or 'sites' field on a related model to
|
|
50
|
+
filter the queryset on.
|
|
51
|
+
|
|
52
|
+
(As long as you're not a superuser)
|
|
53
|
+
"""
|
|
54
|
+
qs = super().get_queryset(request)
|
|
55
|
+
if request.user.is_superuser:
|
|
56
|
+
return qs
|
|
57
|
+
user_sites = get_user_sites(request)
|
|
58
|
+
if hasattr(qs.model, "site"):
|
|
59
|
+
qs = qs.filter(site__in=user_sites)
|
|
60
|
+
elif hasattr(qs.model, "sites"):
|
|
61
|
+
qs = qs.filter(sites__in=user_sites)
|
|
62
|
+
|
|
63
|
+
if hasattr(self, "multisite_filter_fields"):
|
|
64
|
+
for field in self.multisite_filter_fields:
|
|
65
|
+
qkwargs = {"{field}__in".format(field=field): user_sites}
|
|
66
|
+
qs = qs.filter(**qkwargs)
|
|
67
|
+
|
|
68
|
+
return qs
|
|
69
|
+
|
|
70
|
+
def add_view(self, request: WSGIRequest, form_url: str = "", extra_context: dict = None):
|
|
71
|
+
if self.filter_sites_by_current_object:
|
|
72
|
+
if hasattr(self.model, "site") or hasattr(self.model, "sites"):
|
|
73
|
+
self.object_sites = tuple()
|
|
74
|
+
return super().add_view(request, form_url, extra_context)
|
|
75
|
+
|
|
76
|
+
def change_view(
|
|
77
|
+
self, request: WSGIRequest, object_id, form_url: str = "", extra_context: dict = None
|
|
78
|
+
):
|
|
79
|
+
if self.filter_sites_by_current_object:
|
|
80
|
+
object_instance = self.get_object(request, object_id)
|
|
81
|
+
try:
|
|
82
|
+
self.object_sites = object_instance.sites.values_list("pk", flat=True)
|
|
83
|
+
except AttributeError:
|
|
84
|
+
try:
|
|
85
|
+
self.object_sites = (object_instance.site.pk,)
|
|
86
|
+
except AttributeError:
|
|
87
|
+
pass # assume the object doesn't belong to a site
|
|
88
|
+
return super().change_view(request, object_id, form_url, extra_context)
|
|
89
|
+
|
|
90
|
+
def handle_multisite_foreign_keys(self, db_field, request, **kwargs):
|
|
91
|
+
"""
|
|
92
|
+
Filters the foreignkey queryset for fields referencing other models
|
|
93
|
+
to those models assigned to a site belonging to the current member
|
|
94
|
+
(if they aren't a superuser), and (optionally) belonging to the same
|
|
95
|
+
site as the current object.
|
|
96
|
+
|
|
97
|
+
Also prevents (non-super) users from assigning objects to sites that
|
|
98
|
+
they are not members of.
|
|
99
|
+
|
|
100
|
+
If the foreign key does not have a site/sites field directly, you can
|
|
101
|
+
specify a path to a site/sites field to filter on by setting the key:
|
|
102
|
+
|
|
103
|
+
- multisite_foreign_key_site_path
|
|
104
|
+
|
|
105
|
+
to a dictionary pointing specific foreign key field instances
|
|
106
|
+
from their model to the site field to filter on something
|
|
107
|
+
like:
|
|
108
|
+
|
|
109
|
+
multisite_indirect_foreign_key_path = {
|
|
110
|
+
'plan_instance': 'plan__site'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for a field named 'plan_instance' referencing a model with a
|
|
114
|
+
foreign key named 'plan' having a foreign key to 'site'.
|
|
115
|
+
|
|
116
|
+
To filter the FK queryset to the same sites the current object belongs
|
|
117
|
+
to, simply set `filter_sites_by_current_object` to `True`.
|
|
118
|
+
|
|
119
|
+
Caveats:
|
|
120
|
+
|
|
121
|
+
1) If you're adding an object that belongs to a site (or sites),
|
|
122
|
+
and you've set `self.limit_sites_by_current_object = True`,
|
|
123
|
+
then the FK fields to objects that also belong to a site won't show
|
|
124
|
+
any objects. This is due to filtering on an empty queryset.
|
|
125
|
+
"""
|
|
126
|
+
user_sites = get_user_sites(request)
|
|
127
|
+
try:
|
|
128
|
+
remote_model = db_field.remote_field.model
|
|
129
|
+
except AttributeError:
|
|
130
|
+
remote_model = db_field.related_model
|
|
131
|
+
if hasattr(remote_model, "site"):
|
|
132
|
+
kwargs["queryset"] = remote_model._default_manager.filter(site__in=user_sites)
|
|
133
|
+
if hasattr(remote_model, "sites"):
|
|
134
|
+
kwargs["queryset"] = remote_model._default_manager.filter(sites__in=user_sites)
|
|
135
|
+
if db_field.name == "site" or db_field.name == "sites":
|
|
136
|
+
kwargs["queryset"] = user_sites
|
|
137
|
+
if (
|
|
138
|
+
hasattr(self, "multisite_indirect_foreign_key_path")
|
|
139
|
+
and db_field.name in self.multisite_indirect_foreign_key_path.keys()
|
|
140
|
+
):
|
|
141
|
+
fkey = self.multisite_indirect_foreign_key_path[db_field.name]
|
|
142
|
+
kwargs["queryset"] = remote_model._default_manager.filter(**{fkey: user_sites})
|
|
143
|
+
|
|
144
|
+
return kwargs
|
|
145
|
+
|
|
146
|
+
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
|
147
|
+
kwargs = self.handle_multisite_foreign_keys(db_field, request, **kwargs)
|
|
148
|
+
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
|
149
|
+
|
|
150
|
+
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
|
151
|
+
kwargs = self.handle_multisite_foreign_keys(db_field, request, **kwargs)
|
|
152
|
+
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
|
153
|
+
|
|
154
|
+
def get_changelist(self, request, **kwargs):
|
|
155
|
+
"""
|
|
156
|
+
Restrict the site filter (if there is one) to sites you are
|
|
157
|
+
associated with, or remove it entirely if you're just
|
|
158
|
+
associated with one site. Unless you're a super-user, of
|
|
159
|
+
course.
|
|
160
|
+
"""
|
|
161
|
+
return MultisiteChangeList
|
multisite/apps.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from django.apps import AppConfig as DjangoAppConfig
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.db.models.signals import post_migrate
|
|
4
|
+
|
|
5
|
+
from multisite.hacks import use_framework_for_site_cache
|
|
6
|
+
from multisite.utils import create_or_sync_canonical_from_all_sites
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def post_migrate_sync_alias(apps=None, **kwargs):
|
|
10
|
+
"""Syncs canonical Alias objects for all existing Site objects."""
|
|
11
|
+
create_or_sync_canonical_from_all_sites(apps)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AppConfig(DjangoAppConfig):
|
|
15
|
+
name = "multisite"
|
|
16
|
+
verbose_name = "Multisite"
|
|
17
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
18
|
+
|
|
19
|
+
def import_models(self):
|
|
20
|
+
use_framework_for_site_cache()
|
|
21
|
+
super().import_models()
|
|
22
|
+
|
|
23
|
+
def ready(self):
|
|
24
|
+
if not getattr(settings, "MULTISITE_REGISTER_POST_MIGRATE_SYNC_ALIAS", True):
|
|
25
|
+
post_migrate.connect(
|
|
26
|
+
post_migrate_sync_alias,
|
|
27
|
+
sender=self,
|
|
28
|
+
dispatch_uid="multisite.post_migrate_sync_alias",
|
|
29
|
+
)
|
multisite/exceptions.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MultisiteError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MultisiteSiteDoesNotExist(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MultisiteCacheError(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MultisiteFallbackError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MultisiteDisallowedHost(Exception):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MultisiteInvalidHostError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MultisiteServerError(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MultisiteCookieDomainDepthError(Exception):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def debug_raise_disallowed_host_exception(e):
|
|
37
|
+
if getattr(settings, "MULTISITE_DEBUG", None):
|
|
38
|
+
raise MultisiteDisallowedHost(
|
|
39
|
+
f"DisallowedHost. To silence this exception, set MULTISITE_DEBUG=False. Got {e}."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def debug_raise_cache_missed_exception(netloc, alias):
|
|
44
|
+
if getattr(settings, "MULTISITE_DEBUG", None):
|
|
45
|
+
raise MultisiteCacheError(
|
|
46
|
+
f"Cache missed. Got location {netloc} and Alias {alias}. "
|
|
47
|
+
"To silence this exception, set MULTISITE_DEBUG=False."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def debug_check_status_code(response, **kwargs) -> None:
|
|
52
|
+
if response.status_code == 500 and getattr(settings, "MULTISITE_DEBUG", None):
|
|
53
|
+
raise MultisiteServerError(
|
|
54
|
+
f"HttpResponse 500. {kwargs}. Got {response}."
|
|
55
|
+
"To silence this exception, set MULTISITE_DEBUG=False."
|
|
56
|
+
)
|
|
57
|
+
return None
|
multisite/forms.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import absolute_import, unicode_literals
|
|
2
|
+
|
|
3
|
+
from django.contrib.sites.admin import SiteAdmin
|
|
4
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
5
|
+
|
|
6
|
+
from .models import Alias
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SiteForm(SiteAdmin.form):
|
|
10
|
+
def clean_domain(self):
|
|
11
|
+
domain = self.cleaned_data["domain"]
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
alias = Alias.objects.get(domain=domain)
|
|
15
|
+
except ObjectDoesNotExist:
|
|
16
|
+
# New Site that doesn't clobber an Alias
|
|
17
|
+
return domain
|
|
18
|
+
|
|
19
|
+
if alias.site_id == self.instance.pk and alias.is_canonical:
|
|
20
|
+
return domain
|
|
21
|
+
|
|
22
|
+
raise ValidationError('Cannot overwrite non-canonical Alias: "%s"' % alias.domain)
|
multisite/hacks.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
7
|
+
from django.db.models.signals import post_delete, post_save
|
|
8
|
+
|
|
9
|
+
__all__ = ["use_framework_for_site_cache"]
|
|
10
|
+
|
|
11
|
+
from multisite.exceptions import MultisiteSiteDoesNotExist
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def use_framework_for_site_cache():
|
|
15
|
+
"""Patches sites app to use the caching framework instead of a dict.
|
|
16
|
+
|
|
17
|
+
This patch has to exist because SITE_CACHE is normally a dict,
|
|
18
|
+
which is local only to the process. When running multiple
|
|
19
|
+
processes, a change to a Site will not be reflected across other
|
|
20
|
+
ones.
|
|
21
|
+
"""
|
|
22
|
+
from django.contrib.sites import models
|
|
23
|
+
|
|
24
|
+
# Patch the SITE_CACHE
|
|
25
|
+
site_cache = SiteCache()
|
|
26
|
+
models.SITE_CACHE = DictCache(site_cache)
|
|
27
|
+
|
|
28
|
+
# Patch the SiteManager class
|
|
29
|
+
models.SiteManager.clear_cache = site_manager_clear_cache
|
|
30
|
+
models.SiteManager._get_site_by_id = site_manager_get_site_by_id
|
|
31
|
+
|
|
32
|
+
# Hooks to update SiteCache
|
|
33
|
+
post_save.connect(site_cache._site_changed_hook, sender=models.Site)
|
|
34
|
+
post_delete.connect(site_cache._site_deleted_hook, sender=models.Site)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def site_manager_clear_cache(self):
|
|
38
|
+
"""Clears the ``Site`` object cache.
|
|
39
|
+
|
|
40
|
+
Override `SiteManager.clear_cache` so it doesn't clobber
|
|
41
|
+
SITE_CACHE
|
|
42
|
+
"""
|
|
43
|
+
models = sys.modules.get(self.__class__.__module__)
|
|
44
|
+
models.SITE_CACHE.clear()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def site_manager_get_site_by_id(self, site_id):
|
|
48
|
+
"""Patch _get_site_by_id to retrieve the site from the cache at
|
|
49
|
+
the beginning of the method to avoid a race condition.
|
|
50
|
+
|
|
51
|
+
Override SiteManager._get_site_by_id
|
|
52
|
+
"""
|
|
53
|
+
models = sys.modules.get(self.__class__.__module__)
|
|
54
|
+
site = models.SITE_CACHE.get(site_id)
|
|
55
|
+
if site is None:
|
|
56
|
+
try:
|
|
57
|
+
site = self.get(pk=site_id)
|
|
58
|
+
except ObjectDoesNotExist as e:
|
|
59
|
+
raise MultisiteSiteDoesNotExist(f"{e} Got site_id={site_id}.")
|
|
60
|
+
models.SITE_CACHE[site_id] = site
|
|
61
|
+
return site
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SiteCache:
|
|
65
|
+
"""Wrapper for SITE_CACHE that assigns a key_prefix."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, cache=None):
|
|
68
|
+
from django.core.cache import caches
|
|
69
|
+
|
|
70
|
+
if cache is None:
|
|
71
|
+
cache_alias = getattr(settings, "CACHE_MULTISITE_ALIAS", "default")
|
|
72
|
+
self._key_prefix = getattr(
|
|
73
|
+
settings,
|
|
74
|
+
"CACHE_MULTISITE_KEY_PREFIX",
|
|
75
|
+
settings.CACHES[cache_alias].get("KEY_PREFIX", ""),
|
|
76
|
+
)
|
|
77
|
+
cache = caches[cache_alias]
|
|
78
|
+
else:
|
|
79
|
+
self._key_prefix = getattr(
|
|
80
|
+
settings, "CACHE_MULTISITE_KEY_PREFIX", cache.key_prefix
|
|
81
|
+
)
|
|
82
|
+
self._cache = cache
|
|
83
|
+
|
|
84
|
+
def _get_cache_key(self, key):
|
|
85
|
+
return "sites.%s.%s" % (self.key_prefix, key)
|
|
86
|
+
|
|
87
|
+
def _clean_site(self, site):
|
|
88
|
+
# Force site.id to be an int, not a SiteID object.
|
|
89
|
+
site.id = int(site.id)
|
|
90
|
+
return site
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def key_prefix(self):
|
|
94
|
+
return self._key_prefix
|
|
95
|
+
|
|
96
|
+
def get(self, key, *args, **kwargs):
|
|
97
|
+
return self._cache.get(key=self._get_cache_key(key), *args, **kwargs)
|
|
98
|
+
|
|
99
|
+
def set(self, key, value, *args, **kwargs):
|
|
100
|
+
self._cache.set(
|
|
101
|
+
key=self._get_cache_key(key), value=self._clean_site(value), *args, **kwargs
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def delete(self, key, *args, **kwargs):
|
|
105
|
+
self._cache.delete(key=self._get_cache_key(key), *args, **kwargs)
|
|
106
|
+
|
|
107
|
+
def __contains__(self, key, *args, **kwargs):
|
|
108
|
+
return self._cache.__contains__(key=self._get_cache_key(key), *args, **kwargs)
|
|
109
|
+
|
|
110
|
+
def clear(self, *args, **kwargs):
|
|
111
|
+
self._cache.clear(*args, **kwargs)
|
|
112
|
+
|
|
113
|
+
def _site_changed_hook(self, sender, instance, raw, *args, **kwargs):
|
|
114
|
+
if raw:
|
|
115
|
+
return
|
|
116
|
+
self.set(key=instance.pk, value=instance)
|
|
117
|
+
|
|
118
|
+
def _site_deleted_hook(self, sender, instance, *args, **kwargs):
|
|
119
|
+
self.delete(key=instance.pk)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class DictCache:
|
|
123
|
+
"""Add dictionary protocol to `django.core.cache.backends.
|
|
124
|
+
BaseCache`.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, cache):
|
|
128
|
+
self._cache = cache
|
|
129
|
+
|
|
130
|
+
def __getitem__(self, key):
|
|
131
|
+
"""x.__getitem__(y) <==> x[y]"""
|
|
132
|
+
hash(key) # Raise TypeError if unhashable
|
|
133
|
+
result = self._cache.get(key=key)
|
|
134
|
+
if result is None:
|
|
135
|
+
raise KeyError(key)
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
def __setitem__(self, key, value):
|
|
139
|
+
"""x.__setitem__(i, y) <==> x[i]=y"""
|
|
140
|
+
hash(key) # Raise TypeError if unhashable
|
|
141
|
+
self._cache.set(key=key, value=value)
|
|
142
|
+
|
|
143
|
+
def __delitem__(self, key):
|
|
144
|
+
"""x.__delitem__(y) <==> del x[y]"""
|
|
145
|
+
hash(key) # Raise TypeError if unhashable
|
|
146
|
+
self._cache.delete(key=key)
|
|
147
|
+
|
|
148
|
+
def __contains__(self, item):
|
|
149
|
+
"""D.__contains__(k) -> True if D has a key k, else False."""
|
|
150
|
+
hash(item) # Raise TypeError if unhashable
|
|
151
|
+
return self._cache.__contains__(key=item)
|
|
152
|
+
|
|
153
|
+
def clear(self):
|
|
154
|
+
"""D.clear() -> None. Remove all items from D."""
|
|
155
|
+
self._cache.clear()
|
|
156
|
+
|
|
157
|
+
def get(self, key, default=None, version=None):
|
|
158
|
+
"""D.key(k[, d]) -> k if D has a key k, else d.
|
|
159
|
+
|
|
160
|
+
Defaults to None.
|
|
161
|
+
"""
|
|
162
|
+
hash(key) # Raise TypeError if unhashable
|
|
163
|
+
return self._cache.get(key=key, default=default, version=version)
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function, unicode_literals
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
import tldextract
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.core.management.base import BaseCommand
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Command(BaseCommand):
|
|
12
|
+
def handle(self, **options):
|
|
13
|
+
self.setup_logging(verbosity=options.get("verbosity", 1))
|
|
14
|
+
|
|
15
|
+
cache_dir = getattr(
|
|
16
|
+
settings, "MULTISITE_PUBLIC_SUFFIX_LIST_CACHE_DIR", tempfile.gettempdir()
|
|
17
|
+
)
|
|
18
|
+
self.log("Updating {cache_dir}".format(cache_dir=cache_dir))
|
|
19
|
+
|
|
20
|
+
extract = tldextract.TLDExtract(cache_dir=cache_dir)
|
|
21
|
+
extract.update(fetch_now=True)
|
|
22
|
+
self.log("Done.")
|
|
23
|
+
|
|
24
|
+
def setup_logging(self, verbosity):
|
|
25
|
+
self.verbosity = int(verbosity)
|
|
26
|
+
|
|
27
|
+
# Connect to tldextract's logger
|
|
28
|
+
self.logger = logging.getLogger("tldextract")
|
|
29
|
+
if self.verbosity < 2:
|
|
30
|
+
self.logger.setLevel(logging.CRITICAL)
|
|
31
|
+
|
|
32
|
+
def log(self, msg):
|
|
33
|
+
self.logger.info(msg)
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
import tldextract
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
|
|
8
|
+
__all__ = ["CookieDomainMiddleware"]
|
|
9
|
+
|
|
10
|
+
from multisite.exceptions import MultisiteCookieDomainDepthError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_cookie_domain_depth():
|
|
14
|
+
return getattr(settings, "MULTISITE_COOKIE_DOMAIN_DEPTH", 0)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_public_suffix_list_cache_dir():
|
|
18
|
+
return getattr(settings, "MULTISITE_PUBLIC_SUFFIX_LIST_CACHE_DIR", None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CookieDomainMiddleware:
|
|
22
|
+
def __init__(self, get_response):
|
|
23
|
+
self.get_response = get_response
|
|
24
|
+
try:
|
|
25
|
+
self.depth = int(get_cookie_domain_depth())
|
|
26
|
+
except ValueError:
|
|
27
|
+
raise MultisiteCookieDomainDepthError(
|
|
28
|
+
f"Invalid MULTISITE_COOKIE_DOMAIN_DEPTH. Got {get_cookie_domain_depth()}"
|
|
29
|
+
)
|
|
30
|
+
if self.depth < 0:
|
|
31
|
+
raise MultisiteCookieDomainDepthError(
|
|
32
|
+
f"Invalid MULTISITE_COOKIE_DOMAIN_DEPTH. Got {get_cookie_domain_depth()}"
|
|
33
|
+
)
|
|
34
|
+
self.psl_cache_dir = get_public_suffix_list_cache_dir()
|
|
35
|
+
if self.psl_cache_dir is None:
|
|
36
|
+
self.psl_cache_dir = tempfile.gettempdir()
|
|
37
|
+
self._tldextract = None
|
|
38
|
+
|
|
39
|
+
def __call__(self, request):
|
|
40
|
+
response = self.get_response(request)
|
|
41
|
+
matched = self.match_cookies(response)
|
|
42
|
+
if not matched:
|
|
43
|
+
return response # No cookies to edit
|
|
44
|
+
|
|
45
|
+
parsed = self.tldextract(request.get_host())
|
|
46
|
+
if not parsed.suffix:
|
|
47
|
+
return response # IP address or local path
|
|
48
|
+
if not parsed.domain:
|
|
49
|
+
return response # Only TLD
|
|
50
|
+
|
|
51
|
+
subdomains = parsed.subdomain.split(".") if parsed.subdomain else []
|
|
52
|
+
if not self.depth:
|
|
53
|
+
subdomains = [""]
|
|
54
|
+
elif len(subdomains) < self.depth:
|
|
55
|
+
return response # Not enough subdomain parts
|
|
56
|
+
else:
|
|
57
|
+
subdomains = [""] + subdomains[-self.depth :]
|
|
58
|
+
|
|
59
|
+
domain = ".".join(subdomains + [parsed.domain, parsed.suffix])
|
|
60
|
+
|
|
61
|
+
for morsel in matched:
|
|
62
|
+
morsel["domain"] = domain
|
|
63
|
+
return response
|
|
64
|
+
|
|
65
|
+
def tldextract(self, url):
|
|
66
|
+
if self._tldextract is None:
|
|
67
|
+
self._tldextract = tldextract.TLDExtract(cache_dir=self.psl_cache_dir)
|
|
68
|
+
return self._tldextract(url)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def match_cookies(response):
|
|
72
|
+
return [c for c in response.cookies.values() if not c["domain"]]
|