Custom managers for Django ForeignKeys
I have a Django model for keywords that I use extensively as a ForeignKey or as a ManyToManyField throughout the rest of my models. I have a Boolean on the KeyWord model for “categorization only”. If that’s checked, this keyword is only available for categorization of pages and of URLs attached to a page. That is, it’s only available for two out of the many models that use it.
Making it disappear for all models by default is easy.
[toggle code]
- #manager for keywords
-
class KeyManager(models.Manager):
- #most uses of keywords do not get to use "categorizationOnly" keywords
-
def get_query_set(self):
- return super(KeyManager, self).get_query_set().filter(categorizationOnly=False)
-
def categoryKeys(self):
- return super(KeyManager, self).get_query_set()
The default menu selection for KeyWord objects will now only display choices for which categorizationOnly is not true.
At first, I thought setting the queryset ought to work:
ModelChoiceField.queryset
A QuerySet of model objects from which the choices for the field will be derived, and which will be used to validate the user's selection.
It’s easy enough to do:
[toggle code]
-
class PageURLForm(forms.ModelForm):
-
class Meta:
- model = PageURL
-
def __init__(self, *args, **kwargs):
- super(PageURLForm, self).__init__(*args, **kwargs)
- self.fields['category'].queryset = KeyWord.objects.categoryKeys()
-
class Meta:
-
class PageURLAdmin(admin.ModelAdmin):
- form = PageURLForm
- …
But the .queryset description is slightly misleading. The form will validate using the custom queryset, but the model itself will continue to validate using the default manager method. The result is that the restricted values will appear in the pulldown menu, but on trying to save one of them, I end up seeing:
Model KeyWord with pk 638 does not exist.
A grep for the text “with pk” showed that the offender is the validate method in the ForeignKey class in django/db/models/fields/related.py.
[toggle code]
-
def validate(self, value, model_instance):
-
if self.rel.parent_link:
- return
- super(ForeignKey, self).validate(value, model_instance)
-
if value is None:
- return
- using = router.db_for_read(model_instance.__class__, instance=model_instance)
-
qs = self.rel.to._default_manager.using(using).filter(
- **{self.rel.field_name: value}
- )
- qs = qs.complex_filter(self.rel.limit_choices_to)
-
if not qs.exists():
-
raise exceptions.ValidationError(self.error_messages['invalid'] % {
- 'model': self.rel.to._meta.verbose_name, 'pk': value})
-
raise exceptions.ValidationError(self.error_messages['invalid'] % {
-
if self.rel.parent_link:
There’s probably a better way to do this (and if you know it, an example in the comments will be appreciated) but I ended up subclassing ForeignKey and rewriting it to accept a custom manager method:
[toggle code]
- from django.db import router, models
-
class CustomManagerForeignKey(models.ForeignKey):
-
def __init__(self, *args, **kwargs):
-
if 'manager' in kwargs:
- self.customManager = kwargs['manager']()
- del kwargs['manager']
-
else:
- self.customManager = None
- super(CustomManagerForeignKey, self).__init__(*args, **kwargs)
-
if 'manager' in kwargs:
-
def formfield(self, **kwargs):
- field = super(CustomManagerForeignKey, self).formfield(**kwargs)
-
if self.customManager:
- field.queryset = self.customManager
- return field
-
def validate(self, value, model_instance):
-
if self.rel.parent_link:
- return
- super(models.ForeignKey, self).validate(value, model_instance)
-
if value is None:
- return
-
if self.customManager:
- manager = self.customManager
-
else:
- using = router.db_for_read(model_instance.__class__, instance=model_instance)
- manager = self.rel.to._default_manager.using(using)
- qs = manager.filter(**{self.rel.field_name: value})
- qs = qs.complex_filter(self.rel.limit_choices_to)
-
if not qs.exists():
- raise exceptions.ValidationError(self.error_messages['invalid'] % {'model': self.rel.to._meta.verbose_name, 'pk': value})
-
if self.rel.parent_link:
-
def __init__(self, *args, **kwargs):
The __init__ method looks for a “manager” option, and saves it if it’s there. Then, the validate method uses that in preference to the hard-coded default manager method. I can call it using something like this:
[toggle code]
-
class PageURL(models.Model):
- …
- category = CustomManagerForeignKey(KeyWord, blank=True, null=True, manager=KeyWord.objects.categoryKeys)
The main thing I don’t like about this is that it duplicates existing code from the validate method, code that will probably change in upcoming versions of Django. However, anything else seemed to require editing the source code directly, and that will definitely fail on an upgrade.
- Django form fields at Django
- “When you create a Form class, the most important part is defining the fields of the form. Each field has custom validation logic, along with a few other hooks.”
More Django
- Converting an existing Django model to Django-MPTT
- Using a SQL database to mimic a filesystem will, eventually, create bottlenecks when it comes to traversing the filesystem. One solution is modified preordered tree traversal, which saves the tree structure in an easily-used manner inside the model.
- Two search bookmarklets for Django
- Bookmarklets—JavaScript code in a bookmark—can make working with big Django databases much easier.
- Fixing Django’s feed generator without hacking Django
- It looks like it’s going to be a while before the RSS feed generator in Django is going to get fixed, so I looked into subclassing as a way of getting a working guid in my Django RSS feeds.
- ModelForms and FormViews
- This is just a notice because when I did a search, nothing came up. Don’t use ModelForm with FormView, use UpdateView instead.
- Django: fix_ampersands and abbreviations
- The fix_ampersands filter will miss some cases where ampersands need to be replaced.
- 29 more pages with the topic Django, and other related pages
I had this problem as well.
When doing:
class PageURL(models.Model):
key_objects = KeyManager()
...
(I used my own objects, but I am "translating" it to yours)
you will get a surprise!
type(PageURL._default_manager) was for me not models.Manager(), it was instead KeyManager! All I had to do was:
class PageURL(models.Model):
objects = models.Manager()
key_objects = KeyManager()
...
I hope if you test this that you can confirm that you get the same result.
Anders Andersson in Gothenburg, Sweden at 8:43 a.m. March 3rd, 2013
4AWVd
fyi solved in commit 04a2a6b0f9 cb6bb98ed fe84bf4361 216d6 0a4e38
(remove the spaces)
you got it right!
Patrick at 11:22 p.m. October 10th, 2013
4jx2W