Created
October 18, 2012 09:03
-
-
Save kevincarrogan/3910574 to your computer and use it in GitHub Desktop.
A related model choice field for Django
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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