Ticket #13902: 13902_distinct_in_changelist5.patch

File 13902_distinct_in_changelist5.patch, 7.4 KB (added by rasca, 13 years ago)

new patch for trunk after modifications in r15286

  • django/contrib/admin/views/main.py

    diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
    index 886ccd9..1ae1715 100644
    a b class ChangeList(object):  
    169169        return order_field, order_type
    170170
    171171    def get_query_set(self):
     172        is_distinct = False
     173
    172174        qs = self.root_query_set
    173175        lookup_params = self.params.copy() # a dictionary of the query string
    174176        for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR):
    class ChangeList(object):  
    181183                del lookup_params[key]
    182184                lookup_params[smart_str(key)] = value
    183185
     186            if not is_distinct:
     187                # Check if it's a relationship that might return more than one
     188                # instance
     189                try:
     190                    f = self.lookup_opts.get_field_by_name(key.split('__',1)[0])
     191                except models.FieldDoesNotExist:
     192                    raise IncorrectLookupParameters
     193                if (isinstance(f[0], models.related.RelatedObject) or
     194                    isinstance(f[0].rel, (models.ManyToOneRel,
     195                                         models.ManyToManyRel))):
     196                    is_distinct = True
     197
    184198            # if key ends with __in, split parameter into separate values
    185199            if key.endswith('__in'):
    186200                lookup_params[key] = value.split(',')
    class ChangeList(object):  
    244258            for bit in self.query.split():
    245259                or_queries = [models.Q(**{construct_search(str(field_name)): bit}) for field_name in self.search_fields]
    246260                qs = qs.filter(reduce(operator.or_, or_queries))
    247             for field_name in self.search_fields:
    248                 if '__' in field_name:
    249                     qs = qs.distinct()
    250                     break
     261            if not is_distinct:
     262                for field_name in self.search_fields:
     263                    f = self.lookup_opts.get_field_by_name(field_name.split('__',1)[0])
     264                    if (isinstance(f[0], models.related.RelatedObject) or
     265                        isinstance(f[0].rel, (models.ManyToOneRel,
     266                                             models.ManyToManyRel))):
     267                        is_distinct = True
     268                        break
    251269
    252         return qs
     270        if is_distinct:
     271            return qs.distinct()
     272        else:
     273            return qs
    253274
    254275    def url_for_result(self, result):
    255276        return "%s/" % quote(getattr(result, self.pk_attname))
  • tests/regressiontests/admin_changelist/models.py

    diff --git a/tests/regressiontests/admin_changelist/models.py b/tests/regressiontests/admin_changelist/models.py
    index 858d6df..6944c78 100644
    a b class Parent(models.Model):  
    77class Child(models.Model):
    88    parent = models.ForeignKey(Parent, editable=False, null=True)
    99    name = models.CharField(max_length=30, blank=True)
     10    multiple_m2m = models.ManyToManyField(Parent, related_name='multiple_m2m',
     11                                          through='Through')
     12
     13class Through(models.Model):
     14    parent = models.ForeignKey(Parent)
     15    child = models.ForeignKey(Child)
  • tests/regressiontests/admin_changelist/tests.py

    diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py
    index c3f6186..cf8a154 100644
    a b from django.core.paginator import Paginator  
    55from django.template import Context, Template
    66from django.test import TransactionTestCase
    77
    8 from regressiontests.admin_changelist.models import Child, Parent
     8from regressiontests.admin_changelist.models import Child, Parent, Through
    99
    1010
    1111class ChangeListTests(TransactionTestCase):
     12    def setUp(self):
     13        self.new_parent = Parent.objects.create(name='parent')
     14        self.new_child = Child.objects.create(name='name', parent=self.new_parent)
     15
    1216    def test_select_related_preserved(self):
    1317        """
    1418        Regression test for #10348: ChangeList.get_query_set() shouldn't
    class ChangeListTests(TransactionTestCase):  
    3539        template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
    3640        context = Context({'cl': cl})
    3741        table_output = template.render(context)
    38         row_html = '<tbody><tr class="row1"><td><input type="checkbox" class="action-select" value="1" name="_selected_action" /></td><th><a href="1/">name</a></th><td>(None)</td></tr></tbody>'
     42        row_html = '<tbody><tr class="row1"><td><input type="checkbox" class="action-select" value="2" name="_selected_action" /></td><th><a href="2/">name</a></th><td>(None)</td></tr>'
    3943        self.assertFalse(table_output.find(row_html) == -1,
    4044            'Failed to find expected row element: %s' % table_output)
    4145
    class ChangeListTests(TransactionTestCase):  
    4549        Verifies that inclusion tag result_list generates a table when with
    4650        default ModelAdmin settings.
    4751        """
    48         new_parent = Parent.objects.create(name='parent')
    49         new_child = Child.objects.create(name='name', parent=new_parent)
     52        new_parent = self.new_parent
     53        new_child = self.new_child
    5054        request = MockRequest()
    5155        m = ChildAdmin(Child, admin.site)
    5256        cl = ChangeList(request, Child, m.list_display, m.list_display_links,
    class ChangeListTests(TransactionTestCase):  
    6973        when list_editable is enabled are rendered in a div outside the
    7074        table.
    7175        """
    72         new_parent = Parent.objects.create(name='parent')
    73         new_child = Child.objects.create(name='name', parent=new_parent)
     76        new_parent = self.new_parent
     77        new_child = self.new_child
    7478        request = MockRequest()
    7579        m = ChildAdmin(Child, admin.site)
    7680
    class ChangeListTests(TransactionTestCase):  
    136140        self.assertIsInstance(cl.paginator, CustomPaginator)
    137141
    138142
     143    def test_distinct(self):
     144        """
     145        Regression test for #13902: When using a ManyToMany in list_filter,
     146        results may apper more than once
     147        """
     148        new_parent = self.new_parent
     149        new_child = self.new_child
     150        relation1 = Through.objects.create(parent=new_parent, child=new_child)
     151        relation2 = Through.objects.create(parent=new_parent, child=new_child)
     152
     153        m = ChildAdmin(Child, admin.site)
     154        cl = ChangeList(MockFilteredRequest(), Child, m.list_display, m.list_display_links,
     155                m.list_filter, m.date_hierarchy, m.search_fields,
     156                m.list_select_related, m.list_per_page, m.list_editable, m)
     157
     158        cl.get_results(MockFilteredRequest())
     159
     160        # There's only one Child instance
     161        self.assertEqual(cl.result_count, 1)
     162       
    139163class ChildAdmin(admin.ModelAdmin):
    140164    list_display = ['name', 'parent']
    141165    def queryset(self, request):
    142166        return super(ChildAdmin, self).queryset(request).select_related("parent__name")
     167    list_filter = ['multiple_m2m', ]
    143168
    144169
    145170class MockRequest(object):
    146171    GET = {}
    147172
     173class MockFilteredRequest(object):
     174    GET = {'multiple_m2m__id__exact': 1, }
    148175
    149176class CustomPaginator(Paginator):
    150177    def __init__(self, queryset, page_size, orphans=0, allow_empty_first_page=True):
    151178        super(CustomPaginator, self).__init__(queryset, 5, orphans=2,
    152179            allow_empty_first_page=allow_empty_first_page)
     180
Back to Top