Ticket #1873: multi-select-relatedfilterspec.2.diff

File multi-select-relatedfilterspec.2.diff, 17.6 KB (added by marc@…, 9 years ago)
  • contrib/admin/filterspecs.py

     
    3434    def title(self):
    3535        return self.field.verbose_name
    3636
     37    def modifiers(self, cl):
     38        return []
     39
    3740    def output(self, cl):
    3841        t = []
    3942        if self.has_output():
     
    5255        super(RelatedFilterSpec, self).__init__(f, request, params)
    5356        if isinstance(f, models.ManyToManyField):
    5457            self.lookup_title = f.rel.to._meta.verbose_name
     58            self.is_manytomany = True
    5559        else:
    5660            self.lookup_title = f.verbose_name
     61            self.is_manytomany = False
     62        self.lookup_kwarg_and = '%s__%s__list_and' % (f.name, f.rel.to._meta.pk.name)
     63        self.lookup_kwarg_or = '%s__%s__list_or' % (f.name, f.rel.to._meta.pk.name)
     64        if self.is_manytomany and request.GET.get(self.lookup_kwarg_and, False):
     65            self.lookup_kwarg = self.lookup_kwarg_and
     66        else:
     67            self.lookup_kwarg = self.lookup_kwarg_or
     68        self.lookup_val = request.GET.get(self.lookup_kwarg, [])
     69        if self.lookup_val:
     70            self.lookup_val = [int(val) for val in self.lookup_val.split(models.query.LISTVALUE_SEPARATOR)]
     71        self.lookup_choices = f.rel.to._default_manager.all()
     72
     73    def has_output(self):
     74        return len(self.lookup_choices) > 1
     75
     76    def title(self):
     77        return self.lookup_title
     78
     79    def modifiers(self, cl):
     80        if not self.is_manytomany:
     81            return []
     82        pk_val_string = models.query.LISTVALUE_SEPARATOR.join([str(val_copy) for val_copy in self.lookup_val[:]])
     83        qs = cl.get_query_string( {self.lookup_kwarg_or: pk_val_string}, [self.lookup_kwarg_and])
     84        if not pk_val_string:
     85            qs = cl.get_query_string({}, [self.lookup_kwarg])
     86        modifier_or = {'selected': (self.lookup_kwarg is self.lookup_kwarg_or),
     87                       'query_string': qs,
     88                       'display': _('or')}
     89        qs = cl.get_query_string({self.lookup_kwarg_and: pk_val_string}, [self.lookup_kwarg_or])
     90        if not pk_val_string:
     91            qs = cl.get_query_string({}, [self.lookup_kwarg])
     92        modifier_and = {'selected': (self.lookup_kwarg is self.lookup_kwarg_and),
     93                        'query_string': qs,
     94                        'display': _('and')}
     95        return [modifier_or, modifier_and]
     96
     97    def choices(self, cl):
     98        yield {'selected': not self.lookup_val,
     99               'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
     100               'display': _('All')}
     101        for val in self.lookup_choices:
     102            pk_val = getattr(val, self.field.rel.to._meta.pk.attname)
     103            lookup_val_copy = self.lookup_val[:]
     104            if pk_val in lookup_val_copy:
     105                lookup_val_copy.remove(pk_val)
     106            else:
     107                lookup_val_copy.append(pk_val)
     108            pk_val_string = models.query.LISTVALUE_SEPARATOR.join([str(val_copy) for val_copy in lookup_val_copy])
     109            if pk_val_string:
     110                qs = cl.get_query_string( {self.lookup_kwarg: pk_val_string})
     111            else:
     112                qs = cl.get_query_string({}, [self.lookup_kwarg])
     113            yield {'selected': pk_val in self.lookup_val,
     114                   'query_string': qs,
     115                   'display': val}
     116
     117FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec)
     118
     119# ForeignFilterSpec was the original RelatedFilterSpec, but that is changed to
     120# use multi-select...
     121class ForeignFilterSpec(FilterSpec):
     122    def __init__(self, f, request, params):
     123        super(ForeignFilterSpec, self).__init__(f, request, params)
     124        if isinstance(f, models.ManyToManyField):
     125            self.lookup_title = f.rel.to._meta.verbose_name
     126        else:
     127            self.lookup_title = f.verbose_name
    57128        self.lookup_kwarg = '%s__%s__exact' % (f.name, f.rel.to._meta.pk.name)
    58129        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    59130        self.lookup_choices = f.rel.to._default_manager.all()
     
    74145                   'query_string': cl.get_query_string( {self.lookup_kwarg: pk_val}),
    75146                   'display': val}
    76147
    77 FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec)
     148FilterSpec.register(lambda f: bool(f.rel), ForeignFilterSpec)
    78149
    79150class ChoicesFilterSpec(FilterSpec):
    80151    def __init__(self, f, request, params):
  • contrib/admin/media/css/changelists.css

     
    2727#changelist-filter { position:absolute; top:0; right:0; z-index:1000; width:160px; border-left:1px solid #ddd; background:#efefef; margin:0; }
    2828#changelist-filter h2 { font-size:11px; padding:2px 5px; border-bottom:1px solid #ddd; }
    2929#changelist-filter h3 { font-size:12px; margin-bottom:0; }
     30#changelist-filter h3 span.modifier { font-size:11px; font-weight:normal; margin-left: 2px; }
     31#changelist-filter h3 span.modifier a { margin: 0 2px; }
     32#changelist-filter h3 span.modifier a.selected { color: #5b80b2; }
    3033#changelist-filter ul { padding-left:0;margin-left:10px;_margin-right:-10px; }
    3134#changelist-filter li { list-style-type:none; margin-left:0; padding-left:0; }
    3235#changelist-filter a { color:#999; }
  • contrib/admin/templates/admin/filter.html

     
    11{% load i18n %}
    2 <h3>{% blocktrans %} By {{ title }} {% endblocktrans %}</h3>
     2<h3>
     3{% blocktrans %} By {{ title }} {% endblocktrans %}
     4{% if modifiers %}<span class="modifier">({% for modifier in modifiers %}{% if not forloop.first %}|{% endif %}<a{% if modifier.selected %} class="selected"{% endif %} href="{{ modifier.query_string }}">{{ modifier.display }}</a>{% endfor %})</span>{% endif %}
     5</h3>
    36<ul>
    47{% for choice in choices %}
    58    <li{% if choice.selected %} class="selected"{% endif %}>
  • contrib/admin/templatetags/admin_list.py

     
    252252search_form = register.inclusion_tag('admin/search_form.html')(search_form)
    253253
    254254def filter(cl, spec):
    255     return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
     255    return {'title': spec.title(), 'modifiers': spec.modifiers(cl), 'choices' : list(spec.choices(cl))}
    256256filter = register.inclusion_tag('admin/filter.html')(filter)
    257257
    258258def filters(cl):
  • contrib/auth/models.py

     
    7878            (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
    7979            (_('Groups'), {'fields': ('groups',)}),
    8080        )
    81         list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
    82         list_filter = ('is_staff', 'is_superuser')
     81        list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'groups')
     82        list_filter = ('groups', 'is_staff', 'is_superuser')
    8383        search_fields = ('username', 'first_name', 'last_name', 'email')
    8484
    8585    def __str__(self):
  • core/management.py

     
    890890                            if not hasattr(cls, fn):
    891891                                e.add(opts, '"admin.list_display" refers to %r, which isn\'t an attribute, method or property.' % fn)
    892892                        else:
    893                             if isinstance(f, models.ManyToManyField):
    894                                 e.add(opts, '"admin.list_display" doesn\'t support ManyToManyFields (%r).' % fn)
     893                            pass # for this I have added a __repr__ to the ManyRelatedManager
     894                            #if isinstance(f, models.ManyToManyField):
     895                            #    e.add(opts, '"admin.list_display" doesn\'t support ManyToManyFields (%r).' % fn)
    895896                # list_filter
    896897                if not isinstance(opts.admin.list_filter, (list, tuple)):
    897898                    e.add(opts, '"admin.list_filter", if given, must be set to a list or tuple.')
  • db/models/fields/__init__.py

     
    164164        "Returns field's value prepared for database lookup."
    165165        if lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte', 'ne', 'year', 'month', 'day'):
    166166            return [value]
    167         elif lookup_type in ('range', 'in'):
     167        elif lookup_type in ('range', 'in', 'list_or', 'list_and'):
    168168            return value
    169169        elif lookup_type in ('contains', 'icontains'):
    170170            return ["%%%s%%" % prep_for_like_query(value)]
  • db/models/fields/related.py

     
    243243            if self._pk_val is None:
    244244                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model)
    245245
     246        def __repr__(self):
     247            return ", ".join([str(item) for item in self.all()])
     248
    246249        def get_query_set(self):
    247250            return superclass.get_query_set(self).filter(**(self.core_filters))
    248251
  • db/models/query.py

     
    1111    from sets import Set as set
    1212
    1313LOOKUP_SEPARATOR = '__'
     14LISTVALUE_SEPARATOR = ','
    1415
    1516# Size of each "chunk" for get_iterator calls.
    1617# Larger values are slightly faster at the expense of more storage space.
     
    7475        self._distinct = False       # Whether the query should use SELECT DISTINCT.
    7576        self._select = {}            # Dictionary of attname -> SQL.
    7677        self._where = []             # List of extra WHERE clauses to use.
     78        self._groupby = []           # Matching a list of IDs requires a GROUP BY and HAVING clause.
    7779        self._params = []            # List of params to use for extra WHERE clauses.
    7880        self._tables = []            # List of extra tables to use.
    7981        self._offset = None          # OFFSET clause
    8082        self._limit = None           # LIMIT clause
    8183        self._result_cache = None
     84        self.has_groupby = False
    8285
    8386    ########################
    8487    # PYTHON MAGIC METHODS #
     
    183186        select, sql, params = counter._get_sql_clause()
    184187        cursor = connection.cursor()
    185188        cursor.execute("SELECT COUNT(*)" + sql, params)
    186         return cursor.fetchone()[0]
     189        row = cursor.fetchone()
     190        if not row: return 0
     191        if not counter.has_groupby: return row[0]
     192        # Ouch! doing a SELECT COUNT(*) on a GROUP BY query to get the number of
     193        # records won't work, as you actually get more records, nicely grouped.
     194        # So, count the records instead. Perhaps I could just return -1 or something
     195        # for efficiency, but for now, return a correct rowcount
     196        count = 1
     197        while cursor.fetchone():
     198            count+= 1
     199        return count
    187200
    188201    def get(self, *args, **kwargs):
    189202        "Performs the SELECT and returns a single object matching the given keyword arguments."
     
    350363        c._distinct = self._distinct
    351364        c._select = self._select.copy()
    352365        c._where = self._where[:]
     366        c._groupby = self._groupby[:]
    353367        c._params = self._params[:]
    354368        c._tables = self._tables[:]
    355369        c._offset = self._offset
     
    386400        tables = [quote_only_if_word(t) for t in self._tables]
    387401        joins = SortedDict()
    388402        where = self._where[:]
     403        groupby = self._groupby[:]
    389404        params = self._params[:]
    390405
    391406        # Convert self._filters into SQL.
    392         tables2, joins2, where2, params2 = self._filters.get_sql(opts)
     407        tables2, joins2, where2, groupby2, params2 = self._filters.get_sql(opts)
    393408        tables.extend(tables2)
    394409        joins.update(joins2)
    395410        where.extend(where2)
     411        groupby.extend(groupby2)
    396412        params.extend(params2)
    397413
    398414        # Add additional tables and WHERE clauses based on select_related.
     
    419435        if where:
    420436            sql.append(where and "WHERE " + " AND ".join(where))
    421437
     438        # Compose the GROUP BY clause into SQL.
     439        if groupby:
     440            # TODO: check what happens if there's more than one groupby item
     441            sql.append("GROUP BY " + ",".join(select) + (" HAVING count(" + select[0] + ")>=%d" % (groupby[0], )))
     442            self.has_groupby = True
     443
    422444        # ORDER BY clause
    423445        order_by = []
    424446        if self._order_by is not None:
     
    518540        self.args = args
    519541
    520542    def get_sql(self, opts):
    521         tables, joins, where, params = [], SortedDict(), [], []
     543        tables, joins, where, groupby, params = [], SortedDict(), [], [], []
    522544        for val in self.args:
    523             tables2, joins2, where2, params2 = val.get_sql(opts)
     545            tables2, joins2, where2, groupby2, params2 = val.get_sql(opts)
    524546            tables.extend(tables2)
    525547            joins.update(joins2)
    526548            where.extend(where2)
     549            groupby.extend(groupby2)
    527550            params.extend(params2)
    528551        if where:
    529             return tables, joins, ['(%s)' % self.operator.join(where)], params
    530         return tables, joins, [], params
     552            return tables, joins, ['(%s)' % self.operator.join(where)], groupby, params
     553        return tables, joins, [], groupby, params
    531554
    532555class QAnd(QOperator):
    533556    "Encapsulates a combined query that uses 'AND'."
     
    575598    "Encapsulates NOT (...) queries as objects"
    576599
    577600    def get_sql(self, opts):
    578         tables, joins, where, params = super(QNot, self).get_sql(opts)
     601        tables, joins, where, groupby, params = super(QNot, self).get_sql(opts)
    579602        where2 = ['(NOT (%s))' % " AND ".join(where)]
    580         return tables, joins, where2, params
     603        return tables, joins, where2, groupby, params
    581604
    582605def get_where_clause(lookup_type, table_prefix, field_name, value):
    583606    if table_prefix.endswith('.'):
     
    587610        return '%s%s %s' % (table_prefix, field_name, (backend.OPERATOR_MAPPING[lookup_type] % '%s'))
    588611    except KeyError:
    589612        pass
    590     if lookup_type == 'in':
     613    if lookup_type in ('in', 'list_or', 'list_and'):
    591614        return '%s%s IN (%s)' % (table_prefix, field_name, ','.join(['%s' for v in value]))
    592615    elif lookup_type == 'range':
    593616        return '%s%s BETWEEN %%s AND %%s' % (table_prefix, field_name)
     
    648671    # At present, this method only every returns INNER JOINs; the option is
    649672    # there for others to implement custom Q()s, etc that return other join
    650673    # types.
    651     tables, joins, where, params = [], SortedDict(), [], []
     674    tables, joins, where, groupby, params = [], SortedDict(), [], [], []
    652675
    653676    for kwarg, value in kwarg_items:
    654677        if value is not None:
     
    674697            if len(path) < 1:
    675698                raise TypeError, "Cannot parse keyword query %r" % kwarg
    676699
    677             tables2, joins2, where2, params2 = lookup_inner(path, clause, value, opts, opts.db_table, None)
     700            tables2, joins2, where2, groupby2, params2 = lookup_inner(path, clause, value, opts, opts.db_table, None)
    678701            tables.extend(tables2)
    679702            joins.update(joins2)
    680703            where.extend(where2)
     704            groupby.extend(groupby2)
    681705            params.extend(params2)
    682     return tables, joins, where, params
     706    return tables, joins, where, groupby, params
    683707
    684708class FieldFound(Exception):
    685709    "Exception used to short circuit field-finding operations."
     
    699723    return matches[0]
    700724
    701725def lookup_inner(path, clause, value, opts, table, column):
    702     tables, joins, where, params = [], SortedDict(), [], []
     726    tables, joins, where, groupby, params = [], SortedDict(), [], [], []
    703727    current_opts = opts
    704728    current_table = table
    705729    current_column = column
     
    817841            join_column = None
    818842
    819843        # There are name queries remaining. Recurse deeper.
    820         tables2, joins2, where2, params2 = lookup_inner(path, clause, value, new_opts, new_table, join_column)
     844        tables2, joins2, where2, groupby2, params2 = lookup_inner(path, clause, value, new_opts, new_table, join_column)
    821845
    822846        tables.extend(tables2)
    823847        joins.update(joins2)
    824848        where.extend(where2)
     849        groupby.extend(groupby2)
    825850        params.extend(params2)
    826851    else:
    827852        # Evaluate clause on current table.
     
    832857            column = current_column
    833858        else:
    834859            column = field.column
     860        if clause in ('list_or', 'list_and'):
     861            value = value.split(LISTVALUE_SEPARATOR)
     862            min_matches = (clause=='list_or') and 1 or len(value)
     863            groupby.append(min_matches)
    835864
    836865        where.append(get_where_clause(clause, current_table + '.', column, value))
    837866        params.extend(field.get_db_prep_lookup(clause, value))
    838867
    839     return tables, joins, where, params
     868    return tables, joins, where, groupby, params
    840869
    841870def delete_objects(seen_objs):
    842871    "Iterate through a list of seen classes, and remove any instances that are referred to"
Back to Top