Skip to content

Instantly share code, notes, and snippets.

@kevincarrogan
Created October 18, 2012 09:03
Show Gist options
  • Save kevincarrogan/3910574 to your computer and use it in GitHub Desktop.
Save kevincarrogan/3910574 to your computer and use it in GitHub Desktop.
A related model choice field for Django
class RelatedModelChoiceIterator(object):
def __init__(self, field):
self.field = field
self.parent_queryset = field.parent_queryset
self.queryset = field.queryset
self.lookup_name = field.lookup_name
def __iter__(self):
if self.field.empty_label is not None:
yield (u"", self.field.empty_label)
if self.field.cache_choices:
if self.field.choice_cache is None:
choices = []
for obj in self.parent_queryset.all():
sub_query = self.queryset.filter(**{self.lookup_name: obj})
if len(sub_query) > 0:
choices.append((
self.field.label_from_instance(obj),
[
self.choice(sub_obj) for sub_obj in sub_query
]
))
self.field.choice_cache = choices
for choice in self.field.choice_cache:
yield choice
else:
for obj in self.parent_queryset.all():
sub_query = self.queryset.filter(**{self.lookup_name: obj})
if len(sub_query) > 0:
yield (
self.field.label_from_instance(obj),
[
self.choice(sub_obj) for sub_obj in sub_query
]
)
def __len__(self):
return len(self.parent_queryset)
def choice(self, obj):
return (self.field.prepare_value(obj), self.field.label_from_instance(obj))
class RelatedModelChoiceField(forms.ModelChoiceField):
def __init__(self, parent_queryset, queryset, lookup_name, empty_label=u"---------", cache_choices=False,
required=True, widget=None, label=None, initial=None,
help_text=None, to_field_name=None, *args, **kwargs):
self.parent_queryset = parent_queryset
self.lookup_name = lookup_name
super(RelatedModelChoiceField, self).__init__(queryset, empty_label, cache_choices,
required, widget, label, initial,
help_text, to_field_name, *args, **kwargs)
def __deepcopy__(self, memo):
result = super(ChoiceField, self).__deepcopy__(memo)
result.queryset = result.queryset
result.parent_queryset = result.parent_queryset
result.lookup_name = result.lookup_name
return result
def _get_choices(self):
if hasattr(self, '_choices'):
return self._choices
return RelatedModelChoiceIterator(self)
choices = property(_get_choices, ChoiceField._set_choices)
# Usage
class RelatedFieldForm(forms.Form):
related_field = RelatedModelChoiceField(
parent_queryset=ParentModel.objects.all(), # Will be used for the optgroups
queryset=ChildModel.objects.all(), # Will be used for the actual options
lookup_name='lookup_field' # The name of the field that is the foreign key between the child model and the parent model i.e. ChildModel.lookup_field -> ParentModel
)
class ManyToManyRelatedModelChoiceIterator(object):
def __init__(self, field, parent, child):
self.field = field
self.queryset = field.queryset
self.parent = parent
self.child = child
def __iter__(self):
if self.field.empty_label is not None:
yield (u"", self.field.empty_label)
if self.field.cache_choices:
if self.field.choice_cache is None:
choices = []
grouper = groupby(self.queryset.select_related(self.parent).select_related(self.child).order_by('%s__name' % self.parent, '%s__name' % self.child), key=lambda x: x.country)
for k, g in grouper:
choices.append(
self.field.label_from_instance(k),
[
self.choice(sub_obj) for sub_obj in g
]
)
self.field.choice_cache = choices
for choice in self.field.choice_cache:
yield choice
else:
grouper = groupby(self.queryset.select_related(self.parent).select_related(self.child).order_by('%s__name' % self.parent, '%s__name' % self.child), key=lambda x: x.country)
for k, g in grouper:
yield (
self.field.label_from_instance(k),
[
self.choice(sub_obj) for sub_obj in g
]
)
def __len__(self):
return len(self.parent_queryset)
def choice(self, obj):
return (self.field.prepare_value(obj), self.field.label_from_instance(getattr(obj, self.child)))
class ManyToManyRelatedModelChoiceField(forms.ModelChoiceField):
def __init__(self, queryset, parent, child, empty_label=u"---------", cache_choices=False,
required=True, widget=None, label=None, initial=None,
help_text=None, to_field_name=None, *args, **kwargs):
self.parent = parent
self.child = child
super(ManyToManyRelatedModelChoiceField, self).__init__(queryset, empty_label, cache_choices,
required, widget, label, initial,
help_text, to_field_name, *args, **kwargs)
def __deepcopy__(self, memo):
result = super(ChoiceField, self).__deepcopy__(memo)
result.queryset = result.queryset
result.parent = result.parent
result.child = result.child
return result
def _get_choices(self):
if hasattr(self, '_choices'):
return self._choices
return ManyToManyRelatedModelChoiceIterator(self, self.parent, self.child)
choices = property(_get_choices, ChoiceField._set_choices)
# Usage
class RelatedFieldForm(forms.Form):
related_field = ManyToManyRelatedModelChoiceField(
queryset=ThroughModel.objects.all(), # Will be used for the actual options values,
parent='parent_field', # The parent field of the through model to be used for optgroups
child='child_field', # The child field of the through model to be used for the options text
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment