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,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
+ )
@@ -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
File without changes
@@ -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)
@@ -0,0 +1,4 @@
1
+ from .cookie_domain_middleware import CookieDomainMiddleware
2
+ from .dynamic_site_middleware import DynamicSiteMiddleware
3
+
4
+ __all__ = ["CookieDomainMiddleware", "DynamicSiteMiddleware"]
@@ -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"]]