-
Notifications
You must be signed in to change notification settings - Fork 239
Expand file tree
/
Copy pathct_tracker.py
More file actions
166 lines (131 loc) · 6.07 KB
/
ct_tracker.py
File metadata and controls
166 lines (131 loc) · 6.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# ------------------------------------------------------------------------
#
# ct_tracker.py
# FeinCMS
#
# Created by Martin J. Laubach on 02.10.09.
# Copyright (c) 2009 Martin J. Laubach. All rights reserved.
# Updated in 2011 by Matthias Kestenholz for the 1.3 release.
#
# ------------------------------------------------------------------------
"""
Track the content types for pages. Instead of gathering the content
types present in each page at run time, save the current state at
saving time, thus saving at least one DB query on page delivery.
"""
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import class_prepared, post_save, pre_save
from django.utils.translation import gettext_lazy as _
from feincms import extensions
from feincms.contrib.fields import JSONField
from feincms.models import ContentProxy
INVENTORY_VERSION = 1
_translation_map_cache = {}
# ------------------------------------------------------------------------
class TrackerContentProxy(ContentProxy):
def _fetch_content_type_counts(self):
"""
If an object with an empty _ct_inventory is encountered, compute all
the content types currently used on that object and save the list in
the object itself. Further requests for that object can then access
that information and find out which content types are used without
resorting to multiple selects on different ct tables.
It is therefore important that even an "empty" object does not have an
empty _ct_inventory.
"""
if "counts" not in self._cache:
if (
self.item._ct_inventory
and self.item._ct_inventory.get("_version_", -1) == INVENTORY_VERSION
):
try:
self._cache["counts"] = self._from_inventory(
self.item._ct_inventory
)
except KeyError:
# It's possible that the inventory does not fit together
# with the current models anymore, f.e. because a content
# type has been removed.
pass
if "counts" not in self._cache:
super()._fetch_content_type_counts()
self.item._ct_inventory = self._to_inventory(self._cache["counts"])
self.item.__class__.objects.filter(id=self.item.id).update(
_ct_inventory=self.item._ct_inventory
)
# Run post save handler by hand
if hasattr(self.item, "get_descendants"):
self.item.get_descendants(include_self=False).update(
_ct_inventory=None
)
return self._cache["counts"]
def _translation_map(self):
cls = self.item.__class__
if cls not in _translation_map_cache:
# Prime translation map and cache it in the class. This needs to be
# done late as opposed to at class definition time as not all
# information is ready, especially when we are doing a "syncdb" the
# ContentType table does not yet exist
tmap = {}
model_to_contenttype = ContentType.objects.get_for_models(
*self.item._feincms_content_types
)
for idx, fct in enumerate(self.item._feincms_content_types):
dct = model_to_contenttype[fct]
# Rely on non-negative primary keys
tmap[-dct.id] = idx # From-inventory map
tmap[idx] = dct.id # To-inventory map
_translation_map_cache[cls] = tmap
return _translation_map_cache[cls]
def _from_inventory(self, inventory):
"""
Transforms the inventory from Django's content types to FeinCMS's
ContentProxy counts format.
"""
tmap = self._translation_map()
return {
region: [(pk, tmap[-ct]) for pk, ct in items]
for region, items in inventory.items()
if region != "_version_"
}
def _to_inventory(self, counts):
map = self._translation_map()
inventory = {
region: [(pk, map[ct]) for pk, ct in items]
for region, items in counts.items()
}
inventory["_version_"] = INVENTORY_VERSION
return inventory
# ------------------------------------------------------------------------
def class_prepared_handler(sender, **kwargs):
# It might happen under rare circumstances that not all model classes
# are fully loaded and initialized when the translation map is accessed.
# This leads to (lots of) crashes on the server. Better be safe and
# kill the translation map when any class_prepared signal is received.
_translation_map_cache.clear()
class_prepared.connect(class_prepared_handler)
# ------------------------------------------------------------------------
def tree_post_save_handler(sender, instance, **kwargs):
"""
Clobber the _ct_inventory attribute of this object and all sub-objects
on save.
"""
# TODO: Does not find everything it should when ContentProxy content
# inheritance has been customized.
instance.get_descendants(include_self=True).update(_ct_inventory=None)
# ------------------------------------------------------------------------
def single_pre_save_handler(sender, instance, **kwargs):
"""Clobber the _ct_inventory attribute of this object"""
instance._ct_inventory = None
# ------------------------------------------------------------------------
class Extension(extensions.Extension):
def handle_model(self):
self.model.add_to_class(
"_ct_inventory",
JSONField(_("content types"), editable=False, blank=True, null=True),
)
self.model.content_proxy_class = TrackerContentProxy
pre_save.connect(single_pre_save_handler, sender=self.model)
if hasattr(self.model, "get_descendants"):
post_save.connect(tree_post_save_handler, sender=self.model)
# ------------------------------------------------------------------------