I have written the following custom field:
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import signals
from sitetree.models import Tree, TreeItem
from south.modelsinspector import introspector
class AutoTreeItemField(models.ForeignKey):
def __init__(self, *args, **kwargs):
super(AutoTreeItemField, self).__init__(TreeItem, null=True)
self.date_field = kwargs['date_field']
self.__should_appear = kwargs['should_appear']
self.year_menu_item_url = kwargs['year_menu_item_url']
self.month_menu_item_url = kwargs['month_menu_item_url']
self.item_menu_item_url = kwargs['item_menu_item_url']
self.year_format = kwargs.get('year_format', '%Y')
self.month_format = kwargs.get('month_format', '%B')
self.inmenu = kwargs.get('inmenu', True)
self.inbreadcrumbs = kwargs.get('inbreadcrumbs', True)
self.insitetree = kwargs.get('insitetree', True)
self.item_title = kwargs.get('title', 'title')
def contribute_to_class(self, cls, name):
super(AutoTreeItemField, self).contribute_to_class(cls, name)
# Make this object the descriptor for field access.
setattr(cls, self.name, self)
self.tree = self.__get_or_create_tree(cls._meta.verbose_name_plural.lower())
# Delete menu item after the instance is deleted
signals.post_delete.connect(self.__delete, cls, True)
def pre_save(self, model_instance, add):
super(AutoTreeItemField, self).pre_save(model_instance, add)
if self.should_appear(model_instance):
year_menu_item = self.__get_or_create_year_tree_item(model_instance)
month_menu_item = self.__get_or_create_month_tree_item(model_instance, year_menu_item)
menu_item = self.__save_menu_item(model_instance, month_menu_item)
setattr(model_instance, self.get_attname(), menu_item.id)
return menu_item.id
else:
self.__delete_orphans(model_instance)
return None
def __delete(self, **kwargs):
self.__delete_orphans(kwargs['instace'])
def __get_or_create_tree(self, alias):
try:
return Tree.objects.get(alias=alias)
except Tree.DoesNotExist:
return Tree.objects.create(alias=alias)
def should_appear(self, instance):
if isinstance(self.__should_appear, str):
return getattr(instance, self.__should_appear)
elif callable(self.__should_appear):
return self.__should_appear()
def south_field_triple(self):
"""Returns a suitable description of this field for South."""
args, kwargs = introspector(self)
kwargs.update({'date_field': 'None'})
return ('website.blog.fields.AutoTreeItemField', args, kwargs)
def __get_or_create_year_tree_item(self, model_instance):
year = self.__get_year(model_instance)
try:
return TreeItem.objects.get(title=year, tree=self.tree)
except TreeItem.DoesNotExist:
return TreeItem.objects.create(title=year,
url=reverse(self.year_menu_item_url, args = [year]),
tree=self.tree,
inmenu=self.inmenu,
inbreadcrumbs=self.inbreadcrumbs,
insitetree=self.insitetree,
parent=None)
def __get_year(self, model_instance):
return getattr(model_instance, self.date_field).strftime(self.year_format)
def __get_or_create_month_tree_item(self, model_instance, year_menu_item):
month = self.__get_month(model_instance)
try:
return TreeItem.objects.get(title=month, tree=self.tree, parent=year_menu_item)
except TreeItem.DoesNotExist:
return TreeItem.objects.create(title=month,
url=reverse(self.month_menu_item_url, args = [getattr(model_instance, self.date_field).year, getattr(model_instance, self.date_field).month]),
tree=self.tree,
inmenu=self.inmenu,
inbreadcrumbs=self.inbreadcrumbs,
insitetree=self.insitetree,
parent=year_menu_item)
def __get_month(self, model_instance):
return getattr(model_instance, self.date_field).strftime(self.month_format)
def __save_menu_item(self, model_instance, month_tree_item):
try:
item = self.__get_menu_item(model_instance)
item.title = getattr(model_instance, self.item_title)
item.url = model_instance.get_absolute_url()
item.parent = month_tree_item
return item
except TreeItem.DoesNotExist:
return TreeItem.objects.create(title=getattr(model_instance, self.item_title),
url=model_instance.get_absolute_url(),
tree=self.tree,
inmenu=self.inmenu,
inbreadcrumbs=self.inbreadcrumbs,
insitetree=self.insitetree,
parent=month_tree_item)
def __get_year_tree_item(self, model_instance):
year = self.__get_year(model_instance)
return TreeItem.objects.filter(title=year, tree=self.tree)
def __delete_orphans(self, model_instance):
menu_item = self.__get_menu_item(model_instance)
try:
if menu_item is not None:
month_menu_item = menu_item.parent
menu_item.delete()
if TreeItem.objects.filter(parent=month_menu_item, parent__parent=self.__get_year_tree_item(model_instance), tree=self.tree).count():
year_menu_item = month_menu_item.parent
month_menu_item.delete()
if TreeItem.objects.filter(parent=year_menu_item, tree=self.tree).count() == 0:
year_menu_item.delete()
except TreeItem.DoesNotExist:
pass
def __get_menu_item(self, model_instance):
menu_item_id = getattr(model_instance, self.get_attname())
return TreeItem.objects.get(id=menu_item_id)
But when I’m attempting to save it I am getting:
'AutoTreeItemField' object has no attribute '_meta'
Here’s the full stacktrace:
Environment:
Request Method: POST
Request URL: http://127.0.0.1:8000/admin/blog/draftpost/add/
Django Version: 1.3
Python Version: 2.7.1
Installed Applications:
['django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'django.contrib.sites',
'django.contrib.flatpages',
'tagging',
'reversion',
'south',
'sitetree',
'dojango',
'disqus',
'website.blog',
'website.cms']
Installed Middleware:
('django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'dojango.middleware.DojoCollector',
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware')
Traceback:
File "C:\Python27\lib\site-packages\django\core\handlers\base.py" in get_response
111. response = callback(request, *callback_args, **callback_kwargs)
File "C:\Python27\lib\site-packages\django\contrib\admin\options.py" in wrapper
307. return self.admin_site.admin_view(view)(*args, **kwargs)
File "C:\Python27\lib\site-packages\django\utils\decorators.py" in _wrapped_view
93. response = view_func(request, *args, **kwargs)
File "C:\Python27\lib\site-packages\django\views\decorators\cache.py" in _wrapped_view_func
79. response = view_func(request, *args, **kwargs)
File "C:\Python27\lib\site-packages\django\contrib\admin\sites.py" in inner
197. return view(request, *args, **kwargs)
File "C:\Python27\lib\site-packages\django\db\transaction.py" in inner
217. res = func(*args, **kwargs)
File "C:\Python27\lib\site-packages\reversion\revisions.py" in _create_on_success
352. self.end()
File "C:\Python27\lib\site-packages\reversion\revisions.py" in end
274. revision_set = self.follow_relationships(models)
File "C:\Python27\lib\site-packages\reversion\revisions.py" in follow_relationships
244. map(_follow_relationships, object_dict)
File "C:\Python27\lib\site-packages\reversion\revisions.py" in _follow_relationships
243. _follow_relationships(parent_obj)
File "C:\Python27\lib\site-packages\reversion\revisions.py" in _follow_relationships
213. result_dict[obj] = self.get_version_data(obj, VERSION_CHANGE)
File "C:\Python27\lib\site-packages\reversion\revisions.py" in get_version_data
254. serialized_data = serializers.serialize(registration_info.format, [obj], fields=registration_info.fields)
File "C:\Python27\lib\site-packages\django\core\serializers\__init__.py" in serialize
91. s.serialize(queryset, **options)
File "C:\Python27\lib\site-packages\django\core\serializers\base.py" in serialize
48. self.handle_fk_field(obj, field)
File "C:\Python27\lib\site-packages\django\core\serializers\python.py" in handle_fk_field
53. if field.rel.field_name == related._meta.pk.name:
Exception Type: AttributeError at /admin/blog/draftpost/add/
Exception Value: 'AutoTreeItemField' object has no attribute '_meta'
Can anyone exlpain to me how can this be? What could be done to remedy this issue?
The wierd thing is that it happens after the transaction ends and it saves everything I need to the database.
EDIT:
The error is caused by reversion trying to serialize my instance into json. However, this is still my problem because the _meta attribute should have been there and it isn’t. How can this be resolved?
The first thing that comes to my mind is that in the following comment:
you state that your field instance should act as a descriptor, yet it does not implement
__get__nor__set__, which are required for the descriptor protocol. Note thatForeignKeyitself also does not implement those, instead, it uses aReverseSingleRelatedObjectDescriptor.Now, what happens in your case is that the serializer sees that the field is a ForeignKey, therefore it expects a model instance to be sitting in its name on the model instance. It calls
getattr(obj, field.name)which would normally invoke theReverseSingleRelatedObjectDescriptor‘s__get__method and return an instance of the related model. In your case that particulargetattrcall returns your field instance (since it lacks__get__) instead of aTreeIteminstance and that is when things go wrong.So, to help you with your problem, I’d choose a completely different approach. From your code it seems to me that all you need is a few convenient methods accessible from the instances containing your
AutoTreeItemFieldthat work automatically with a relatedTreeItempointed to by aForeignKey.I’d rather keep the field a regular
ForeignKeyand instead attach the convenience methods to your model class. If you need them in several models, you can always create a mixin and if you want it to be really neat, you can make a customForeignKeysubclass that would only override itscontribute_to_classto automatically add the mixin tocls.__bases__.If you need the
ForeignKeys to be in different field names, you can even go as far as dynamically create the names of your methods by prepending them with your field’s name and currying them to supply the field name as a parameter. The possibilities are almost endless.EDIT: This can be done, for example, by creating a mixin class containing all your handy extra methods with names prefixed by
_TREEITEM. You’ll also have to make them accept all your extra parameters, likeyear_format,month_format,inmenuetc. as keyword arguments. Save these in yourForeignKeysubclass’__init__method as you do currently, then in itscontribute_to_classdo something like this:Also, in your
ForeignKeysubclass you can keep yourpre_saveand your signal handler, just remember to call the appropriate methods on your model instance. When you want to call them from within your views or your business logic, you have the handy curried auto-generated aliases available on your model instances, likeobj.myfield_get_year().The other possibility is to implement the descriptor protocol in your
AutoTreeItemFieldto return validTreeIteminstances but in that case you won’t be able to access its extra methods since the descriptor will return model instances instead of itself.