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

282 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-24 16:36 -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, get_class_hierarchy 

40 

41log = logging.getLogger(__name__) 

42 

43 

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

45 """ """ 

46 

47 def __init__(self, verb): 

48 self.verb = verb 

49 

50 def __str__(self): 

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

52 

53 

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

55 """ 

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

57 as "state" for the filter. 

58 

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

60 

61 :param nullable: Boolean indicating whether the filter should 

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

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

64 its nullable flag. 

65 

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

67 on the filter instance. 

68 

69 Filter instances have the following attributes: 

70 

71 .. attribute:: key 

72 

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

74 name" for the grid, but not always. 

75 

76 .. attribute:: label 

77 

78 Display label for the filter field. 

79 

80 .. attribute:: data_type 

81 

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

83 will be one of: 

84 

85 * ``'string'`` 

86 * ``'date'`` 

87 * ``'choice'`` 

88 

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

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

91 need a value input; the verb is enough. 

92 

93 .. attribute:: active 

94 

95 Boolean indicating whether the filter is currently active. 

96 

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

98 

99 .. attribute:: verb 

100 

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

102 

103 See also :attr:`value`. 

104 

105 .. attribute:: choices 

106 

107 OrderedDict of possible values for the filter. 

108 

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

110 update it. 

111 

112 .. attribute:: value 

113 

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

115 

116 See also :attr:`verb`. 

117 

118 .. attribute:: default_active 

119 

120 Boolean indicating whether the filter should be active by 

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

122 

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

124 

125 .. attribute:: default_verb 

126 

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

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

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

130 

131 See also :attr:`default_value`. 

132 

133 .. attribute:: default_value 

134 

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

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

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

138 

139 See also :attr:`default_verb`. 

140 

141 .. attribute:: default_verb_labels 

142 

143 Dict of human-friendly verb labels, keyed by internal verb 

144 names. Subclass can define additional verb labels, and/or 

145 override those defined on the base class. These labels are 

146 "cumulative" so a subclass need only define the ones needing 

147 customization. 

148 

149 See also :meth:`get_verb_labels()`. 

150 """ 

151 

152 data_type = "string" 

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

154 

155 default_verb_labels = { 

156 "is_any": "is any", 

157 "equal": "equal to", 

158 "not_equal": "not equal to", 

159 "greater_than": "greater than", 

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

161 "less_than": "less than", 

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

163 # 'between': "between", 

164 "is_true": "is true", 

165 "is_false": "is false", 

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

167 "is_null": "is null", 

168 "is_not_null": "is not null", 

169 "contains": "contains", 

170 "does_not_contain": "does not contain", 

171 } 

172 

173 valueless_verbs = [ 

174 "is_any", 

175 "is_true", 

176 "is_false", 

177 "is_false_null", 

178 "is_null", 

179 "is_not_null", 

180 ] 

181 

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

183 self, 

184 request, 

185 key, 

186 label=None, 

187 verbs=None, 

188 choices=None, 

189 nullable=None, 

190 default_active=False, 

191 default_verb=None, 

192 default_value=None, 

193 **kwargs, 

194 ): 

195 self.request = request 

196 self.key = key 

197 self.config = self.request.wutta_config 

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

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

200 

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

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

203 self.original_data_type = self.data_type 

204 

205 # active 

206 self.default_active = default_active 

207 self.active = self.default_active 

208 

209 # verb 

210 if verbs is not None: 

211 self.verbs = verbs 

212 if default_verb: 

213 self.default_verb = default_verb 

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

215 

216 # choices 

217 self.set_choices(choices) 

218 

219 # nullable 

220 self.nullable = nullable 

221 

222 # value 

223 self.default_value = default_value 

224 self.value = self.default_value 

225 

226 self.__dict__.update(kwargs) 

227 

228 def __repr__(self): 

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

230 return ( 

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

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

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

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

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

236 ) 

237 

238 def get_verbs(self): 

239 """ 

240 Returns the list of verbs supported by the filter. 

241 """ 

242 verbs = None 

243 

244 if hasattr(self, "verbs"): 

245 verbs = self.verbs 

246 

247 else: 

248 verbs = self.default_verbs 

249 

250 if callable(verbs): 

251 verbs = verbs() 

252 verbs = list(verbs) 

253 

254 if self.nullable: 

255 if "is_null" not in verbs: 

256 verbs.append("is_null") 

257 if "is_not_null" not in verbs: 

258 verbs.append("is_not_null") 

259 

260 if "is_any" not in verbs: 

261 verbs.append("is_any") 

262 

263 return verbs 

264 

265 def get_verb_labels(self): 

266 """ 

267 Retrieve a dict of all defined verb labels for the filter. 

268 

269 This will be something along the lines of:: 

270 

271 { 

272 "equal": "equal to", 

273 "not_equal": "not equal to", 

274 "is_null": "is null", 

275 "is_not_null": "is not null", 

276 } 

277 

278 Note that this will crawl the class hierarchy, collecting all 

279 labels defined (via :attr:`default_verb_labels`). Subclass 

280 labels take precedence over base class labels. 

281 

282 :returns: Dict of verb labels, keyed by internal verb name. 

283 """ 

284 labels = {} 

285 for cls in get_class_hierarchy(self.__class__): 

286 labels.update(cls.default_verb_labels) 

287 for verb in self.get_verbs(): 

288 labels.setdefault(verb, verb) 

289 return labels 

290 

291 def get_valueless_verbs(self): 

292 """ 

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

294 """ 

295 return self.valueless_verbs 

296 

297 def get_default_verb(self): 

298 """ 

299 Returns the default verb for the filter. 

300 """ 

301 verb = None 

302 

303 if hasattr(self, "default_verb"): 

304 verb = self.default_verb 

305 

306 elif hasattr(self, "verb"): 

307 verb = self.verb 

308 

309 if not verb: 

310 verbs = self.get_verbs() 

311 if verbs: 

312 verb = verbs[0] 

313 

314 return verb 

315 

316 def set_choices(self, choices): 

317 """ 

318 Set the value choices for the filter. 

319 

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

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

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

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

324 dropdown. 

325 

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

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

328 ``'string'``. 

329 

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

331 """ 

332 if choices: 

333 self.choices = self.normalize_choices(choices) 

334 self.data_type = "choice" 

335 else: 

336 self.choices = {} 

337 if self.data_type == "choice": 

338 self.data_type = self.original_data_type 

339 

340 def normalize_choices(self, choices): 

341 """ 

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

343 

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

345 

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

347 formats: 

348 

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

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

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

352 value (ordering of choices will be preserved) 

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

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

355 key!) 

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

357 choices (ordering of choices will be preserved) 

358 

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

360 """ 

361 normalized = choices 

362 

363 if isinstance(choices, EnumType): 

364 normalized = OrderedDict( 

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

366 ) 

367 

368 elif isinstance(choices, OrderedDict): 

369 normalized = choices 

370 

371 elif isinstance(choices, dict): 

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

373 

374 elif isinstance(choices, list): 

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

376 

377 return normalized 

378 

379 def coerce_value(self, value): 

380 """ 

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

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

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

384 

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

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

387 

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

389 may override as needed. 

390 

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

392 form submission. 

393 

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

395 filter subclass. 

396 """ 

397 return value 

398 

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

400 """ 

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

402 

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

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

405 

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

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

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

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

410 unchanged. 

411 

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

413 """ 

414 if verb is None: 

415 verb = self.verb 

416 if not verb: 

417 verb = self.get_default_verb() 

418 log.warning( 

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

420 self.key, 

421 verb, 

422 ) 

423 

424 # only attempt for known verbs 

425 if verb not in self.get_verbs(): 

426 raise VerbNotSupported(verb) 

427 

428 # fallback value 

429 if value is UNSPECIFIED: 

430 value = self.value 

431 

432 # locate filter method 

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

434 if not func: 

435 raise VerbNotSupported(verb) 

436 

437 # invoke filter method 

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

439 

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

441 """ 

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

443 data as-is. 

444 """ 

445 return data 

446 

447 

448class AlchemyFilter(GridFilter): 

449 """ 

450 Filter option for a grid with SQLAlchemy query data. 

451 

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

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

454 

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

456 column by which to filter. For instance, 

457 ``model.Person.full_name``. 

458 """ 

459 

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

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

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

463 

464 if self.nullable is None: 

465 columns = self.model_property.prop.columns 

466 if len(columns) == 1: 

467 self.nullable = columns[0].nullable 

468 

469 def filter_equal(self, query, value): 

470 """ 

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

472 """ 

473 value = self.coerce_value(value) 

474 if value is None: 

475 return query 

476 

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

478 

479 def filter_not_equal(self, query, value): 

480 """ 

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

482 """ 

483 value = self.coerce_value(value) 

484 if value is None: 

485 return query 

486 

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

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

489 return query.filter( 

490 sa.or_( 

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

492 self.model_property != value, 

493 ) 

494 ) 

495 

496 def filter_greater_than(self, query, value): 

497 """ 

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

499 """ 

500 value = self.coerce_value(value) 

501 if value is None: 

502 return query 

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

504 

505 def filter_greater_equal(self, query, value): 

506 """ 

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

508 """ 

509 value = self.coerce_value(value) 

510 if value is None: 

511 return query 

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

513 

514 def filter_less_than(self, query, value): 

515 """ 

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

517 """ 

518 value = self.coerce_value(value) 

519 if value is None: 

520 return query 

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

522 

523 def filter_less_equal(self, query, value): 

524 """ 

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

526 """ 

527 value = self.coerce_value(value) 

528 if value is None: 

529 return query 

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

531 

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

533 """ 

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

535 """ 

536 return query.filter( 

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

538 ) 

539 

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

541 """ 

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

543 ignored. 

544 """ 

545 return query.filter( 

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

547 ) 

548 

549 

550class StringAlchemyFilter(AlchemyFilter): 

551 """ 

552 SQLAlchemy filter option for a text data column. 

553 

554 Subclass of :class:`AlchemyFilter`. 

555 """ 

556 

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

558 

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

560 """ """ 

561 if value is not None: 

562 value = str(value) 

563 if value: 

564 return value 

565 return None 

566 

567 def filter_contains(self, query, value): 

568 """ 

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

570 """ 

571 value = self.coerce_value(value) 

572 if not value: 

573 return query 

574 

575 criteria = [] 

576 for val in value.split(): 

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

578 val = f"%{val}%" 

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

580 

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

582 

583 def filter_does_not_contain(self, query, value): 

584 """ 

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

586 """ 

587 value = self.coerce_value(value) 

588 if not value: 

589 return query 

590 

591 criteria = [] 

592 for val in value.split(): 

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

594 val = f"%{val}%" 

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

596 

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

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

599 return query.filter( 

600 sa.or_( 

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

602 sa.and_(*criteria), 

603 ) 

604 ) 

605 

606 

607class NumericAlchemyFilter(AlchemyFilter): 

608 """ 

609 SQLAlchemy filter option for a numeric data column. 

610 

611 Subclass of :class:`AlchemyFilter`. 

612 """ 

613 

614 default_verbs = [ 

615 "equal", 

616 "not_equal", 

617 "greater_than", 

618 "greater_equal", 

619 "less_than", 

620 "less_equal", 

621 ] 

622 

623 

624class IntegerAlchemyFilter(NumericAlchemyFilter): 

625 """ 

626 SQLAlchemy filter option for an integer data column. 

627 

628 Subclass of :class:`NumericAlchemyFilter`. 

629 """ 

630 

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

632 """ """ 

633 if value: 

634 try: 

635 return int(value) 

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

637 pass 

638 return None 

639 

640 

641class BooleanAlchemyFilter(AlchemyFilter): 

642 """ 

643 SQLAlchemy filter option for a boolean data column. 

644 

645 Subclass of :class:`AlchemyFilter`. 

646 """ 

647 

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

649 

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

651 """ """ 

652 

653 # get basic verbs from caller, or default list 

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

655 if callable(verbs): 

656 verbs = verbs() 

657 verbs = list(verbs) 

658 

659 # add some more if column is nullable 

660 if self.nullable: 

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

662 if verb not in verbs: 

663 verbs.append(verb) 

664 

665 # add wildcard 

666 if "is_any" not in verbs: 

667 verbs.append("is_any") 

668 

669 return verbs 

670 

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

672 """ """ 

673 if value is not None: 

674 return bool(value) 

675 return None 

676 

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

678 """ 

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

680 ignored. 

681 """ 

682 return query.filter( 

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

684 ) 

685 

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

687 """ 

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

689 ignored. 

690 """ 

691 return query.filter( 

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

693 ) 

694 

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

696 """ 

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

698 ignored. 

699 """ 

700 return query.filter( 

701 sa.or_( 

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

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

704 ) 

705 ) 

706 

707 

708class DateAlchemyFilter(AlchemyFilter): 

709 """ 

710 SQLAlchemy filter option for a 

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

712 

713 Subclass of :class:`AlchemyFilter`. 

714 

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

716 does not have custom filter logic beyond that. 

717 """ 

718 

719 data_type = "date" 

720 default_verbs = [ 

721 "equal", 

722 "not_equal", 

723 "greater_than", 

724 "greater_equal", 

725 "less_than", 

726 "less_equal", 

727 # 'between', 

728 ] 

729 

730 default_verb_labels = { 

731 "equal": "on", 

732 "not_equal": "not on", 

733 "greater_than": "after", 

734 "greater_equal": "on or after", 

735 "less_than": "before", 

736 "less_equal": "on or before", 

737 # 'between': "between", 

738 } 

739 

740 def coerce_value(self, value): 

741 """ 

742 Convert the given value to a proper 

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

744 

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

746 as-is. 

747 

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

749 and will be converted to a date object. 

750 

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

752 returned. 

753 """ 

754 if value: 

755 if isinstance(value, datetime.date): 

756 return value 

757 

758 try: 

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

760 except ValueError: 

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

762 else: 

763 return dt.date() 

764 

765 return None 

766 

767 

768class DateTimeAlchemyFilter(DateAlchemyFilter): 

769 """ 

770 SQLAlchemy filter option for a 

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

772 

773 Subclass of :class:`DateAlchemyFilter`. 

774 

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

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

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

778 

779 Please note that this class assumes the underlying data uses 

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

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

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

783 """ 

784 

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

786 """ 

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

788 

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

790 given date. 

791 

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

793 

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

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

796 

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

798 """ 

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

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

801 if as_utc: 

802 start = self.app.make_utc(start) 

803 return start 

804 

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

806 """ 

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

808 

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

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

811 day *after* the given date. 

812 

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

814 

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

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

817 

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

819 """ 

820 end = datetime.datetime.combine( 

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

822 ) 

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

824 if as_utc: 

825 end = self.app.make_utc(end) 

826 return end 

827 

828 def filter_equal(self, query, value): 

829 """ 

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

831 date. 

832 """ 

833 value = self.coerce_value(value) 

834 if value is None: 

835 return query 

836 

837 start = self.get_start_datetime(value) 

838 end = self.get_end_datetime(value) 

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

840 self.model_property < end 

841 ) 

842 

843 def filter_not_equal(self, query, value): 

844 """ 

845 Find all records with datetime values which fall outside the 

846 given date. 

847 """ 

848 value = self.coerce_value(value) 

849 if value is None: 

850 return query 

851 

852 start = self.get_start_datetime(value) 

853 end = self.get_end_datetime(value) 

854 return query.filter( 

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

856 ) 

857 

858 def filter_greater_than(self, query, value): 

859 """ 

860 Find all records with datetime values which fall after the 

861 given date. 

862 """ 

863 value = self.coerce_value(value) 

864 if value is None: 

865 return query 

866 

867 end = self.get_end_datetime(value) 

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

869 

870 def filter_greater_equal(self, query, value): 

871 """ 

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

873 the given date. 

874 """ 

875 value = self.coerce_value(value) 

876 if value is None: 

877 return query 

878 

879 start = self.get_start_datetime(value) 

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

881 

882 def filter_less_than(self, query, value): 

883 """ 

884 Find all records with datetime values which fall before the 

885 given date. 

886 """ 

887 value = self.coerce_value(value) 

888 if value is None: 

889 return query 

890 

891 start = self.get_start_datetime(value) 

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

893 

894 def filter_less_equal(self, query, value): 

895 """ 

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

897 the given date. 

898 """ 

899 value = self.coerce_value(value) 

900 if value is None: 

901 return query 

902 

903 end = self.get_end_datetime(value) 

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

905 

906 

907default_sqlalchemy_filters = { 

908 None: AlchemyFilter, 

909 sa.String: StringAlchemyFilter, 

910 sa.Text: StringAlchemyFilter, 

911 sa.Numeric: NumericAlchemyFilter, 

912 sa.Integer: IntegerAlchemyFilter, 

913 sa.Boolean: BooleanAlchemyFilter, 

914 sa.Date: DateAlchemyFilter, 

915 sa.DateTime: DateTimeAlchemyFilter, 

916}