Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django_filters/filters.py: 51%

357 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from collections import OrderedDict 

2from datetime import timedelta 

3 

4from django import forms 

5from django.core.validators import MaxValueValidator 

6from django.db.models import Q 

7from django.db.models.constants import LOOKUP_SEP 

8from django.forms.utils import pretty_name 

9from django.utils.itercompat import is_iterable 

10from django.utils.timezone import now 

11from django.utils.translation import gettext_lazy as _ 

12 

13from .conf import settings 

14from .constants import EMPTY_VALUES 

15from .fields import ( 

16 BaseCSVField, 

17 BaseRangeField, 

18 ChoiceField, 

19 DateRangeField, 

20 DateTimeRangeField, 

21 IsoDateTimeField, 

22 IsoDateTimeRangeField, 

23 LookupChoiceField, 

24 ModelChoiceField, 

25 ModelMultipleChoiceField, 

26 MultipleChoiceField, 

27 RangeField, 

28 TimeRangeField, 

29) 

30from .utils import get_model_field, label_for_filter 

31 

32__all__ = [ 

33 "AllValuesFilter", 

34 "AllValuesMultipleFilter", 

35 "BaseCSVFilter", 

36 "BaseInFilter", 

37 "BaseRangeFilter", 

38 "BooleanFilter", 

39 "CharFilter", 

40 "ChoiceFilter", 

41 "DateFilter", 

42 "DateFromToRangeFilter", 

43 "DateRangeFilter", 

44 "DateTimeFilter", 

45 "DateTimeFromToRangeFilter", 

46 "DurationFilter", 

47 "Filter", 

48 "IsoDateTimeFilter", 

49 "IsoDateTimeFromToRangeFilter", 

50 "LookupChoiceFilter", 

51 "ModelChoiceFilter", 

52 "ModelMultipleChoiceFilter", 

53 "MultipleChoiceFilter", 

54 "NumberFilter", 

55 "NumericRangeFilter", 

56 "OrderingFilter", 

57 "RangeFilter", 

58 "TimeFilter", 

59 "TimeRangeFilter", 

60 "TypedChoiceFilter", 

61 "TypedMultipleChoiceFilter", 

62 "UUIDFilter", 

63] 

64 

65 

66class Filter: 

67 creation_counter = 0 

68 field_class = forms.Field 

69 

70 def __init__( 

71 self, 

72 field_name=None, 

73 lookup_expr=None, 

74 *, 

75 label=None, 

76 method=None, 

77 distinct=False, 

78 exclude=False, 

79 **kwargs 

80 ): 

81 if lookup_expr is None: 

82 lookup_expr = settings.DEFAULT_LOOKUP_EXPR 

83 self.field_name = field_name 

84 self.lookup_expr = lookup_expr 

85 self.label = label 

86 self.method = method 

87 self.distinct = distinct 

88 self.exclude = exclude 

89 

90 self.extra = kwargs 

91 self.extra.setdefault("required", False) 

92 

93 self.creation_counter = Filter.creation_counter 

94 Filter.creation_counter += 1 

95 

96 def get_method(self, qs): 

97 """Return filter method based on whether we're excluding 

98 or simply filtering. 

99 """ 

100 return qs.exclude if self.exclude else qs.filter 

101 

102 def method(): 

103 """ 

104 Filter method needs to be lazily resolved, as it may be dependent on 

105 the 'parent' FilterSet. 

106 """ 

107 

108 def fget(self): 

109 return self._method 

110 

111 def fset(self, value): 

112 self._method = value 

113 

114 # clear existing FilterMethod 

115 if isinstance(self.filter, FilterMethod): 115 ↛ 116line 115 didn't jump to line 116, because the condition on line 115 was never true

116 del self.filter 

117 

118 # override filter w/ FilterMethod. 

119 if value is not None: 

120 self.filter = FilterMethod(self) 

121 

122 return locals() 

123 

124 method = property(**method()) 

125 

126 def label(): 

127 def fget(self): 

128 if self._label is None and hasattr(self, "model"): 128 ↛ 132line 128 didn't jump to line 132, because the condition on line 128 was never false

129 self._label = label_for_filter( 

130 self.model, self.field_name, self.lookup_expr, self.exclude 

131 ) 

132 return self._label 

133 

134 def fset(self, value): 

135 self._label = value 

136 

137 return locals() 

138 

139 label = property(**label()) 

140 

141 @property 

142 def field(self): 

143 if not hasattr(self, "_field"): 143 ↛ 150line 143 didn't jump to line 150, because the condition on line 143 was never false

144 field_kwargs = self.extra.copy() 

145 

146 if settings.DISABLE_HELP_TEXT: 146 ↛ 147line 146 didn't jump to line 147, because the condition on line 146 was never true

147 field_kwargs.pop("help_text", None) 

148 

149 self._field = self.field_class(label=self.label, **field_kwargs) 

150 return self._field 

151 

152 def filter(self, qs, value): 

153 if value in EMPTY_VALUES: 

154 return qs 

155 if self.distinct: 155 ↛ 156line 155 didn't jump to line 156, because the condition on line 155 was never true

156 qs = qs.distinct() 

157 lookup = "%s__%s" % (self.field_name, self.lookup_expr) 

158 qs = self.get_method(qs)(**{lookup: value}) 

159 return qs 

160 

161 

162class CharFilter(Filter): 

163 field_class = forms.CharField 

164 

165 

166class BooleanFilter(Filter): 

167 field_class = forms.NullBooleanField 

168 

169 

170class ChoiceFilter(Filter): 

171 field_class = ChoiceField 

172 

173 def __init__(self, *args, **kwargs): 

174 self.null_value = kwargs.get("null_value", settings.NULL_CHOICE_VALUE) 

175 super().__init__(*args, **kwargs) 

176 

177 def filter(self, qs, value): 

178 if value != self.null_value: 178 ↛ 181line 178 didn't jump to line 181, because the condition on line 178 was never false

179 return super().filter(qs, value) 

180 

181 qs = self.get_method(qs)( 

182 **{"%s__%s" % (self.field_name, self.lookup_expr): None} 

183 ) 

184 return qs.distinct() if self.distinct else qs 

185 

186 

187class TypedChoiceFilter(Filter): 

188 field_class = forms.TypedChoiceField 

189 

190 

191class UUIDFilter(Filter): 

192 field_class = forms.UUIDField 

193 

194 

195class MultipleChoiceFilter(Filter): 

196 """ 

197 This filter performs OR(by default) or AND(using conjoined=True) query 

198 on the selected options. 

199 

200 Advanced usage 

201 -------------- 

202 Depending on your application logic, when all or no choices are selected, 

203 filtering may be a no-operation. In this case you may wish to avoid the 

204 filtering overhead, particularly if using a `distinct` call. 

205 

206 You can override `get_filter_predicate` to use a custom filter. 

207 By default it will use the filter's name for the key, and the value will 

208 be the model object - or in case of passing in `to_field_name` the 

209 value of that attribute on the model. 

210 

211 Set `always_filter` to `False` after instantiation to enable the default 

212 `is_noop` test. You can override `is_noop` if you need a different test 

213 for your application. 

214 

215 `distinct` defaults to `True` as to-many relationships will generally 

216 require this. 

217 """ 

218 

219 field_class = MultipleChoiceField 

220 

221 always_filter = True 

222 

223 def __init__(self, *args, **kwargs): 

224 kwargs.setdefault("distinct", True) 

225 self.conjoined = kwargs.pop("conjoined", False) 

226 self.null_value = kwargs.get("null_value", settings.NULL_CHOICE_VALUE) 

227 super().__init__(*args, **kwargs) 

228 

229 def is_noop(self, qs, value): 

230 """ 

231 Return `True` to short-circuit unnecessary and potentially slow 

232 filtering. 

233 """ 

234 if self.always_filter: 

235 return False 

236 

237 # A reasonable default for being a noop... 

238 if self.extra.get("required") and len(value) == len(self.field.choices): 

239 return True 

240 

241 return False 

242 

243 def filter(self, qs, value): 

244 if not value: 

245 # Even though not a noop, no point filtering if empty. 

246 return qs 

247 

248 if self.is_noop(qs, value): 

249 return qs 

250 

251 if not self.conjoined: 

252 q = Q() 

253 for v in set(value): 

254 if v == self.null_value: 

255 v = None 

256 predicate = self.get_filter_predicate(v) 

257 if self.conjoined: 

258 qs = self.get_method(qs)(**predicate) 

259 else: 

260 q |= Q(**predicate) 

261 

262 if not self.conjoined: 

263 qs = self.get_method(qs)(q) 

264 

265 return qs.distinct() if self.distinct else qs 

266 

267 def get_filter_predicate(self, v): 

268 name = self.field_name 

269 if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR: 

270 name = LOOKUP_SEP.join([name, self.lookup_expr]) 

271 try: 

272 return {name: getattr(v, self.field.to_field_name)} 

273 except (AttributeError, TypeError): 

274 return {name: v} 

275 

276 

277class TypedMultipleChoiceFilter(MultipleChoiceFilter): 

278 field_class = forms.TypedMultipleChoiceField 

279 

280 

281class DateFilter(Filter): 

282 field_class = forms.DateField 

283 

284 

285class DateTimeFilter(Filter): 

286 field_class = forms.DateTimeField 

287 

288 

289class IsoDateTimeFilter(DateTimeFilter): 

290 """ 

291 Uses IsoDateTimeField to support filtering on ISO 8601 formatted datetimes. 

292 

293 For context see: 

294 

295 * https://code.djangoproject.com/ticket/23448 

296 * https://github.com/encode/django-rest-framework/issues/1338 

297 * https://github.com/carltongibson/django-filter/pull/264 

298 """ 

299 

300 field_class = IsoDateTimeField 

301 

302 

303class TimeFilter(Filter): 

304 field_class = forms.TimeField 

305 

306 

307class DurationFilter(Filter): 

308 field_class = forms.DurationField 

309 

310 

311class QuerySetRequestMixin: 

312 """ 

313 Add callable functionality to filters that support the ``queryset`` 

314 argument. If the ``queryset`` is callable, then it **must** accept the 

315 ``request`` object as a single argument. 

316 

317 This is useful for filtering querysets by properties on the ``request`` 

318 object, such as the user. 

319 

320 Example:: 

321 

322 def departments(request): 

323 company = request.user.company 

324 return company.department_set.all() 

325 

326 class EmployeeFilter(filters.FilterSet): 

327 department = filters.ModelChoiceFilter(queryset=departments) 

328 ... 

329 

330 The above example restricts the set of departments to those in the logged-in 

331 user's associated company. 

332 

333 """ 

334 

335 def __init__(self, *args, **kwargs): 

336 self.queryset = kwargs.get("queryset") 

337 super().__init__(*args, **kwargs) 

338 

339 def get_request(self): 

340 try: 

341 return self.parent.request 

342 except AttributeError: 

343 return None 

344 

345 def get_queryset(self, request): 

346 queryset = self.queryset 

347 

348 if callable(queryset): 348 ↛ 349line 348 didn't jump to line 349, because the condition on line 348 was never true

349 return queryset(request) 

350 return queryset 

351 

352 @property 

353 def field(self): 

354 request = self.get_request() 

355 queryset = self.get_queryset(request) 

356 

357 if queryset is not None: 357 ↛ 360line 357 didn't jump to line 360, because the condition on line 357 was never false

358 self.extra["queryset"] = queryset 

359 

360 return super().field 

361 

362 

363class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter): 

364 field_class = ModelChoiceField 

365 

366 def __init__(self, *args, **kwargs): 

367 kwargs.setdefault("empty_label", settings.EMPTY_CHOICE_LABEL) 

368 super().__init__(*args, **kwargs) 

369 

370 

371class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter): 

372 field_class = ModelMultipleChoiceField 

373 

374 

375class NumberFilter(Filter): 

376 field_class = forms.DecimalField 

377 

378 def get_max_validator(self): 

379 """ 

380 Return a MaxValueValidator for the field, or None to disable. 

381 """ 

382 return MaxValueValidator(1e50) 

383 

384 @property 

385 def field(self): 

386 if not hasattr(self, "_field"): 

387 field = super().field 

388 max_validator = self.get_max_validator() 

389 if max_validator: 

390 field.validators.append(max_validator) 

391 

392 self._field = field 

393 return self._field 

394 

395 

396class NumericRangeFilter(Filter): 

397 field_class = RangeField 

398 

399 def filter(self, qs, value): 

400 if value: 

401 if value.start is not None and value.stop is not None: 

402 value = (value.start, value.stop) 

403 elif value.start is not None: 

404 self.lookup_expr = "startswith" 

405 value = value.start 

406 elif value.stop is not None: 

407 self.lookup_expr = "endswith" 

408 value = value.stop 

409 

410 return super().filter(qs, value) 

411 

412 

413class RangeFilter(Filter): 

414 field_class = RangeField 

415 

416 def filter(self, qs, value): 

417 if value: 

418 if value.start is not None and value.stop is not None: 

419 self.lookup_expr = "range" 

420 value = (value.start, value.stop) 

421 elif value.start is not None: 

422 self.lookup_expr = "gte" 

423 value = value.start 

424 elif value.stop is not None: 

425 self.lookup_expr = "lte" 

426 value = value.stop 

427 

428 return super().filter(qs, value) 

429 

430 

431def _truncate(dt): 

432 return dt.date() 

433 

434 

435class DateRangeFilter(ChoiceFilter): 

436 choices = [ 

437 ("today", _("Today")), 

438 ("yesterday", _("Yesterday")), 

439 ("week", _("Past 7 days")), 

440 ("month", _("This month")), 

441 ("year", _("This year")), 

442 ] 

443 

444 filters = { 444 ↛ exitline 444 didn't jump to the function exit

445 "today": lambda qs, name: qs.filter( 

446 **{ 

447 "%s__year" % name: now().year, 

448 "%s__month" % name: now().month, 

449 "%s__day" % name: now().day, 

450 } 

451 ), 

452 "yesterday": lambda qs, name: qs.filter( 

453 **{ 

454 "%s__year" % name: (now() - timedelta(days=1)).year, 

455 "%s__month" % name: (now() - timedelta(days=1)).month, 

456 "%s__day" % name: (now() - timedelta(days=1)).day, 

457 } 

458 ), 

459 "week": lambda qs, name: qs.filter( 

460 **{ 

461 "%s__gte" % name: _truncate(now() - timedelta(days=7)), 

462 "%s__lt" % name: _truncate(now() + timedelta(days=1)), 

463 } 

464 ), 

465 "month": lambda qs, name: qs.filter( 

466 **{"%s__year" % name: now().year, "%s__month" % name: now().month} 

467 ), 

468 "year": lambda qs, name: qs.filter( 

469 **{ 

470 "%s__year" % name: now().year, 

471 } 

472 ), 

473 } 

474 

475 def __init__(self, choices=None, filters=None, *args, **kwargs): 

476 if choices is not None: 

477 self.choices = choices 

478 if filters is not None: 

479 self.filters = filters 

480 

481 unique = set([x[0] for x in self.choices]) ^ set(self.filters) 

482 assert not unique, ( 

483 "Keys must be present in both 'choices' and 'filters'. Missing keys: " 

484 "'%s'" % ", ".join(sorted(unique)) 

485 ) 

486 

487 # null choice not relevant 

488 kwargs.setdefault("null_label", None) 

489 super().__init__(choices=self.choices, *args, **kwargs) 

490 

491 def filter(self, qs, value): 

492 if not value: 

493 return qs 

494 

495 assert value in self.filters 

496 

497 qs = self.filters[value](qs, self.field_name) 

498 return qs.distinct() if self.distinct else qs 

499 

500 

501class DateFromToRangeFilter(RangeFilter): 

502 field_class = DateRangeField 

503 

504 

505class DateTimeFromToRangeFilter(RangeFilter): 

506 field_class = DateTimeRangeField 

507 

508 

509class IsoDateTimeFromToRangeFilter(RangeFilter): 

510 field_class = IsoDateTimeRangeField 

511 

512 

513class TimeRangeFilter(RangeFilter): 

514 field_class = TimeRangeField 

515 

516 

517class AllValuesFilter(ChoiceFilter): 

518 @property 

519 def field(self): 

520 qs = self.model._default_manager.distinct() 

521 qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True) 

522 self.extra["choices"] = [(o, o) for o in qs] 

523 return super().field 

524 

525 

526class AllValuesMultipleFilter(MultipleChoiceFilter): 

527 @property 

528 def field(self): 

529 qs = self.model._default_manager.distinct() 

530 qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True) 

531 self.extra["choices"] = [(o, o) for o in qs] 

532 return super().field 

533 

534 

535class BaseCSVFilter(Filter): 

536 """ 

537 Base class for CSV type filters, such as IN and RANGE. 

538 """ 

539 

540 base_field_class = BaseCSVField 

541 

542 def __init__(self, *args, **kwargs): 

543 kwargs.setdefault("help_text", _("Multiple values may be separated by commas.")) 

544 super().__init__(*args, **kwargs) 

545 

546 class ConcreteCSVField(self.base_field_class, self.field_class): 

547 pass 

548 

549 ConcreteCSVField.__name__ = self._field_class_name( 

550 self.field_class, self.lookup_expr 

551 ) 

552 

553 self.field_class = ConcreteCSVField 

554 

555 @classmethod 

556 def _field_class_name(cls, field_class, lookup_expr): 

557 """ 

558 Generate a suitable class name for the concrete field class. This is not 

559 completely reliable, as not all field class names are of the format 

560 <Type>Field. 

561 

562 ex:: 

563 

564 BaseCSVFilter._field_class_name(DateTimeField, 'year__in') 

565 

566 returns 'DateTimeYearInField' 

567 

568 """ 

569 # DateTimeField => DateTime 

570 type_name = field_class.__name__ 

571 if type_name.endswith("Field"): 

572 type_name = type_name[:-5] 

573 

574 # year__in => YearIn 

575 parts = lookup_expr.split(LOOKUP_SEP) 

576 expression_name = "".join(p.capitalize() for p in parts) 

577 

578 # DateTimeYearInField 

579 return str("%s%sField" % (type_name, expression_name)) 

580 

581 

582class BaseInFilter(BaseCSVFilter): 

583 def __init__(self, *args, **kwargs): 

584 kwargs.setdefault("lookup_expr", "in") 

585 super().__init__(*args, **kwargs) 

586 

587 

588class BaseRangeFilter(BaseCSVFilter): 

589 base_field_class = BaseRangeField 

590 

591 def __init__(self, *args, **kwargs): 

592 kwargs.setdefault("lookup_expr", "range") 

593 super().__init__(*args, **kwargs) 

594 

595 

596class LookupChoiceFilter(Filter): 

597 """ 

598 A combined filter that allows users to select the lookup expression from a dropdown. 

599 

600 * ``lookup_choices`` is an optional argument that accepts multiple input 

601 formats, and is ultimately normlized as the choices used in the lookup 

602 dropdown. See ``.get_lookup_choices()`` for more information. 

603 

604 * ``field_class`` is an optional argument that allows you to set the inner 

605 form field class used to validate the value. Default: ``forms.CharField`` 

606 

607 ex:: 

608 

609 price = django_filters.LookupChoiceFilter( 

610 field_class=forms.DecimalField, 

611 lookup_choices=[ 

612 ('exact', 'Equals'), 

613 ('gt', 'Greater than'), 

614 ('lt', 'Less than'), 

615 ] 

616 ) 

617 

618 """ 

619 

620 field_class = forms.CharField 

621 outer_class = LookupChoiceField 

622 

623 def __init__( 

624 self, field_name=None, lookup_choices=None, field_class=None, **kwargs 

625 ): 

626 self.empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL) 

627 

628 super(LookupChoiceFilter, self).__init__(field_name=field_name, **kwargs) 

629 

630 self.lookup_choices = lookup_choices 

631 if field_class is not None: 

632 self.field_class = field_class 

633 

634 @classmethod 

635 def normalize_lookup(cls, lookup): 

636 """ 

637 Normalize the lookup into a tuple of ``(lookup expression, display value)`` 

638 

639 If the ``lookup`` is already a tuple, the tuple is not altered. 

640 If the ``lookup`` is a string, a tuple is returned with the lookup 

641 expression used as the basis for the display value. 

642 

643 ex:: 

644 

645 >>> LookupChoiceFilter.normalize_lookup(('exact', 'Equals')) 

646 ('exact', 'Equals') 

647 

648 >>> LookupChoiceFilter.normalize_lookup('has_key') 

649 ('has_key', 'Has key') 

650 

651 """ 

652 if isinstance(lookup, str): 

653 return (lookup, pretty_name(lookup)) 

654 return (lookup[0], lookup[1]) 

655 

656 def get_lookup_choices(self): 

657 """ 

658 Get the lookup choices in a format suitable for ``django.forms.ChoiceField``. 

659 If the filter is initialized with ``lookup_choices``, this value is normalized 

660 and passed to the underlying ``LookupChoiceField``. If no choices are provided, 

661 they are generated from the corresponding model field's registered lookups. 

662 """ 

663 lookups = self.lookup_choices 

664 if lookups is None: 

665 field = get_model_field(self.model, self.field_name) 

666 lookups = field.get_lookups() 

667 

668 return [self.normalize_lookup(lookup) for lookup in lookups] 

669 

670 @property 

671 def field(self): 

672 if not hasattr(self, "_field"): 

673 inner_field = super().field 

674 lookups = self.get_lookup_choices() 

675 

676 self._field = self.outer_class( 

677 inner_field, 

678 lookups, 

679 label=self.label, 

680 empty_label=self.empty_label, 

681 required=self.extra["required"], 

682 ) 

683 

684 return self._field 

685 

686 def filter(self, qs, lookup): 

687 if not lookup: 

688 return super().filter(qs, None) 

689 

690 self.lookup_expr = lookup.lookup_expr 

691 return super().filter(qs, lookup.value) 

692 

693 

694class OrderingFilter(BaseCSVFilter, ChoiceFilter): 

695 """ 

696 Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts 

697 two additional arguments that are used to build the ordering choices. 

698 

699 * ``fields`` is a mapping of {model field name: parameter name}. The 

700 parameter names are exposed in the choices and mask/alias the field 

701 names used in the ``order_by()`` call. Similar to field ``choices``, 

702 ``fields`` accepts the 'list of two-tuples' syntax that retains order. 

703 ``fields`` may also just be an iterable of strings. In this case, the 

704 field names simply double as the exposed parameter names. 

705 

706 * ``field_labels`` is an optional argument that allows you to customize 

707 the display label for the corresponding parameter. It accepts a mapping 

708 of {field name: human readable label}. Keep in mind that the key is the 

709 field name, and not the exposed parameter name. 

710 

711 Additionally, you can just provide your own ``choices`` if you require 

712 explicit control over the exposed options. For example, when you might 

713 want to disable descending sort options. 

714 

715 This filter is also CSV-based, and accepts multiple ordering params. The 

716 default select widget does not enable the use of this, but it is useful 

717 for APIs. 

718 

719 """ 

720 

721 descending_fmt = _("%s (descending)") 

722 

723 def __init__(self, *args, **kwargs): 

724 """ 

725 ``fields`` may be either a mapping or an iterable. 

726 ``field_labels`` must be a map of field names to display labels 

727 """ 

728 fields = kwargs.pop("fields", {}) 

729 fields = self.normalize_fields(fields) 

730 field_labels = kwargs.pop("field_labels", {}) 

731 

732 self.param_map = {v: k for k, v in fields.items()} 

733 

734 if "choices" not in kwargs: 

735 kwargs["choices"] = self.build_choices(fields, field_labels) 

736 

737 kwargs.setdefault("label", _("Ordering")) 

738 kwargs.setdefault("help_text", "") 

739 kwargs.setdefault("null_label", None) 

740 super().__init__(*args, **kwargs) 

741 

742 def get_ordering_value(self, param): 

743 descending = param.startswith("-") 

744 param = param[1:] if descending else param 

745 field_name = self.param_map.get(param, param) 

746 

747 return "-%s" % field_name if descending else field_name 

748 

749 def filter(self, qs, value): 

750 if value in EMPTY_VALUES: 

751 return qs 

752 

753 ordering = [self.get_ordering_value(param) for param in value] 

754 return qs.order_by(*ordering) 

755 

756 @classmethod 

757 def normalize_fields(cls, fields): 

758 """ 

759 Normalize the fields into an ordered map of {field name: param name} 

760 """ 

761 # fields is a mapping, copy into new OrderedDict 

762 if isinstance(fields, dict): 

763 return OrderedDict(fields) 

764 

765 # convert iterable of values => iterable of pairs (field name, param name) 

766 assert is_iterable( 

767 fields 

768 ), "'fields' must be an iterable (e.g., a list, tuple, or mapping)." 

769 

770 # fields is an iterable of field names 

771 assert all( 

772 isinstance(field, str) 

773 or is_iterable(field) 

774 and len(field) == 2 # may need to be wrapped in parens 

775 for field in fields 

776 ), "'fields' must contain strings or (field name, param name) pairs." 

777 

778 return OrderedDict([(f, f) if isinstance(f, str) else f for f in fields]) 

779 

780 def build_choices(self, fields, labels): 

781 ascending = [ 

782 (param, labels.get(field, _(pretty_name(param)))) 

783 for field, param in fields.items() 

784 ] 

785 descending = [ 

786 ("-%s" % param, labels.get("-%s" % param, self.descending_fmt % label)) 

787 for param, label in ascending 

788 ] 

789 

790 # interleave the ascending and descending choices 

791 return [val for pair in zip(ascending, descending) for val in pair] 

792 

793 

794class FilterMethod: 

795 """ 

796 This helper is used to override Filter.filter() when a 'method' argument 

797 is passed. It proxies the call to the actual method on the filter's parent. 

798 """ 

799 

800 def __init__(self, filter_instance): 

801 self.f = filter_instance 

802 

803 def __call__(self, qs, value): 

804 if value in EMPTY_VALUES: 804 ↛ 807line 804 didn't jump to line 807, because the condition on line 804 was never false

805 return qs 

806 

807 return self.method(qs, self.f.field_name, value) 

808 

809 @property 

810 def method(self): 

811 """ 

812 Resolve the method on the parent filterset. 

813 """ 

814 instance = self.f 

815 

816 # noop if 'method' is a function 

817 if callable(instance.method): 

818 return instance.method 

819 

820 # otherwise, method is the name of a method on the parent FilterSet. 

821 assert hasattr( 

822 instance, "parent" 

823 ), "Filter '%s' must have a parent FilterSet to find '.%s()'" % ( 

824 instance.field_name, 

825 instance.method, 

826 ) 

827 

828 parent = instance.parent 

829 method = getattr(parent, instance.method, None) 

830 

831 assert callable( 

832 method 

833 ), "Expected parent FilterSet '%s.%s' to have a '.%s()' method." % ( 

834 parent.__class__.__module__, 

835 parent.__class__.__name__, 

836 instance.method, 

837 ) 

838 

839 return method