Coverage for .tox / coverage / lib / python3.11 / site-packages / wuttaweb / grids / filters.py: 100%

279 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-21 19:06 -0500

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# wuttaweb -- Web App for Wutta Framework 

5# Copyright © 2024-2026 Lance Edgar 

6# 

7# This file is part of Wutta Framework. 

8# 

9# Wutta Framework is free software: you can redistribute it and/or modify it 

10# under the terms of the GNU General Public License as published by the Free 

11# Software Foundation, either version 3 of the License, or (at your option) any 

12# later version. 

13# 

14# Wutta Framework is distributed in the hope that it will be useful, but 

15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

17# more details. 

18# 

19# You should have received a copy of the GNU General Public License along with 

20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>. 

21# 

22################################################################################ 

23""" 

24Grid Filters 

25""" 

26 

27import datetime 

28import logging 

29from collections import OrderedDict 

30 

31try: 

32 from enum import EnumType 

33except ImportError: # pragma: no cover 

34 # nb. python <= 3.10 

35 from enum import EnumMeta as EnumType 

36 

37import sqlalchemy as sa 

38 

39from wuttjamaican.util import UNSPECIFIED 

40 

41 

42log = logging.getLogger(__name__) 

43 

44 

45class VerbNotSupported(Exception): # pylint: disable=empty-docstring 

46 """ """ 

47 

48 def __init__(self, verb): 

49 self.verb = verb 

50 

51 def __str__(self): 

52 return f"unknown filter verb not supported: {self.verb}" 

53 

54 

55class GridFilter: # pylint: disable=too-many-instance-attributes 

56 """ 

57 Filter option for a grid. Represents both the "features" as well 

58 as "state" for the filter. 

59 

60 :param request: Current :term:`request` object. 

61 

62 :param nullable: Boolean indicating whether the filter should 

63 include ``is_null`` and ``is_not_null`` verbs. If not 

64 specified, the column will be inspected (if possible) and use 

65 its nullable flag. 

66 

67 :param \\**kwargs: Any additional kwargs will be set as attributes 

68 on the filter instance. 

69 

70 Filter instances have the following attributes: 

71 

72 .. attribute:: key 

73 

74 Unique key for the filter. This often corresponds to a "column 

75 name" for the grid, but not always. 

76 

77 .. attribute:: label 

78 

79 Display label for the filter field. 

80 

81 .. attribute:: data_type 

82 

83 Simplistic "data type" which the filter supports. So far this 

84 will be one of: 

85 

86 * ``'string'`` 

87 * ``'date'`` 

88 * ``'choice'`` 

89 

90 Note that this mainly applies to the "value input" used by the 

91 filter. There is no data type for boolean since it does not 

92 need a value input; the verb is enough. 

93 

94 .. attribute:: active 

95 

96 Boolean indicating whether the filter is currently active. 

97 

98 See also :attr:`verb` and :attr:`value`. 

99 

100 .. attribute:: verb 

101 

102 Verb for current filter, if :attr:`active` is true. 

103 

104 See also :attr:`value`. 

105 

106 .. attribute:: choices 

107 

108 OrderedDict of possible values for the filter. 

109 

110 This is safe to read from, but use :meth:`set_choices()` to 

111 update it. 

112 

113 .. attribute:: value 

114 

115 Value for current filter, if :attr:`active` is true. 

116 

117 See also :attr:`verb`. 

118 

119 .. attribute:: default_active 

120 

121 Boolean indicating whether the filter should be active by 

122 default, i.e. when first displaying the grid. 

123 

124 See also :attr:`default_verb` and :attr:`default_value`. 

125 

126 .. attribute:: default_verb 

127 

128 Filter verb to use by default. This will be auto-selected when 

129 the filter is first activated, or when first displaying the 

130 grid if :attr:`default_active` is true. 

131 

132 See also :attr:`default_value`. 

133 

134 .. attribute:: default_value 

135 

136 Filter value to use by default. This will be auto-populated 

137 when the filter is first activated, or when first displaying 

138 the grid if :attr:`default_active` is true. 

139 

140 See also :attr:`default_verb`. 

141 """ 

142 

143 data_type = "string" 

144 default_verbs = ["equal", "not_equal"] 

145 

146 default_verb_labels = { 

147 "is_any": "is any", 

148 "equal": "equal to", 

149 "not_equal": "not equal to", 

150 "greater_than": "greater than", 

151 "greater_equal": "greater than or equal to", 

152 "less_than": "less than", 

153 "less_equal": "less than or equal to", 

154 # 'between': "between", 

155 "is_true": "is true", 

156 "is_false": "is false", 

157 "is_false_null": "is false or null", 

158 "is_null": "is null", 

159 "is_not_null": "is not null", 

160 "contains": "contains", 

161 "does_not_contain": "does not contain", 

162 } 

163 

164 valueless_verbs = [ 

165 "is_any", 

166 "is_true", 

167 "is_false", 

168 "is_false_null", 

169 "is_null", 

170 "is_not_null", 

171 ] 

172 

173 def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments 

174 self, 

175 request, 

176 key, 

177 label=None, 

178 verbs=None, 

179 choices=None, 

180 nullable=None, 

181 default_active=False, 

182 default_verb=None, 

183 default_value=None, 

184 **kwargs, 

185 ): 

186 self.request = request 

187 self.key = key 

188 self.config = self.request.wutta_config 

189 self.app = self.config.get_app() 

190 self.label = label or self.app.make_title(self.key) 

191 

192 # remember original data type in case we need to revert, 

193 # e.g. after changing it to 'choices' and back again 

194 self.original_data_type = self.data_type 

195 

196 # active 

197 self.default_active = default_active 

198 self.active = self.default_active 

199 

200 # verb 

201 if verbs is not None: 

202 self.verbs = verbs 

203 if default_verb: 

204 self.default_verb = default_verb 

205 self.verb = None # active verb is set later 

206 

207 # choices 

208 self.set_choices(choices) 

209 

210 # nullable 

211 self.nullable = nullable 

212 

213 # value 

214 self.default_value = default_value 

215 self.value = self.default_value 

216 

217 self.__dict__.update(kwargs) 

218 

219 def __repr__(self): 

220 verb = getattr(self, "verb", None) 

221 return ( 

222 f"{self.__class__.__name__}(" 

223 f"key='{self.key}', " 

224 f"active={self.active}, " 

225 f"verb={repr(verb)}, " 

226 f"value={repr(self.value)})" 

227 ) 

228 

229 def get_verbs(self): 

230 """ 

231 Returns the list of verbs supported by the filter. 

232 """ 

233 verbs = None 

234 

235 if hasattr(self, "verbs"): 

236 verbs = self.verbs 

237 

238 else: 

239 verbs = self.default_verbs 

240 

241 if callable(verbs): 

242 verbs = verbs() 

243 verbs = list(verbs) 

244 

245 if self.nullable: 

246 if "is_null" not in verbs: 

247 verbs.append("is_null") 

248 if "is_not_null" not in verbs: 

249 verbs.append("is_not_null") 

250 

251 if "is_any" not in verbs: 

252 verbs.append("is_any") 

253 

254 return verbs 

255 

256 def get_verb_labels(self): 

257 """ 

258 Returns a dict of all defined verb labels. 

259 """ 

260 # TODO: should traverse hierarchy 

261 labels = {verb: verb for verb in self.get_verbs()} 

262 labels.update(self.default_verb_labels) 

263 return labels 

264 

265 def get_valueless_verbs(self): 

266 """ 

267 Returns a list of verb names which do not need a value. 

268 """ 

269 return self.valueless_verbs 

270 

271 def get_default_verb(self): 

272 """ 

273 Returns the default verb for the filter. 

274 """ 

275 verb = None 

276 

277 if hasattr(self, "default_verb"): 

278 verb = self.default_verb 

279 

280 elif hasattr(self, "verb"): 

281 verb = self.verb 

282 

283 if not verb: 

284 verbs = self.get_verbs() 

285 if verbs: 

286 verb = verbs[0] 

287 

288 return verb 

289 

290 def set_choices(self, choices): 

291 """ 

292 Set the value choices for the filter. 

293 

294 If ``choices`` is non-empty, it is passed to 

295 :meth:`normalize_choices()` and the result is assigned to 

296 :attr:`choices`. Also, the :attr:`data_type` is set to 

297 ``'choice'`` so the UI will present the value input as a 

298 dropdown. 

299 

300 But if ``choices`` is empty, :attr:`choices` is set to an 

301 empty dict, and :attr:`data_type` is set (back) to 

302 ``'string'``. 

303 

304 :param choices: Collection of "choices" or ``None``. 

305 """ 

306 if choices: 

307 self.choices = self.normalize_choices(choices) 

308 self.data_type = "choice" 

309 else: 

310 self.choices = {} 

311 if self.data_type == "choice": 

312 self.data_type = self.original_data_type 

313 

314 def normalize_choices(self, choices): 

315 """ 

316 Normalize a collection of "choices" to standard ``OrderedDict``. 

317 

318 This is called automatically by :meth:`set_choices()`. 

319 

320 :param choices: A collection of "choices" in one of the following 

321 formats: 

322 

323 * :class:`python:enum.Enum` class 

324 * simple list, each value of which should be a string, 

325 which is assumed to be able to serve as both key and 

326 value (ordering of choices will be preserved) 

327 * simple dict, keys and values of which will define the 

328 choices (note that the final choices will be sorted by 

329 key!) 

330 * OrderedDict, keys and values of which will define the 

331 choices (ordering of choices will be preserved) 

332 

333 :rtype: :class:`python:collections.OrderedDict` 

334 """ 

335 normalized = choices 

336 

337 if isinstance(choices, EnumType): 

338 normalized = OrderedDict( 

339 [(member.name, member.value) for member in choices] 

340 ) 

341 

342 elif isinstance(choices, OrderedDict): 

343 normalized = choices 

344 

345 elif isinstance(choices, dict): 

346 normalized = OrderedDict([(key, choices[key]) for key in sorted(choices)]) 

347 

348 elif isinstance(choices, list): 

349 normalized = OrderedDict([(key, key) for key in choices]) 

350 

351 return normalized 

352 

353 def coerce_value(self, value): 

354 """ 

355 Coerce the given value to the correct type/format for use with 

356 the filter. This is where e.g. a boolean or date filter 

357 should convert input string to ``bool`` or ``date`` value. 

358 

359 This is (usually) called from a filter method, when applying 

360 the filter. See also :meth:`apply_filter()`. 

361 

362 Default logic on the base class returns value as-is; subclass 

363 may override as needed. 

364 

365 :param value: Input string provided by the user via the filter 

366 form submission. 

367 

368 :returns: Value of the appropriate type, depending on the 

369 filter subclass. 

370 """ 

371 return value 

372 

373 def apply_filter(self, data, verb=None, value=UNSPECIFIED): 

374 """ 

375 Filter the given data set according to a verb/value pair. 

376 

377 If verb and/or value are not specified, will use :attr:`verb` 

378 and/or :attr:`value` instead. 

379 

380 This method does not directly filter the data; rather it 

381 delegates (based on ``verb``) to some other method. The 

382 latter may choose *not* to filter the data, e.g. if ``value`` 

383 is empty, in which case this may return the original data set 

384 unchanged. 

385 

386 :returns: The (possibly) filtered data set. 

387 """ 

388 if verb is None: 

389 verb = self.verb 

390 if not verb: 

391 verb = self.get_default_verb() 

392 log.warning( 

393 "missing verb for '%s' filter, will use default verb: %s", 

394 self.key, 

395 verb, 

396 ) 

397 

398 # only attempt for known verbs 

399 if verb not in self.get_verbs(): 

400 raise VerbNotSupported(verb) 

401 

402 # fallback value 

403 if value is UNSPECIFIED: 

404 value = self.value 

405 

406 # locate filter method 

407 func = getattr(self, f"filter_{verb}", None) 

408 if not func: 

409 raise VerbNotSupported(verb) 

410 

411 # invoke filter method 

412 return func(data, value) # pylint: disable=not-callable 

413 

414 def filter_is_any(self, data, value): # pylint: disable=unused-argument 

415 """ 

416 This is a no-op which always ignores the value and returns the 

417 data as-is. 

418 """ 

419 return data 

420 

421 

422class AlchemyFilter(GridFilter): 

423 """ 

424 Filter option for a grid with SQLAlchemy query data. 

425 

426 This is a subclass of :class:`GridFilter`. It requires a 

427 ``model_property`` to know how to filter the query. 

428 

429 :param model_property: Property of a model class, representing the 

430 column by which to filter. For instance, 

431 ``model.Person.full_name``. 

432 """ 

433 

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

435 self.model_property = kwargs.pop("model_property") 

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

437 

438 if self.nullable is None: 

439 columns = self.model_property.prop.columns 

440 if len(columns) == 1: 

441 self.nullable = columns[0].nullable 

442 

443 def filter_equal(self, query, value): 

444 """ 

445 Filter data with an equal (``=``) condition. 

446 """ 

447 value = self.coerce_value(value) 

448 if value is None: 

449 return query 

450 

451 return query.filter(self.model_property == value) 

452 

453 def filter_not_equal(self, query, value): 

454 """ 

455 Filter data with a not equal (``!=``) condition. 

456 """ 

457 value = self.coerce_value(value) 

458 if value is None: 

459 return query 

460 

461 # sql probably excludes null values from results, but user 

462 # probably does not expect that, so explicitly include them. 

463 return query.filter( 

464 sa.or_( 

465 self.model_property == None, # pylint: disable=singleton-comparison 

466 self.model_property != value, 

467 ) 

468 ) 

469 

470 def filter_greater_than(self, query, value): 

471 """ 

472 Filter data with a greater than (``>``) condition. 

473 """ 

474 value = self.coerce_value(value) 

475 if value is None: 

476 return query 

477 return query.filter(self.model_property > value) 

478 

479 def filter_greater_equal(self, query, value): 

480 """ 

481 Filter data with a greater than or equal (``>=``) condition. 

482 """ 

483 value = self.coerce_value(value) 

484 if value is None: 

485 return query 

486 return query.filter(self.model_property >= value) 

487 

488 def filter_less_than(self, query, value): 

489 """ 

490 Filter data with a less than (``<``) condition. 

491 """ 

492 value = self.coerce_value(value) 

493 if value is None: 

494 return query 

495 return query.filter(self.model_property < value) 

496 

497 def filter_less_equal(self, query, value): 

498 """ 

499 Filter data with a less than or equal (``<=``) condition. 

500 """ 

501 value = self.coerce_value(value) 

502 if value is None: 

503 return query 

504 return query.filter(self.model_property <= value) 

505 

506 def filter_is_null(self, query, value): # pylint: disable=unused-argument 

507 """ 

508 Filter data with an ``IS NULL`` query. The value is ignored. 

509 """ 

510 return query.filter( 

511 self.model_property == None # pylint: disable=singleton-comparison 

512 ) 

513 

514 def filter_is_not_null(self, query, value): # pylint: disable=unused-argument 

515 """ 

516 Filter data with an ``IS NOT NULL`` query. The value is 

517 ignored. 

518 """ 

519 return query.filter( 

520 self.model_property != None # pylint: disable=singleton-comparison 

521 ) 

522 

523 

524class StringAlchemyFilter(AlchemyFilter): 

525 """ 

526 SQLAlchemy filter option for a text data column. 

527 

528 Subclass of :class:`AlchemyFilter`. 

529 """ 

530 

531 default_verbs = ["contains", "does_not_contain", "equal", "not_equal"] 

532 

533 def coerce_value(self, value): # pylint: disable=empty-docstring 

534 """ """ 

535 if value is not None: 

536 value = str(value) 

537 if value: 

538 return value 

539 return None 

540 

541 def filter_contains(self, query, value): 

542 """ 

543 Filter data with an ``ILIKE`` condition. 

544 """ 

545 value = self.coerce_value(value) 

546 if not value: 

547 return query 

548 

549 criteria = [] 

550 for val in value.split(): 

551 val = val.replace("_", r"\_") 

552 val = f"%{val}%" 

553 criteria.append(self.model_property.ilike(val)) 

554 

555 return query.filter(sa.and_(*criteria)) 

556 

557 def filter_does_not_contain(self, query, value): 

558 """ 

559 Filter data with a ``NOT ILIKE`` condition. 

560 """ 

561 value = self.coerce_value(value) 

562 if not value: 

563 return query 

564 

565 criteria = [] 

566 for val in value.split(): 

567 val = val.replace("_", r"\_") 

568 val = f"%{val}%" 

569 criteria.append(~self.model_property.ilike(val)) 

570 

571 # sql probably excludes null values from results, but user 

572 # probably does not expect that, so explicitly include them. 

573 return query.filter( 

574 sa.or_( 

575 self.model_property == None, # pylint: disable=singleton-comparison 

576 sa.and_(*criteria), 

577 ) 

578 ) 

579 

580 

581class NumericAlchemyFilter(AlchemyFilter): 

582 """ 

583 SQLAlchemy filter option for a numeric data column. 

584 

585 Subclass of :class:`AlchemyFilter`. 

586 """ 

587 

588 default_verbs = [ 

589 "equal", 

590 "not_equal", 

591 "greater_than", 

592 "greater_equal", 

593 "less_than", 

594 "less_equal", 

595 ] 

596 

597 

598class IntegerAlchemyFilter(NumericAlchemyFilter): 

599 """ 

600 SQLAlchemy filter option for an integer data column. 

601 

602 Subclass of :class:`NumericAlchemyFilter`. 

603 """ 

604 

605 def coerce_value(self, value): # pylint: disable=empty-docstring 

606 """ """ 

607 if value: 

608 try: 

609 return int(value) 

610 except Exception: # pylint: disable=broad-exception-caught 

611 pass 

612 return None 

613 

614 

615class BooleanAlchemyFilter(AlchemyFilter): 

616 """ 

617 SQLAlchemy filter option for a boolean data column. 

618 

619 Subclass of :class:`AlchemyFilter`. 

620 """ 

621 

622 default_verbs = ["is_true", "is_false"] 

623 

624 def get_verbs(self): # pylint: disable=empty-docstring 

625 """ """ 

626 

627 # get basic verbs from caller, or default list 

628 verbs = getattr(self, "verbs", self.default_verbs) 

629 if callable(verbs): 

630 verbs = verbs() 

631 verbs = list(verbs) 

632 

633 # add some more if column is nullable 

634 if self.nullable: 

635 for verb in ("is_false_null", "is_null", "is_not_null"): 

636 if verb not in verbs: 

637 verbs.append(verb) 

638 

639 # add wildcard 

640 if "is_any" not in verbs: 

641 verbs.append("is_any") 

642 

643 return verbs 

644 

645 def coerce_value(self, value): # pylint: disable=empty-docstring 

646 """ """ 

647 if value is not None: 

648 return bool(value) 

649 return None 

650 

651 def filter_is_true(self, query, value): # pylint: disable=unused-argument 

652 """ 

653 Filter data with an "is true" condition. The value is 

654 ignored. 

655 """ 

656 return query.filter( 

657 self.model_property == True # pylint: disable=singleton-comparison 

658 ) 

659 

660 def filter_is_false(self, query, value): # pylint: disable=unused-argument 

661 """ 

662 Filter data with an "is false" condition. The value is 

663 ignored. 

664 """ 

665 return query.filter( 

666 self.model_property == False # pylint: disable=singleton-comparison 

667 ) 

668 

669 def filter_is_false_null(self, query, value): # pylint: disable=unused-argument 

670 """ 

671 Filter data with "is false or null" condition. The value is 

672 ignored. 

673 """ 

674 return query.filter( 

675 sa.or_( 

676 self.model_property == False, # pylint: disable=singleton-comparison 

677 self.model_property == None, # pylint: disable=singleton-comparison 

678 ) 

679 ) 

680 

681 

682class DateAlchemyFilter(AlchemyFilter): 

683 """ 

684 SQLAlchemy filter option for a 

685 :class:`~sqlalchemy:sqlalchemy.types.Date` column. 

686 

687 Subclass of :class:`AlchemyFilter`. 

688 

689 This filter class has custom logic to coerce the input value, but 

690 does not have custom filter logic beyond that. 

691 """ 

692 

693 data_type = "date" 

694 default_verbs = [ 

695 "equal", 

696 "not_equal", 

697 "greater_than", 

698 "greater_equal", 

699 "less_than", 

700 "less_equal", 

701 # 'between', 

702 ] 

703 

704 default_verb_labels = { 

705 "equal": "on", 

706 "not_equal": "not on", 

707 "greater_than": "after", 

708 "greater_equal": "on or after", 

709 "less_than": "before", 

710 "less_equal": "on or before", 

711 # 'between': "between", 

712 "is_any": "is any", 

713 } 

714 

715 def coerce_value(self, value): 

716 """ 

717 Convert the given value to a proper 

718 :class:`python:datetime.date` object, if applicable. 

719 

720 If the input value is already a date object, it is returned 

721 as-is. 

722 

723 Otherwise it is assumed to be a string in ``%Y-%m-%d`` format, 

724 and will be converted to a date object. 

725 

726 If the conversion fails, or no value is provided, ``None`` is 

727 returned. 

728 """ 

729 if value: 

730 if isinstance(value, datetime.date): 

731 return value 

732 

733 try: 

734 dt = datetime.datetime.strptime(value, "%Y-%m-%d") 

735 except ValueError: 

736 log.warning("invalid date value: %s", value) 

737 else: 

738 return dt.date() 

739 

740 return None 

741 

742 

743class DateTimeAlchemyFilter(DateAlchemyFilter): 

744 """ 

745 SQLAlchemy filter option for a 

746 :class:`~sqlalchemy:sqlalchemy.types.DateTime` column. 

747 

748 Subclass of :class:`DateAlchemyFilter`. 

749 

750 This filter class has custom logic to coerce the input value, 

751 inherited from parent class. It also has custom filter logic 

752 for most verbs (not/equal, greater/less than etc.). 

753 

754 Please note that this class assumes the underlying data uses 

755 "naive UTC" values. It therefore will convert to/from local time 

756 zone accordingly, to ensure user gets the data they expect. For 

757 more info see :doc:`wuttjamaican:narr/datetime`. 

758 """ 

759 

760 def get_start_datetime(self, value, as_utc=True): 

761 """ 

762 Calculate the "start" timestamp for the given date value. 

763 

764 The return value will be the "first possible moment" of the 

765 given date. 

766 

767 :param value: :class:`python:datetime.date` instance 

768 

769 :param as_utc: Indicates the return value should be naive/UTC; 

770 set this to ``False`` to get the aware/local value. 

771 

772 :returns: :class:`python:datetime.datetime` instance 

773 """ 

774 start = datetime.datetime.combine(value, datetime.time(0)) 

775 start = self.app.localtime(start, from_utc=False) 

776 if as_utc: 

777 start = self.app.make_utc(start) 

778 return start 

779 

780 def get_end_datetime(self, value, as_utc=True): 

781 """ 

782 Calculate the "end" timestamp for the given date value. 

783 

784 Due to the nature of queries involving this "end" boundary, 

785 the return value will be the "first possible moment" of the 

786 day *after* the given date. 

787 

788 :param value: :class:`python:datetime.date` instance 

789 

790 :param as_utc: Indicates the return value should be naive/UTC; 

791 set this to ``False`` to get the aware/local value. 

792 

793 :returns: :class:`python:datetime.datetime` instance 

794 """ 

795 end = datetime.datetime.combine( 

796 value + datetime.timedelta(days=1), datetime.time(0) 

797 ) 

798 end = self.app.localtime(end, from_utc=False) 

799 if as_utc: 

800 end = self.app.make_utc(end) 

801 return end 

802 

803 def filter_equal(self, query, value): 

804 """ 

805 Find all records with datetime values which fall on the given 

806 date. 

807 """ 

808 value = self.coerce_value(value) 

809 if value is None: 

810 return query 

811 

812 start = self.get_start_datetime(value) 

813 end = self.get_end_datetime(value) 

814 return query.filter(self.model_property >= start).filter( 

815 self.model_property < end 

816 ) 

817 

818 def filter_not_equal(self, query, value): 

819 """ 

820 Find all records with datetime values which fall outside the 

821 given date. 

822 """ 

823 value = self.coerce_value(value) 

824 if value is None: 

825 return query 

826 

827 start = self.get_start_datetime(value) 

828 end = self.get_end_datetime(value) 

829 return query.filter( 

830 sa.or_(self.model_property < start, self.model_property >= end) 

831 ) 

832 

833 def filter_greater_than(self, query, value): 

834 """ 

835 Find all records with datetime values which fall after the 

836 given date. 

837 """ 

838 value = self.coerce_value(value) 

839 if value is None: 

840 return query 

841 

842 end = self.get_end_datetime(value) 

843 return query.filter(self.model_property >= end) 

844 

845 def filter_greater_equal(self, query, value): 

846 """ 

847 Find all records with datetime values which fall on or after 

848 the given date. 

849 """ 

850 value = self.coerce_value(value) 

851 if value is None: 

852 return query 

853 

854 start = self.get_start_datetime(value) 

855 return query.filter(self.model_property >= start) 

856 

857 def filter_less_than(self, query, value): 

858 """ 

859 Find all records with datetime values which fall before the 

860 given date. 

861 """ 

862 value = self.coerce_value(value) 

863 if value is None: 

864 return query 

865 

866 start = self.get_start_datetime(value) 

867 return query.filter(self.model_property < start) 

868 

869 def filter_less_equal(self, query, value): 

870 """ 

871 Find all records with datetime values which fall on or before 

872 the given date. 

873 """ 

874 value = self.coerce_value(value) 

875 if value is None: 

876 return query 

877 

878 end = self.get_end_datetime(value) 

879 return query.filter(self.model_property < end) 

880 

881 

882default_sqlalchemy_filters = { 

883 None: AlchemyFilter, 

884 sa.String: StringAlchemyFilter, 

885 sa.Text: StringAlchemyFilter, 

886 sa.Numeric: NumericAlchemyFilter, 

887 sa.Integer: IntegerAlchemyFilter, 

888 sa.Boolean: BooleanAlchemyFilter, 

889 sa.Date: DateAlchemyFilter, 

890 sa.DateTime: DateTimeAlchemyFilter, 

891}