Ticket #1873: multi-select-relatedfilterspec.2.diff
File multi-select-relatedfilterspec.2.diff, 17.6 KB (added by , 19 years ago) |
---|
-
contrib/admin/filterspecs.py
34 34 def title(self): 35 35 return self.field.verbose_name 36 36 37 def modifiers(self, cl): 38 return [] 39 37 40 def output(self, cl): 38 41 t = [] 39 42 if self.has_output(): … … 52 55 super(RelatedFilterSpec, self).__init__(f, request, params) 53 56 if isinstance(f, models.ManyToManyField): 54 57 self.lookup_title = f.rel.to._meta.verbose_name 58 self.is_manytomany = True 55 59 else: 56 60 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 117 FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec) 118 119 # ForeignFilterSpec was the original RelatedFilterSpec, but that is changed to 120 # use multi-select... 121 class 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 57 128 self.lookup_kwarg = '%s__%s__exact' % (f.name, f.rel.to._meta.pk.name) 58 129 self.lookup_val = request.GET.get(self.lookup_kwarg, None) 59 130 self.lookup_choices = f.rel.to._default_manager.all() … … 74 145 'query_string': cl.get_query_string( {self.lookup_kwarg: pk_val}), 75 146 'display': val} 76 147 77 FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec)148 FilterSpec.register(lambda f: bool(f.rel), ForeignFilterSpec) 78 149 79 150 class ChoicesFilterSpec(FilterSpec): 80 151 def __init__(self, f, request, params): -
contrib/admin/media/css/changelists.css
27 27 #changelist-filter { position:absolute; top:0; right:0; z-index:1000; width:160px; border-left:1px solid #ddd; background:#efefef; margin:0; } 28 28 #changelist-filter h2 { font-size:11px; padding:2px 5px; border-bottom:1px solid #ddd; } 29 29 #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; } 30 33 #changelist-filter ul { padding-left:0;margin-left:10px;_margin-right:-10px; } 31 34 #changelist-filter li { list-style-type:none; margin-left:0; padding-left:0; } 32 35 #changelist-filter a { color:#999; } -
contrib/admin/templates/admin/filter.html
1 1 {% 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> 3 6 <ul> 4 7 {% for choice in choices %} 5 8 <li{% if choice.selected %} class="selected"{% endif %}> -
contrib/admin/templatetags/admin_list.py
252 252 search_form = register.inclusion_tag('admin/search_form.html')(search_form) 253 253 254 254 def 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))} 256 256 filter = register.inclusion_tag('admin/filter.html')(filter) 257 257 258 258 def filters(cl): -
contrib/auth/models.py
78 78 (_('Important dates'), {'fields': ('last_login', 'date_joined')}), 79 79 (_('Groups'), {'fields': ('groups',)}), 80 80 ) 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') 83 83 search_fields = ('username', 'first_name', 'last_name', 'email') 84 84 85 85 def __str__(self): -
core/management.py
890 890 if not hasattr(cls, fn): 891 891 e.add(opts, '"admin.list_display" refers to %r, which isn\'t an attribute, method or property.' % fn) 892 892 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) 895 896 # list_filter 896 897 if not isinstance(opts.admin.list_filter, (list, tuple)): 897 898 e.add(opts, '"admin.list_filter", if given, must be set to a list or tuple.') -
db/models/fields/__init__.py
164 164 "Returns field's value prepared for database lookup." 165 165 if lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte', 'ne', 'year', 'month', 'day'): 166 166 return [value] 167 elif lookup_type in ('range', 'in' ):167 elif lookup_type in ('range', 'in', 'list_or', 'list_and'): 168 168 return value 169 169 elif lookup_type in ('contains', 'icontains'): 170 170 return ["%%%s%%" % prep_for_like_query(value)] -
db/models/fields/related.py
243 243 if self._pk_val is None: 244 244 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model) 245 245 246 def __repr__(self): 247 return ", ".join([str(item) for item in self.all()]) 248 246 249 def get_query_set(self): 247 250 return superclass.get_query_set(self).filter(**(self.core_filters)) 248 251 -
db/models/query.py
11 11 from sets import Set as set 12 12 13 13 LOOKUP_SEPARATOR = '__' 14 LISTVALUE_SEPARATOR = ',' 14 15 15 16 # Size of each "chunk" for get_iterator calls. 16 17 # Larger values are slightly faster at the expense of more storage space. … … 74 75 self._distinct = False # Whether the query should use SELECT DISTINCT. 75 76 self._select = {} # Dictionary of attname -> SQL. 76 77 self._where = [] # List of extra WHERE clauses to use. 78 self._groupby = [] # Matching a list of IDs requires a GROUP BY and HAVING clause. 77 79 self._params = [] # List of params to use for extra WHERE clauses. 78 80 self._tables = [] # List of extra tables to use. 79 81 self._offset = None # OFFSET clause 80 82 self._limit = None # LIMIT clause 81 83 self._result_cache = None 84 self.has_groupby = False 82 85 83 86 ######################## 84 87 # PYTHON MAGIC METHODS # … … 183 186 select, sql, params = counter._get_sql_clause() 184 187 cursor = connection.cursor() 185 188 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 187 200 188 201 def get(self, *args, **kwargs): 189 202 "Performs the SELECT and returns a single object matching the given keyword arguments." … … 350 363 c._distinct = self._distinct 351 364 c._select = self._select.copy() 352 365 c._where = self._where[:] 366 c._groupby = self._groupby[:] 353 367 c._params = self._params[:] 354 368 c._tables = self._tables[:] 355 369 c._offset = self._offset … … 386 400 tables = [quote_only_if_word(t) for t in self._tables] 387 401 joins = SortedDict() 388 402 where = self._where[:] 403 groupby = self._groupby[:] 389 404 params = self._params[:] 390 405 391 406 # 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) 393 408 tables.extend(tables2) 394 409 joins.update(joins2) 395 410 where.extend(where2) 411 groupby.extend(groupby2) 396 412 params.extend(params2) 397 413 398 414 # Add additional tables and WHERE clauses based on select_related. … … 419 435 if where: 420 436 sql.append(where and "WHERE " + " AND ".join(where)) 421 437 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 422 444 # ORDER BY clause 423 445 order_by = [] 424 446 if self._order_by is not None: … … 518 540 self.args = args 519 541 520 542 def get_sql(self, opts): 521 tables, joins, where, params = [], SortedDict(), [], []543 tables, joins, where, groupby, params = [], SortedDict(), [], [], [] 522 544 for val in self.args: 523 tables2, joins2, where2, params2 = val.get_sql(opts)545 tables2, joins2, where2, groupby2, params2 = val.get_sql(opts) 524 546 tables.extend(tables2) 525 547 joins.update(joins2) 526 548 where.extend(where2) 549 groupby.extend(groupby2) 527 550 params.extend(params2) 528 551 if where: 529 return tables, joins, ['(%s)' % self.operator.join(where)], params530 return tables, joins, [], params552 return tables, joins, ['(%s)' % self.operator.join(where)], groupby, params 553 return tables, joins, [], groupby, params 531 554 532 555 class QAnd(QOperator): 533 556 "Encapsulates a combined query that uses 'AND'." … … 575 598 "Encapsulates NOT (...) queries as objects" 576 599 577 600 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) 579 602 where2 = ['(NOT (%s))' % " AND ".join(where)] 580 return tables, joins, where2, params603 return tables, joins, where2, groupby, params 581 604 582 605 def get_where_clause(lookup_type, table_prefix, field_name, value): 583 606 if table_prefix.endswith('.'): … … 587 610 return '%s%s %s' % (table_prefix, field_name, (backend.OPERATOR_MAPPING[lookup_type] % '%s')) 588 611 except KeyError: 589 612 pass 590 if lookup_type == 'in':613 if lookup_type in ('in', 'list_or', 'list_and'): 591 614 return '%s%s IN (%s)' % (table_prefix, field_name, ','.join(['%s' for v in value])) 592 615 elif lookup_type == 'range': 593 616 return '%s%s BETWEEN %%s AND %%s' % (table_prefix, field_name) … … 648 671 # At present, this method only every returns INNER JOINs; the option is 649 672 # there for others to implement custom Q()s, etc that return other join 650 673 # types. 651 tables, joins, where, params = [], SortedDict(), [], []674 tables, joins, where, groupby, params = [], SortedDict(), [], [], [] 652 675 653 676 for kwarg, value in kwarg_items: 654 677 if value is not None: … … 674 697 if len(path) < 1: 675 698 raise TypeError, "Cannot parse keyword query %r" % kwarg 676 699 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) 678 701 tables.extend(tables2) 679 702 joins.update(joins2) 680 703 where.extend(where2) 704 groupby.extend(groupby2) 681 705 params.extend(params2) 682 return tables, joins, where, params706 return tables, joins, where, groupby, params 683 707 684 708 class FieldFound(Exception): 685 709 "Exception used to short circuit field-finding operations." … … 699 723 return matches[0] 700 724 701 725 def lookup_inner(path, clause, value, opts, table, column): 702 tables, joins, where, params = [], SortedDict(), [], []726 tables, joins, where, groupby, params = [], SortedDict(), [], [], [] 703 727 current_opts = opts 704 728 current_table = table 705 729 current_column = column … … 817 841 join_column = None 818 842 819 843 # 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) 821 845 822 846 tables.extend(tables2) 823 847 joins.update(joins2) 824 848 where.extend(where2) 849 groupby.extend(groupby2) 825 850 params.extend(params2) 826 851 else: 827 852 # Evaluate clause on current table. … … 832 857 column = current_column 833 858 else: 834 859 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) 835 864 836 865 where.append(get_where_clause(clause, current_table + '.', column, value)) 837 866 params.extend(field.get_db_prep_lookup(clause, value)) 838 867 839 return tables, joins, where, params868 return tables, joins, where, groupby, params 840 869 841 870 def delete_objects(seen_objs): 842 871 "Iterate through a list of seen classes, and remove any instances that are referred to"