Coverage for src/paperap/models/document/queryset.py: 97%

209 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-20 13:17 -0400

1""" 

2---------------------------------------------------------------------------- 

3 

4 METADATA: 

5 

6 File: queryset.py 

7 Project: paperap 

8 Created: 2025-03-04 

9 Version: 0.0.8 

10 Author: Jess Mann 

11 Email: jess@jmann.me 

12 Copyright (c) 2025 Jess Mann 

13 

14---------------------------------------------------------------------------- 

15 

16 LAST MODIFIED: 

17 

18 2025-03-04 By Jess Mann 

19 

20""" 

21 

22from __future__ import annotations 

23 

24import logging 

25from datetime import datetime 

26from functools import singledispatchmethod 

27from typing import TYPE_CHECKING, Any, NamedTuple, Optional, Self, Union, overload 

28 

29from paperap.models.abstract.queryset import BaseQuerySet, StandardQuerySet 

30from paperap.models.mixins.queryset import HasOwner 

31 

32if TYPE_CHECKING: 

33 from paperap.models.correspondent import Correspondent 

34 from paperap.models.document.model import Document, DocumentNote 

35 

36logger = logging.getLogger(__name__) 

37 

38_OperationType = Union[str, "_QueryParam"] 

39_QueryParam = Union["CustomFieldQuery", tuple[str, _OperationType, Any]] 

40 

41 

42class CustomFieldQuery(NamedTuple): 

43 field: str 

44 operation: _OperationType 

45 value: Any 

46 

47 

48class DocumentNoteQuerySet(StandardQuerySet["DocumentNote"]): 

49 pass 

50 

51 

52class DocumentQuerySet(StandardQuerySet["Document"], HasOwner): 

53 """ 

54 QuerySet for Paperless-ngx documents with specialized filtering methods. 

55 

56 Examples: 

57 >>> # Search for documents 

58 >>> docs = client.documents().search("invoice") 

59 >>> for doc in docs: 

60 ... print(doc.title) 

61 

62 >>> # Find documents similar to a specific document 

63 >>> similar_docs = client.documents().more_like(42) 

64 >>> for doc in similar_docs: 

65 ... print(doc.title) 

66 

67 """ 

68 

69 def tag_id(self, tag_id: int | list[int]) -> Self: 

70 """ 

71 Filter documents that have the specified tag ID(s). 

72 

73 Args: 

74 tag_id: A single tag ID or list of tag IDs 

75 

76 Returns: 

77 Filtered DocumentQuerySet 

78 

79 """ 

80 if isinstance(tag_id, list): 

81 return self.filter(tags__id__in=tag_id) 

82 return self.filter(tags__id=tag_id) 

83 

84 def tag_name(self, tag_name: str, *, exact: bool = True, case_insensitive: bool = True) -> Self: 

85 """ 

86 Filter documents that have a tag with the specified name. 

87 

88 Args: 

89 tag_name: The name of the tag 

90 exact: If True, match the exact tag name, otherwise use contains 

91 

92 Returns: 

93 Filtered DocumentQuerySet 

94 

95 """ 

96 return self.filter_field_by_str("tags__name", tag_name, exact=exact, case_insensitive=case_insensitive) 

97 

98 def title(self, title: str, *, exact: bool = True, case_insensitive: bool = True) -> Self: 

99 """ 

100 Filter documents by title. 

101 

102 Args: 

103 title: The document title to filter by 

104 exact: If True, match the exact title, otherwise use contains 

105 

106 Returns: 

107 Filtered DocumentQuerySet 

108 

109 """ 

110 return self.filter_field_by_str("title", title, exact=exact, case_insensitive=case_insensitive) 

111 

112 def search(self, query: str) -> "DocumentQuerySet": 

113 """ 

114 Search for documents using a query string. 

115 

116 Args: 

117 query: The search query. 

118 

119 Returns: 

120 A queryset with the search results. 

121 

122 Examples: 

123 >>> docs = client.documents().search("invoice") 

124 >>> for doc in docs: 

125 ... print(doc.title) 

126 

127 """ 

128 return self.filter(query=query) 

129 

130 def more_like(self, document_id: int) -> "DocumentQuerySet": 

131 """ 

132 Find documents similar to the specified document. 

133 

134 Args: 

135 document_id: The ID of the document to find similar documents for. 

136 

137 Returns: 

138 A queryset with similar documents. 

139 

140 Examples: 

141 >>> similar_docs = client.documents().more_like(42) 

142 >>> for doc in similar_docs: 

143 ... print(doc.title) 

144 

145 """ 

146 return self.filter(more_like_id=document_id) 

147 

148 def correspondent( 

149 self, value: int | str | None = None, *, exact: bool = True, case_insensitive: bool = True, **kwargs 

150 ) -> Self: 

151 """ 

152 Filter documents by correspondent. 

153 

154 Any number of filter arguments can be provided, but at least one must be specified. 

155 

156 Args: 

157 value: The correspondent ID or name to filter by 

158 exact: If True, match the exact value, otherwise use contains 

159 **kwargs: Additional filters (slug, id, name) 

160 

161 Returns: 

162 Filtered DocumentQuerySet 

163 

164 Raises: 

165 ValueError: If no valid filters are provided 

166 

167 Examples: 

168 # Filter by ID 

169 client.documents().all().correspondent(1) 

170 client.documents().all().correspondent(id=1) 

171 

172 # Filter by name 

173 client.documents().all().correspondent("John Doe") 

174 client.documents().all().correspondent(name="John Doe") 

175 

176 # Filter by name (exact match) 

177 client.documents().all().correspondent("John Doe", exact=True) 

178 client.documents().all().correspondent(name="John Doe", exact=True) 

179 

180 # Filter by slug 

181 client.documents().all().correspondent(slug="john-doe") 

182 

183 # Filter by ID and name 

184 client.documents().all().correspondent(1, name="John Doe") 

185 client.documents().all().correspondent(id=1, name="John Doe") 

186 client.documents().all().correspondent("John Doe", id=1) 

187 

188 """ 

189 # Track if any filters were applied 

190 filters_applied = False 

191 result = self 

192 

193 if value is not None: 

194 if isinstance(value, int): 

195 result = self.correspondent_id(value) 

196 filters_applied = True 

197 elif isinstance(value, str): 

198 result = self.correspondent_name(value, exact=exact, case_insensitive=case_insensitive) 

199 filters_applied = True 

200 else: 

201 raise TypeError("Invalid value type for correspondent filter") 

202 

203 if (slug := kwargs.get("slug")) is not None: 

204 result = result.correspondent_slug(slug, exact=exact, case_insensitive=case_insensitive) 

205 filters_applied = True 

206 if (pk := kwargs.get("id")) is not None: 

207 result = result.correspondent_id(pk) 

208 filters_applied = True 

209 if (name := kwargs.get("name")) is not None: 

210 result = result.correspondent_name(name, exact=exact, case_insensitive=case_insensitive) 

211 filters_applied = True 

212 

213 # If no filters have been applied, raise an error 

214 if not filters_applied: 

215 raise ValueError("No valid filters provided for correspondent") 

216 

217 return result 

218 

219 def correspondent_id(self, correspondent_id: int) -> Self: 

220 """ 

221 Filter documents by correspondent ID. 

222 

223 Args: 

224 correspondent_id: The correspondent ID to filter by 

225 

226 Returns: 

227 Filtered DocumentQuerySet 

228 

229 """ 

230 return self.filter(correspondent__id=correspondent_id) 

231 

232 def correspondent_name(self, name: str, *, exact: bool = True, case_insensitive: bool = True) -> Self: 

233 """ 

234 Filter documents by correspondent name. 

235 

236 Args: 

237 name: The correspondent name to filter by 

238 exact: If True, match the exact name, otherwise use contains 

239 

240 Returns: 

241 Filtered DocumentQuerySet 

242 

243 """ 

244 return self.filter_field_by_str("correspondent__name", name, exact=exact, case_insensitive=case_insensitive) 

245 

246 def correspondent_slug(self, slug: str, *, exact: bool = True, case_insensitive: bool = True) -> Self: 

247 """ 

248 Filter documents by correspondent slug. 

249 

250 Args: 

251 slug: The correspondent slug to filter by 

252 exact: If True, match the exact slug, otherwise use contains 

253 

254 Returns: 

255 Filtered DocumentQuerySet 

256 

257 """ 

258 return self.filter_field_by_str("correspondent__slug", slug, exact=exact, case_insensitive=case_insensitive) 

259 

260 def document_type( 

261 self, value: int | str | None = None, *, exact: bool = True, case_insensitive: bool = True, **kwargs 

262 ) -> Self: 

263 """ 

264 Filter documents by document type. 

265 

266 Any number of filter arguments can be provided, but at least one must be specified. 

267 

268 Args: 

269 value: The document type ID or name to filter by 

270 exact: If True, match the exact value, otherwise use contains 

271 **kwargs: Additional filters (id, name) 

272 

273 Returns: 

274 Filtered DocumentQuerySet 

275 

276 Raises: 

277 ValueError: If no valid filters are provided 

278 

279 Examples: 

280 # Filter by ID 

281 client.documents().all().document_type(1) 

282 client.documents().all().document_type(id=1) 

283 

284 # Filter by name 

285 client.documents().all().document_type("Invoice") 

286 client.documents().all().document_type(name="Invoice") 

287 

288 # Filter by name (exact match) 

289 client.documents().all().document_type("Invoice", exact=True) 

290 client.documents().all().document_type(name="Invoice", exact=True) 

291 

292 # Filter by ID and name 

293 client.documents().all().document_type(1, name="Invoice") 

294 client.documents().all().document_type(id=1, name="Invoice") 

295 client.documents().all().document_type("Invoice", id=1) 

296 

297 """ 

298 # Track if any filters were applied 

299 filters_applied = False 

300 result = self 

301 

302 if value is not None: 

303 if isinstance(value, int): 

304 result = self.document_type_id(value) 

305 filters_applied = True 

306 elif isinstance(value, str): 

307 result = self.document_type_name(value, exact=exact, case_insensitive=case_insensitive) 

308 filters_applied = True 

309 else: 

310 raise TypeError("Invalid value type for document type filter") 

311 

312 if (pk := kwargs.get("id")) is not None: 

313 result = result.document_type_id(pk) 

314 filters_applied = True 

315 if (name := kwargs.get("name")) is not None: 

316 result = result.document_type_name(name, exact=exact, case_insensitive=case_insensitive) 

317 filters_applied = True 

318 

319 # If no filters have been applied, raise an error 

320 if not filters_applied: 

321 raise ValueError("No valid filters provided for document type") 

322 

323 return result 

324 

325 def document_type_id(self, document_type_id: int) -> Self: 

326 """ 

327 Filter documents by document type ID. 

328 

329 Args: 

330 document_type_id: The document type ID to filter by 

331 

332 Returns: 

333 Filtered DocumentQuerySet 

334 

335 """ 

336 return self.filter(document_type__id=document_type_id) 

337 

338 def document_type_name(self, name: str, *, exact: bool = True, case_insensitive: bool = True) -> Self: 

339 """ 

340 Filter documents by document type name. 

341 

342 Args: 

343 name: The document type name to filter by 

344 exact: If True, match the exact name, otherwise use contains 

345 

346 Returns: 

347 Filtered DocumentQuerySet 

348 

349 """ 

350 return self.filter_field_by_str("document_type__name", name, exact=exact, case_insensitive=case_insensitive) 

351 

352 def storage_path( 

353 self, value: int | str | None = None, *, exact: bool = True, case_insensitive: bool = True, **kwargs 

354 ) -> Self: 

355 """ 

356 Filter documents by storage path. 

357 

358 Any number of filter arguments can be provided, but at least one must be specified. 

359 

360 Args: 

361 value: The storage path ID or name to filter by 

362 exact: If True, match the exact value, otherwise use contains 

363 **kwargs: Additional filters (id, name) 

364 

365 Returns: 

366 Filtered DocumentQuerySet 

367 

368 Raises: 

369 ValueError: If no valid filters are provided 

370 

371 Examples: 

372 # Filter by ID 

373 client.documents().all().storage_path(1) 

374 client.documents().all().storage_path(id=1) 

375 

376 # Filter by name 

377 client.documents().all().storage_path("Invoices") 

378 client.documents().all().storage_path(name="Invoices") 

379 

380 # Filter by name (exact match) 

381 client.documents().all().storage_path("Invoices", exact=True) 

382 client.documents().all().storage_path(name="Invoices", exact=True) 

383 

384 # Filter by ID and name 

385 client.documents().all().storage_path(1, name="Invoices") 

386 client.documents().all().storage_path(id=1, name="Invoices") 

387 client.documents().all().storage_path("Invoices", id=1) 

388 

389 """ 

390 # Track if any filters were applied 

391 filters_applied = False 

392 result = self 

393 

394 if value is not None: 

395 if isinstance(value, int): 

396 result = self.storage_path_id(value) 

397 filters_applied = True 

398 elif isinstance(value, str): 

399 result = self.storage_path_name(value, exact=exact, case_insensitive=case_insensitive) 

400 filters_applied = True 

401 else: 

402 raise TypeError("Invalid value type for storage path filter") 

403 

404 if (pk := kwargs.get("id")) is not None: 

405 result = result.storage_path_id(pk) 

406 filters_applied = True 

407 if (name := kwargs.get("name")) is not None: 

408 result = result.storage_path_name(name, exact=exact, case_insensitive=case_insensitive) 

409 filters_applied = True 

410 

411 # If no filters have been applied, raise an error 

412 if not filters_applied: 

413 raise ValueError("No valid filters provided for storage path") 

414 

415 return result 

416 

417 def storage_path_id(self, storage_path_id: int) -> Self: 

418 """ 

419 Filter documents by storage path ID. 

420 

421 Args: 

422 storage_path_id: The storage path ID to filter by 

423 

424 Returns: 

425 Filtered DocumentQuerySet 

426 

427 """ 

428 return self.filter(storage_path__id=storage_path_id) 

429 

430 def storage_path_name(self, name: str, *, exact: bool = True, case_insensitive: bool = True) -> Self: 

431 """ 

432 Filter documents by storage path name. 

433 

434 Args: 

435 name: The storage path name to filter by 

436 exact: If True, match the exact name, otherwise use contains 

437 

438 Returns: 

439 Filtered DocumentQuerySet 

440 

441 """ 

442 return self.filter_field_by_str("storage_path__name", name, exact=exact, case_insensitive=case_insensitive) 

443 

444 def content(self, text: str) -> Self: 

445 """ 

446 Filter documents whose content contains the specified text. 

447 

448 Args: 

449 text: The text to search for in document content 

450 

451 Returns: 

452 Filtered DocumentQuerySet 

453 

454 """ 

455 return self.filter(content__contains=text) 

456 

457 def added_after(self, date_str: str) -> Self: 

458 """ 

459 Filter documents added after the specified date. 

460 

461 Args: 

462 date_str: ISO format date string (YYYY-MM-DD) 

463 

464 Returns: 

465 Filtered DocumentQuerySet 

466 

467 """ 

468 return self.filter(added__gt=date_str) 

469 

470 def added_before(self, date_str: str) -> Self: 

471 """ 

472 Filter documents added before the specified date. 

473 

474 Args: 

475 date_str: ISO format date string (YYYY-MM-DD) 

476 

477 Returns: 

478 Filtered DocumentQuerySet 

479 

480 """ 

481 return self.filter(added__lt=date_str) 

482 

483 def asn(self, value: str, *, exact: bool = True, case_insensitive: bool = True) -> Self: 

484 """ 

485 Filter documents by archive serial number. 

486 

487 Args: 

488 value: The archive serial number to filter by 

489 exact: If True, match the exact value, otherwise use contains 

490 

491 Returns: 

492 Filtered DocumentQuerySet 

493 

494 """ 

495 return self.filter_field_by_str("asn", value, exact=exact, case_insensitive=case_insensitive) 

496 

497 def original_file_name(self, name: str, *, exact: bool = True, case_insensitive: bool = True) -> Self: 

498 """ 

499 Filter documents by original file name. 

500 

501 Args: 

502 name: The original file name to filter by 

503 exact: If True, match the exact name, otherwise use contains 

504 

505 Returns: 

506 Filtered DocumentQuerySet 

507 

508 """ 

509 return self.filter_field_by_str("original_file_name", name, exact=exact, case_insensitive=case_insensitive) 

510 

511 def user_can_change(self, value: bool) -> Self: 

512 """ 

513 Filter documents by user change permission. 

514 

515 Args: 

516 value: True to filter documents the user can change 

517 

518 Returns: 

519 Filtered DocumentQuerySet 

520 

521 """ 

522 return self.filter(user_can_change=value) 

523 

524 def custom_field_fullsearch(self, value: str, *, case_insensitive: bool = True) -> Self: 

525 """ 

526 Filter documents by searching through both custom field name and value. 

527 

528 Args: 

529 value: The search string 

530 

531 Returns: 

532 Filtered DocumentQuerySet 

533 

534 """ 

535 if case_insensitive: 

536 return self.filter(custom_fields__icontains=value) 

537 raise NotImplementedError("Case-sensitive custom field search is not supported by Paperless NGX") 

538 

539 def custom_field(self, field: str, value: Any, *, exact: bool = False, case_insensitive: bool = True) -> Self: 

540 """ 

541 Filter documents by custom field. 

542 

543 Args: 

544 field: The name of the custom field 

545 value: The value to filter by 

546 exact: If True, match the exact value, otherwise use contains 

547 

548 Returns: 

549 Filtered DocumentQuerySet 

550 

551 """ 

552 if exact: 

553 if case_insensitive: 

554 return self.custom_field_query(field, "iexact", value) 

555 return self.custom_field_query(field, "exact", value) 

556 if case_insensitive: 

557 return self.custom_field_query(field, "icontains", value) 

558 return self.custom_field_query(field, "contains", value) 

559 

560 def has_custom_field_id(self, pk: int | list[int], *, exact: bool = False) -> Self: 

561 """ 

562 Filter documents that have a custom field with the specified ID(s). 

563 

564 Args: 

565 pk: A single custom field ID or list of custom field IDs 

566 exact: If True, return results that have exactly these ids and no others 

567 

568 Returns: 

569 Filtered DocumentQuerySet 

570 

571 """ 

572 if exact: 

573 return self.filter(custom_fields__id__all=pk) 

574 return self.filter(custom_fields__id__in=pk) 

575 

576 def _normalize_custom_field_query_item(self, value: Any) -> str: 

577 if isinstance(value, tuple): 

578 # Check if it's a CustomFieldQuery 

579 try: 

580 converted_value = CustomFieldQuery(*value) 

581 return self._normalize_custom_field_query(converted_value) 

582 except TypeError: 

583 # It's a tuple, not a CustomFieldQuery 

584 pass 

585 

586 if isinstance(value, str): 

587 return f'"{value}"' 

588 if isinstance(value, (list, tuple)): 

589 values = [str(self._normalize_custom_field_query_item(v)) for v in value] 

590 return f"[{', '.join(values)}]" 

591 if isinstance(value, bool): 

592 return str(value).lower() 

593 

594 return str(value) 

595 

596 def _normalize_custom_field_query(self, query: _QueryParam) -> str: 

597 try: 

598 if not isinstance(query, CustomFieldQuery): 

599 query = CustomFieldQuery(*query) 

600 except TypeError as te: 

601 raise TypeError("Invalid custom field query format") from te 

602 

603 field, operation, value = query 

604 operation = self._normalize_custom_field_query_item(operation) 

605 value = self._normalize_custom_field_query_item(value) 

606 return f'["{field}", {operation}, {value}]' 

607 

608 @overload 

609 def custom_field_query(self, query: _QueryParam) -> Self: 

610 """ 

611 Filter documents by custom field query. 

612 

613 Args: 

614 query: A list representing a custom field query 

615 

616 Returns: 

617 Filtered DocumentQuerySet 

618 

619 """ 

620 ... 

621 

622 @overload 

623 def custom_field_query(self, field: str, operation: _OperationType, value: Any) -> Self: 

624 """ 

625 Filter documents by custom field query. 

626 

627 Args: 

628 field: The name of the custom field 

629 operation: The operation to perform 

630 value: The value to filter by 

631 

632 Returns: 

633 Filtered DocumentQuerySet 

634 

635 """ 

636 ... 

637 

638 @singledispatchmethod # type: ignore # mypy does not handle singledispatchmethod with overloads correctly 

639 def custom_field_query(self, *args, **kwargs: Any) -> Self: 

640 """ 

641 Filter documents by custom field query. 

642 """ 

643 raise TypeError("Invalid custom field query format") 

644 

645 @custom_field_query.register # type: ignore # mypy does not handle singledispatchmethod with overloads correctly 

646 def _(self, query: CustomFieldQuery) -> Self: 

647 query_str = self._normalize_custom_field_query(query) 

648 return self.filter(custom_field_query=query_str) 

649 

650 @custom_field_query.register # type: ignore # mypy does not handle singledispatchmethod with overloads correctly 

651 def _(self, field: str, operation: str | CustomFieldQuery | tuple[str, Any, Any], value: Any) -> Self: 

652 query = CustomFieldQuery(field, operation, value) 

653 query_str = self._normalize_custom_field_query(query) 

654 return self.filter(custom_field_query=query_str) 

655 

656 def custom_field_range(self, field: str, start: str, end: str) -> Self: 

657 """ 

658 Filter documents with a custom field value within a specified range. 

659 

660 Args: 

661 field: The name of the custom field 

662 start: The start value of the range 

663 end: The end value of the range 

664 

665 Returns: 

666 Filtered DocumentQuerySet 

667 

668 """ 

669 return self.custom_field_query(field, "range", [start, end]) 

670 

671 def custom_field_exact(self, field: str, value: Any) -> Self: 

672 """ 

673 Filter documents with a custom field value that matches exactly. 

674 

675 Args: 

676 field: The name of the custom field 

677 value: The exact value to match 

678 

679 Returns: 

680 Filtered DocumentQuerySet 

681 

682 """ 

683 return self.custom_field_query(field, "exact", value) 

684 

685 def custom_field_in(self, field: str, values: list[Any]) -> Self: 

686 """ 

687 Filter documents with a custom field value in a list of values. 

688 

689 Args: 

690 field: The name of the custom field 

691 values: The list of values to match 

692 

693 Returns: 

694 Filtered DocumentQuerySet 

695 

696 """ 

697 return self.custom_field_query(field, "in", values) 

698 

699 def custom_field_isnull(self, field: str) -> Self: 

700 """ 

701 Filter documents with a custom field that is null or empty. 

702 

703 Args: 

704 field: The name of the custom field 

705 

706 Returns: 

707 Filtered DocumentQuerySet 

708 

709 """ 

710 return self.custom_field_query("OR", (field, "isnull", True), [field, "exact", ""]) 

711 

712 def custom_field_exists(self, field: str, exists: bool = True) -> Self: 

713 """ 

714 Filter documents based on the existence of a custom field. 

715 

716 Args: 

717 field: The name of the custom field 

718 exists: True to filter documents where the field exists, False otherwise 

719 

720 Returns: 

721 Filtered DocumentQuerySet 

722 

723 """ 

724 return self.custom_field_query(field, "exists", exists) 

725 

726 def custom_field_contains(self, field: str, values: list[Any]) -> Self: 

727 """ 

728 Filter documents with a custom field that contains all specified values. 

729 

730 Args: 

731 field: The name of the custom field 

732 values: The list of values that the field should contain 

733 

734 Returns: 

735 Filtered DocumentQuerySet 

736 

737 """ 

738 return self.custom_field_query(field, "contains", values) 

739 

740 def has_custom_fields(self) -> Self: 

741 """ 

742 Filter documents that have custom fields. 

743 """ 

744 return self.filter(has_custom_fields=True) 

745 

746 def no_custom_fields(self) -> Self: 

747 """ 

748 Filter documents that do not have custom fields. 

749 """ 

750 return self.filter(has_custom_fields=False) 

751 

752 def notes(self, text: str) -> Self: 

753 """ 

754 Filter documents whose notes contain the specified text. 

755 

756 Args: 

757 text: The text to search for in document notes 

758 

759 Returns: 

760 Filtered DocumentQuerySet 

761 

762 """ 

763 return self.filter(notes__contains=text) 

764 

765 def created_before(self, date: datetime | str) -> Self: 

766 """ 

767 Filter models created before a given date. 

768 

769 Args: 

770 date: The date to filter by 

771 

772 Returns: 

773 Filtered QuerySet 

774 

775 """ 

776 if isinstance(date, datetime): 

777 return self.filter(created__lt=date.strftime("%Y-%m-%d")) 

778 return self.filter(created__lt=date) 

779 

780 def created_after(self, date: datetime | str) -> Self: 

781 """ 

782 Filter models created after a given date. 

783 

784 Args: 

785 date: The date to filter by 

786 

787 Returns: 

788 Filtered QuerySet 

789 

790 """ 

791 if isinstance(date, datetime): 

792 return self.filter(created__gt=date.strftime("%Y-%m-%d")) 

793 return self.filter(created__gt=date) 

794 

795 def created_between(self, start: datetime | str, end: datetime | str) -> Self: 

796 """ 

797 Filter models created between two dates. 

798 

799 Args: 

800 start: The start date to filter by 

801 end: The end date to filter by 

802 

803 Returns: 

804 Filtered QuerySet 

805 

806 """ 

807 if isinstance(start, datetime): 

808 start = start.strftime("%Y-%m-%d") 

809 if isinstance(end, datetime): 

810 end = end.strftime("%Y-%m-%d") 

811 

812 return self.filter(created__range=(start, end))