Index: contrib/admin/filterspecs.py =================================================================== --- contrib/admin/filterspecs.py (revision 2901) +++ contrib/admin/filterspecs.py (working copy) @@ -34,6 +34,9 @@ def title(self): return self.field.verbose_name + def modifiers(self, cl): + return [] + def output(self, cl): t = [] if self.has_output(): @@ -52,8 +55,76 @@ super(RelatedFilterSpec, self).__init__(f, request, params) if isinstance(f, models.ManyToManyField): self.lookup_title = f.rel.to._meta.verbose_name + self.is_manytomany = True else: self.lookup_title = f.verbose_name + self.is_manytomany = False + self.lookup_kwarg_and = '%s__%s__list_and' % (f.name, f.rel.to._meta.pk.name) + self.lookup_kwarg_or = '%s__%s__list_or' % (f.name, f.rel.to._meta.pk.name) + if self.is_manytomany and request.GET.get(self.lookup_kwarg_and, False): + self.lookup_kwarg = self.lookup_kwarg_and + else: + self.lookup_kwarg = self.lookup_kwarg_or + self.lookup_val = request.GET.get(self.lookup_kwarg, []) + if self.lookup_val: + self.lookup_val = [int(val) for val in self.lookup_val.split(models.query.LISTVALUE_SEPARATOR)] + self.lookup_choices = f.rel.to._default_manager.all() + + def has_output(self): + return len(self.lookup_choices) > 1 + + def title(self): + return self.lookup_title + + def modifiers(self, cl): + if not self.is_manytomany: + return [] + pk_val_string = models.query.LISTVALUE_SEPARATOR.join([str(val_copy) for val_copy in self.lookup_val[:]]) + qs = cl.get_query_string( {self.lookup_kwarg_or: pk_val_string}, [self.lookup_kwarg_and]) + if not pk_val_string: + qs = cl.get_query_string({}, [self.lookup_kwarg]) + modifier_or = {'selected': (self.lookup_kwarg is self.lookup_kwarg_or), + 'query_string': qs, + 'display': _('or')} + qs = cl.get_query_string({self.lookup_kwarg_and: pk_val_string}, [self.lookup_kwarg_or]) + if not pk_val_string: + qs = cl.get_query_string({}, [self.lookup_kwarg]) + modifier_and = {'selected': (self.lookup_kwarg is self.lookup_kwarg_and), + 'query_string': qs, + 'display': _('and')} + return [modifier_or, modifier_and] + + def choices(self, cl): + yield {'selected': not self.lookup_val, + 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), + 'display': _('All')} + for val in self.lookup_choices: + pk_val = getattr(val, self.field.rel.to._meta.pk.attname) + lookup_val_copy = self.lookup_val[:] + if pk_val in lookup_val_copy: + lookup_val_copy.remove(pk_val) + else: + lookup_val_copy.append(pk_val) + pk_val_string = models.query.LISTVALUE_SEPARATOR.join([str(val_copy) for val_copy in lookup_val_copy]) + if pk_val_string: + qs = cl.get_query_string( {self.lookup_kwarg: pk_val_string}) + else: + qs = cl.get_query_string({}, [self.lookup_kwarg]) + yield {'selected': pk_val in self.lookup_val, + 'query_string': qs, + 'display': val} + +FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec) + +# ForeignFilterSpec was the original RelatedFilterSpec, but that is changed to +# use multi-select... +class ForeignFilterSpec(FilterSpec): + def __init__(self, f, request, params): + super(ForeignFilterSpec, self).__init__(f, request, params) + if isinstance(f, models.ManyToManyField): + self.lookup_title = f.rel.to._meta.verbose_name + else: + self.lookup_title = f.verbose_name self.lookup_kwarg = '%s__%s__exact' % (f.name, f.rel.to._meta.pk.name) self.lookup_val = request.GET.get(self.lookup_kwarg, None) self.lookup_choices = f.rel.to._default_manager.all() @@ -74,7 +145,7 @@ 'query_string': cl.get_query_string( {self.lookup_kwarg: pk_val}), 'display': val} -FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec) +FilterSpec.register(lambda f: bool(f.rel), ForeignFilterSpec) class ChoicesFilterSpec(FilterSpec): def __init__(self, f, request, params): Index: contrib/admin/media/css/changelists.css =================================================================== --- contrib/admin/media/css/changelists.css (revision 2901) +++ contrib/admin/media/css/changelists.css (working copy) @@ -27,6 +27,9 @@ #changelist-filter { position:absolute; top:0; right:0; z-index:1000; width:160px; border-left:1px solid #ddd; background:#efefef; margin:0; } #changelist-filter h2 { font-size:11px; padding:2px 5px; border-bottom:1px solid #ddd; } #changelist-filter h3 { font-size:12px; margin-bottom:0; } +#changelist-filter h3 span.modifier { font-size:11px; font-weight:normal; margin-left: 2px; } +#changelist-filter h3 span.modifier a { margin: 0 2px; } +#changelist-filter h3 span.modifier a.selected { color: #5b80b2; } #changelist-filter ul { padding-left:0;margin-left:10px;_margin-right:-10px; } #changelist-filter li { list-style-type:none; margin-left:0; padding-left:0; } #changelist-filter a { color:#999; } Index: contrib/admin/templates/admin/filter.html =================================================================== --- contrib/admin/templates/admin/filter.html (revision 2901) +++ contrib/admin/templates/admin/filter.html (working copy) @@ -1,5 +1,8 @@ {% load i18n %} -

{% blocktrans %} By {{ title }} {% endblocktrans %}

+

+{% blocktrans %} By {{ title }} {% endblocktrans %} +{% if modifiers %}({% for modifier in modifiers %}{% if not forloop.first %}|{% endif %}{{ modifier.display }}{% endfor %}){% endif %} +