# This is necessary because model forms store changes to self.instance in their clean method.
# Mutter mutter.
value = self.instance.value
- self._cached_value_ct = self.instance.value_content_type
+ self._cached_value_ct_id = self.instance.value_content_type_id
self._cached_value = value
# If there is a value, pull in its fields.
def save(self, *args, **kwargs):
# At this point, the cleaned_data has already been stored on self.instance.
- if self.instance.value_content_type != self._cached_value_ct:
+ if self.instance.value_content_type_id != self._cached_value_ct_id:
# The value content type has changed. Clear the old value, if there was one.
if self._cached_value:
self._cached_value.delete()
# Now create a new value instance so that on next instantiation, the form will
# know what fields to add.
- if self.instance.value_content_type is not None:
- self.instance.value = self.instance.value_content_type.model_class().objects.create()
+ if self.instance.value_content_type_id is not None:
+ self.instance.value = ContentType.objects.get_for_id(self.instance.value_content_type_id).model_class().objects.create()
elif self.instance.value is not None:
# The value content type is the same, but one of the value fields has changed.
def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
try:
- ct = ContentType.objects.get(app_label=app_label, model=model)
+ ct = ContentType.objects.get_by_natural_key(app_label, model)
location = ct.model_class()._default_manager.get(pk=pk)
except ObjectDoesNotExist:
raise Http404
url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
)
if self.article_archive_page:
- urlpatterns += patterns('',
- url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
- )
+ urlpatterns += self.feed_patterns(r'^%s' % self.article_permalink_base, 'get_all_articles', 'article_archive_page', 'articles')
if self.article_permalink_style in 'DMY':
urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
if self.article_permalink_style in 'DM':
#encoding: utf-8
from UserDict import DictMixin
+from hashlib import sha1
+from django.contrib.sites.models import Site
+from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.urlresolvers import NoReverseMatch
from django.core.validators import RegexValidator, MinValueValidator
class NavigationMapper(object, DictMixin):
"""
- The :class:`NavigationMapper` is a dictionary-like object which allows easy fetching of the root items of a navigation for a node according to a key. The fetching goes through the :class:`NavigationManager` and can thus take advantage of the navigation cache. A :class:`NavigationMapper` instance will be available on each node instance as :attr:`Node.navigation` if :mod:`~philo.contrib.shipherd` is in the :setting:`INSTALLED_APPS`
+ The :class:`NavigationMapper` is a dictionary-like object which allows easy fetching of the root items of a navigation for a node according to a key. A :class:`NavigationMapper` instance will be available on each node instance as :attr:`Node.navigation` if :mod:`~philo.contrib.shipherd` is in the :setting:`INSTALLED_APPS`
"""
def __init__(self, node):
self.node = node
+ self._cache = {}
def __getitem__(self, key):
- return Navigation.objects.get_cache_for(self.node)[key]['root_items']
-
- def keys(self):
- return Navigation.objects.get_cache_for(self.node).keys()
+ if key not in self._cache:
+ try:
+ self._cache[key] = Navigation.objects.get_for_node(self.node, key)
+ except Navigation.DoesNotExist:
+ self._cache[key] = None
+ return self._cache[key]
def navigation(self):
Node.navigation = property(navigation)
-class NavigationCacheQuerySet(models.query.QuerySet):
- """
- This subclass will trigger general cache clearing for Navigation.objects when a mass
- update or deletion is performed. As there is no convenient way to iterate over the
- changed or deleted instances, there's no way to be more precise about what gets cleared.
-
- """
- def update(self, *args, **kwargs):
- super(NavigationCacheQuerySet, self).update(*args, **kwargs)
- Navigation.objects.clear_cache()
-
- def delete(self, *args, **kwargs):
- super(NavigationCacheQuerySet, self).delete(*args, **kwargs)
- Navigation.objects.clear_cache()
-
-
class NavigationManager(models.Manager):
- """
- Since navigation on a site will be hit frequently, is relatively costly to compute, and is changed relatively infrequently, the NavigationManager maintains a cache which maps nodes to navigations.
-
- """
use_for_related = True
- _cache = {}
- def get_query_set(self):
- """
- Returns a :class:`NavigationCacheQuerySet` instance.
-
- """
- return NavigationCacheQuerySet(self.model, using=self._db)
-
- def get_cache_for(self, node, update_targets=True):
- """Returns the navigation cache for a given :class:`.Node`. If update_targets is ``True``, then :meth:`update_targets_for` will be run with the :class:`.Node`."""
- created = False
- if not self.has_cache_for(node):
- self.create_cache_for(node)
- created = True
-
- if update_targets and not created:
- self.update_targets_for(node)
-
- return self.__class__._cache[self.db][node]
-
- def has_cache_for(self, node):
- """Returns ``True`` if a cache exists for the :class:`.Node` and ``False`` otherwise."""
- return self.db in self.__class__._cache and node in self.__class__._cache[self.db]
-
- def create_cache_for(self, node):
- """This method loops through the :class:`.Node`\ s ancestors and caches all unique navigation keys."""
- ancestors = node.get_ancestors(ascending=True, include_self=True)
-
- nodes_to_cache = []
-
- for node in ancestors:
- if self.has_cache_for(node):
- cache = self.get_cache_for(node).copy()
- break
- else:
- nodes_to_cache.insert(0, node)
- else:
- cache = {}
-
- for node in nodes_to_cache:
- cache = cache.copy()
- cache.update(self._build_cache_for(node))
- self.__class__._cache.setdefault(self.db, {})[node] = cache
-
- def _build_cache_for(self, node):
- cache = {}
- tree_id_attr = NavigationItem._mptt_meta.tree_id_attr
- level_attr = NavigationItem._mptt_meta.level_attr
-
- for navigation in node.navigation_set.all():
- tree_ids = navigation.roots.values_list(tree_id_attr)
- items = list(NavigationItem.objects.filter(**{'%s__in' % tree_id_attr: tree_ids, '%s__lt' % level_attr: navigation.depth}).order_by('order', 'lft'))
+ def get_for_node(self, node, key):
+ cache_key = self._get_cache_key(node, key)
+ cached = cache.get(cache_key)
+
+ if cached is None:
+ opts = Node._mptt_meta
+ left = getattr(node, opts.left_attr)
+ right = getattr(node, opts.right_attr)
+ tree_id = getattr(node, opts.tree_id_attr)
+ kwargs = {
+ "node__%s__lte" % opts.left_attr: left,
+ "node__%s__gte" % opts.right_attr: right,
+ "node__%s" % opts.tree_id_attr: tree_id
+ }
+ navs = self.filter(key=key, **kwargs).select_related('node').order_by('-node__%s' % opts.level_attr)
+ nav = navs[0]
+ roots = nav.roots.all().select_related('target_node').order_by('order')
+ item_opts = NavigationItem._mptt_meta
+ by_pk = {}
+ tree_ids = []
- root_items = []
+ site_root_node = Site.objects.get_current().root_node
- for item in items:
- item._is_cached = True
-
- if not hasattr(item, '_cached_children'):
- item._cached_children = []
-
- if item.parent:
- # alternatively, if I don't want to force it to a list, I could keep track of
- # instances where the parent hasn't yet been met and do this step later for them.
- # delayed action.
- item.parent = items[items.index(item.parent)]
- if not hasattr(item.parent, '_cached_children'):
- item.parent._cached_children = []
- item.parent._cached_children.append(item)
- else:
- root_items.append(item)
+ for root in roots:
+ by_pk[root.pk] = root
+ tree_ids.append(getattr(root, item_opts.tree_id_attr))
+ root._cached_children = []
+ if root.target_node:
+ root.target_node.get_path(root=site_root_node)
+ root.navigation = nav
- cache[navigation.key] = {
- 'navigation': navigation,
- 'root_items': root_items,
- 'items': items
+ kwargs = {
+ '%s__in' % item_opts.tree_id_attr: tree_ids,
+ '%s__lt' % item_opts.level_attr: nav.depth,
+ '%s__gt' % item_opts.level_attr: 0
}
+ items = NavigationItem.objects.filter(**kwargs).select_related('target_node').order_by('level', 'order')
+ for item in items:
+ by_pk[item.pk] = item
+ item._cached_children = []
+ parent_pk = getattr(item, '%s_id' % item_opts.parent_attr)
+ item.parent = by_pk[parent_pk]
+ item.parent._cached_children.append(item)
+ if item.target_node:
+ item.target_node.get_path(root=site_root_node)
+
+ cached = roots
+ cache.set(cache_key, cached)
- return cache
-
- def clear_cache_for(self, node):
- """Clear the cache for the :class:`.Node` and all its descendants. The navigation for this node has probably changed, and it isn't worth it to figure out which descendants were actually affected by this."""
- if not self.has_cache_for(node):
- # Already cleared.
- return
-
- descendants = node.get_descendants(include_self=True)
- cache = self.__class__._cache[self.db]
- for node in descendants:
- cache.pop(node, None)
+ return cached
- def update_targets_for(self, node):
- """Manually updates the target nodes for the :class:`.Node`'s cache in case something's changed there. This is a less complex operation than rebuilding the :class:`.Node`'s cache."""
- caches = self.__class__._cache[self.db][node].values()
-
- target_pks = set()
-
- for cache in caches:
- target_pks |= set([item.target_node_id for item in cache['items']])
-
- # A distinct query is not strictly necessary. TODO: benchmark the efficiency
- # with/without distinct.
- targets = list(Node.objects.filter(pk__in=target_pks).distinct())
+ def _get_cache_key(self, node, key):
+ opts = Node._mptt_meta
+ left = getattr(node, opts.left_attr)
+ right = getattr(node, opts.right_attr)
+ tree_id = getattr(node, opts.tree_id_attr)
+ parent_id = getattr(node, "%s_id" % opts.parent_attr)
- for cache in caches:
- for item in cache['items']:
- if item.target_node_id:
- item.target_node = targets[targets.index(item.target_node)]
-
- def clear_cache(self):
- """Clears the manager's entire navigation cache."""
- self.__class__._cache.pop(self.db, None)
+ return sha1(unicode(left) + unicode(right) + unicode(tree_id) + unicode(parent_id) + unicode(node.pk) + unicode(key)).hexdigest()
class Navigation(Entity):
#: There is no limit to the depth of a tree of :class:`NavigationItem`\ s, but ``depth`` will limit how much of the tree will be displayed.
depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.")
- def __init__(self, *args, **kwargs):
- super(Navigation, self).__init__(*args, **kwargs)
- self._initial_data = model_to_dict(self)
-
def __unicode__(self):
return "%s[%s]" % (self.node, self.key)
- def _has_changed(self):
- return self._initial_data != model_to_dict(self)
-
- def save(self, *args, **kwargs):
- super(Navigation, self).save(*args, **kwargs)
-
- if self._has_changed():
- Navigation.objects.clear_cache_for(self.node)
- self._initial_data = model_to_dict(self)
-
- def delete(self, *args, **kwargs):
- super(Navigation, self).delete(*args, **kwargs)
- Navigation.objects.clear_cache_for(self.node)
-
class Meta:
unique_together = ('node', 'key')
-class NavigationItemManager(TreeEntityManager):
- use_for_related = True
-
- def get_query_set(self):
- """Returns a :class:`NavigationCacheQuerySet` instance."""
- return NavigationCacheQuerySet(self.model, using=self._db)
-
-
class NavigationItem(TreeEntity, TargetURLModel):
- #: A :class:`NavigationItemManager` instance
- objects = NavigationItemManager()
-
#: A :class:`ForeignKey` to a :class:`Navigation` instance. If this is not null, then the :class:`NavigationItem` will be a root node of the :class:`Navigation` instance.
navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.")
#: The text which will be displayed in the navigation. This is a :class:`CharField` instance with max length 50.
#: The order in which the :class:`NavigationItem` will be displayed.
order = models.PositiveSmallIntegerField(default=0)
- def __init__(self, *args, **kwargs):
- super(NavigationItem, self).__init__(*args, **kwargs)
- self._initial_data = model_to_dict(self)
- self._is_cached = False
-
def get_path(self, root=None, pathsep=u' › ', field='text'):
return super(NavigationItem, self).get_path(root, pathsep, field)
path = property(get_path)
# the same as the request path, check whether the target node is an ancestor
# of the requested node. If so, this is active unless the target node
# is the same as the ``host node`` for this navigation structure.
- try:
- host_node = self.get_root().navigation.node
- except AttributeError:
- pass
- else:
- if self.target_node != host_node and self.target_node.is_ancestor_of(request.node):
- return True
+ root = self
+
+ # The common case will be cached items, whose parents are cached with them.
+ while root.parent is not None:
+ root = root.parent
+
+ host_node_id = root.navigation.node_id
+ if self.target_node.pk != host_node_id and self.target_node.is_ancestor_of(request.node):
+ return True
return False
for child in self.get_children():
if child.is_active(request) or child.has_active_descendants(request):
return True
- return False
-
- def _has_changed(self):
- if model_to_dict(self) == self._initial_data:
- return False
- return True
-
- def _clear_cache(self):
- try:
- root = self.get_root()
- if self.get_level() < root.navigation.depth:
- Navigation.objects.clear_cache_for(self.get_root().navigation.node)
- except AttributeError:
- pass
-
- def save(self, *args, **kwargs):
- super(NavigationItem, self).save(*args, **kwargs)
-
- if self._has_changed():
- self._clear_cache()
-
- def delete(self, *args, **kwargs):
- super(NavigationItem, self).delete(*args, **kwargs)
- self._clear_cache()
\ No newline at end of file
+ return False
\ No newline at end of file
def has_navigation(node, key=None):
"""Returns ``True`` if the node has a :class:`.Navigation` with the given key and ``False`` otherwise. If ``key`` is ``None``, returns whether the node has any :class:`.Navigation`\ s at all."""
try:
- nav = node.navigation
- if key is not None:
- if key in nav and bool(node.navigation[key]):
- return True
- elif key not in node.navigation:
- return False
- return bool(node.navigation)
+ return bool(node.navigation[key])
except:
return False
def navigation_host(node, key):
"""Returns the :class:`.Node` which hosts the :class:`.Navigation` which ``node`` has inherited for ``key``. Returns ``node`` if any exceptions are encountered."""
try:
- return Navigation.objects.filter(node__in=node.get_ancestors(include_self=True), key=key).order_by('-node__level')[0].node
+ return node.navigation[key].node
except:
return node
\ No newline at end of file
.. seealso:: :meth:`get_feed_type`
"""
- urlpatterns = patterns('')
+ feed_patterns = ()
if self.feeds_enabled:
suffixes = [(self.feed_suffix, None)] + [(slug, slug) for slug in registry]
for suffix, feed_type in suffixes:
feed_view = http_not_acceptable(self.feed_view(get_items_attr, reverse_name, feed_type))
feed_pattern = r'%s%s%s$' % (base, "/" if base and base[-1] != "^" else "", suffix)
- urlpatterns += patterns('',
- url(feed_pattern, feed_view, name="%s_%s" % (reverse_name, suffix)),
- )
- urlpatterns += patterns('',
- url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
- )
- return urlpatterns
+ feed_patterns += (url(feed_pattern, feed_view, name="%s_%s" % (reverse_name, suffix)),)
+ feed_patterns += (url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name),)
+ return patterns('', *feed_patterns)
def get_object(self, request, **kwargs):
"""By default, returns the object stored in the attribute named by :attr:`object_attr`. This can be overridden for subclasses that publish different data for different URL parameters. It is part of the :class:`django.contrib.syndication.views.Feed` API."""
'Meta': {'object_name': 'File'},
'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'philo.foreignkeyvalue': {
'Meta': {'object_name': 'ForeignKeyValue'},
'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
},
'philo.node': {
- 'Meta': {'object_name': 'Node'},
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
},
'philo.template': {
- 'Meta': {'object_name': 'Template'},
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
'code': ('philo.models.fields.TemplateField', [], {}),
'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
objects = TreeEntityManager()
parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
- def get_path(self, root=None, pathsep='/', field='slug'):
+ def get_path(self, root=None, pathsep='/', field='pk', memoize=True):
"""
:param root: Only return the path since this object.
:param pathsep: The path separator to use when constructing an instance's path
:param field: The field to pull path information from for each ancestor.
+ :param memoize: Whether to use memoized results. Since, in most cases, the ancestors of a TreeEntity will not change over the course of an instance's lifetime, this defaults to ``True``.
:returns: A string representation of an object's path.
"""
if root == self:
return ''
- if root is None and self.is_root_node():
+ parent_id = getattr(self, "%s_id" % self._mptt_meta.parent_attr)
+ if getattr(root, 'pk', None) == parent_id:
return getattr(self, field, '?')
if root is not None and not self.is_descendant_of(root):
raise AncestorDoesNotExist(root)
+ if memoize:
+ memo_args = (parent_id, getattr(root, 'pk', None), pathsep, getattr(self, field, '?'))
+ try:
+ return self._path_memo[memo_args]
+ except AttributeError:
+ self._path_memo = {}
+ except KeyError:
+ pass
+
qs = self.get_ancestors(include_self=True)
if root is not None:
qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
- return pathsep.join([getattr(parent, field, '?') for parent in qs])
+ path = pathsep.join([getattr(parent, field, '?') for parent in qs])
+
+ if memoize:
+ self._path_memo[memo_args] = path
+
+ return path
path = property(get_path)
def get_attribute_mapper(self, mapper=None):
"""
if mapper is None:
- if self.parent:
+ if getattr(self, "%s_id" % self._mptt_meta.parent_attr):
mapper = TreeAttributeMapper
else:
mapper = AttributeMapper
objects = SlugTreeEntityManager()
slug = models.SlugField(max_length=255)
- def get_path(self, root=None, pathsep='/', field='slug'):
- return super(SlugTreeEntity, self).get_path(root, pathsep, field)
+ def get_path(self, root=None, pathsep='/', field='slug', memoize=True):
+ return super(SlugTreeEntity, self).get_path(root, pathsep, field, memoize)
path = property(get_path)
def clean(self):
- if self.parent is None:
+ if getattr(self, "%s_id" % self._mptt_meta.parent_attr) is None:
try:
self._default_manager.exclude(pk=self.pk).get(slug=self.slug, parent__isnull=True)
except self.DoesNotExist:
@property
def accepts_subpath(self):
"""A property shortcut for :attr:`self.view.accepts_subpath <View.accepts_subpath>`"""
- if self.view:
- return self.view.accepts_subpath
+ if self.view_object_id and self.view_content_type_id:
+ return ContentType.objects.get_for_id(self.view_content_type_id).model_class().accepts_subpath
return False
def handles_subpath(self, subpath):
- if self.view:
- return self.view.handles_subpath(subpath)
+ if self.view_object_id and self.view_content_type_id:
+ return ContentType.objects.get_for_id(self.view_content_type_id).model_class().handles_subpath(subpath)
return False
def render_to_response(self, request, extra_context=None):
"""This is a shortcut method for :meth:`View.render_to_response`"""
- if self.view:
+ if self.view_object_id and self.view_content_type_id:
+ view_model = ContentType.objects.get_for_id(self.view_content_type_id).model_class()
+ self.view = view_model._default_manager.select_related(depth=1).get(pk=self.view_object_id)
return self.view.render_to_response(request, extra_context)
raise Http404
#: A generic relation back to nodes.
nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id')
- #: Property or attribute which defines whether this :class:`View` can handle subpaths. Default: ``False``
+ #: An attribute on the class which defines whether this :class:`View` can handle subpaths. Default: ``False``
accepts_subpath = False
- def handles_subpath(self, subpath):
+ @classmethod
+ def handles_subpath(cls, subpath):
"""Returns True if the :class:`View` handles the given subpath, and False otherwise."""
- if not self.accepts_subpath and subpath != "/":
+ if not cls.accepts_subpath and subpath != "/":
return False
return True
"""Returns urlpatterns that point to views (generally methods on the class). :class:`MultiView`\ s can be thought of as "managing" these subpaths."""
raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
- def handles_subpath(self, subpath):
- if not super(MultiView, self).handles_subpath(subpath):
- return False
- try:
- resolve(subpath, urlconf=self)
- except Http404:
- return False
- return True
-
def actually_render_to_response(self, request, extra_context=None):
"""
Resolves the remaining subpath left after finding this :class:`View`'s node using :attr:`self.urlpatterns <urlpatterns>` and renders the view function (or method) found with the appropriate args and kwargs.
try:
app_label, model = params[3].strip('"').split('.')
- ct = ContentType.objects.get(app_label=app_label, model=model)
+ ct = ContentType.objects.get_by_natural_key(app_label, model)
except ValueError:
raise template.TemplateSyntaxError('"%s" template tag option "with" requires an argument of the form app_label.model (see django.contrib.contenttypes)' % tag)
except ContentType.DoesNotExist:
if option_token == 'references':
try:
app_label, model = remaining_tokens.pop(0).strip('"').split('.')
- references = ContentType.objects.get(app_label=app_label, model=model)
+ references = ContentType.objects.get_by_natural_key(app_label, model)
except IndexError:
raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument specifying a content type' % tag)
except ValueError:
except ValueError:
raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tagname)
try:
- ct = ContentType.objects.get(app_label=app_label, model=model)
+ ct = ContentType.objects.get_by_natural_key(app_label, model)
except ContentType.DoesNotExist:
raise template.TemplateSyntaxError('"%s" template tag requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tagname)
return ct
if self.with_obj is None and self.view_name is None:
url = node.get_absolute_url()
else:
- if not node.view.accepts_subpath:
+ if not node.accepts_subpath:
return settings.TEMPLATE_STRING_IF_INVALID
if self.with_obj is not None:
value_lookups = {}
for a in attributes:
- value_lookups.setdefault(a.value_content_type, []).append(a.value_object_id)
+ value_lookups.setdefault(a.value_content_type_id, []).append(a.value_object_id)
self._attributes_cache[a.key] = a
values_bulk = {}
- for ct, pks in value_lookups.items():
- values_bulk[ct] = ct.model_class().objects.in_bulk(pks)
+ for ct_pk, pks in value_lookups.items():
+ values_bulk[ct_pk] = ContentType.objects.get_for_id(ct_pk).model_class().objects.in_bulk(pks)
- self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type].get(a.value_object_id), 'value', None)) for a in attributes]))
+ self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type_id].get(a.value_object_id), 'value', None)) for a in attributes]))
self._cache_filled = True
def clear_cache(self):