Django

Code

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

File multi-select-relatedfilterspec.2.diff, 17.6 kB (added by marc@million.nl, 3 years ago)
  • contrib/admin/filterspecs.py

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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"