diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index 14651d2..d446612 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:paperless_api/src/constants.dart'; import 'package:paperless_api/src/models/query_parameters/asn_query.dart'; import 'package:paperless_api/src/models/query_parameters/correspondent_query.dart'; +import 'package:paperless_api/src/models/query_parameters/date_range_query.dart'; import 'package:paperless_api/src/models/query_parameters/document_type_query.dart'; import 'package:paperless_api/src/models/query_parameters/query_type.dart'; import 'package:paperless_api/src/models/query_parameters/sort_field.dart'; @@ -29,16 +30,12 @@ class DocumentFilter extends Equatable { final TagsQuery tags; final SortField sortField; final SortOrder sortOrder; - final DateTime? addedDateAfter; - final DateTime? addedDateBefore; - final DateTime? createdDateAfter; - final DateTime? createdDateBefore; + final DateRangeQuery added; + final DateRangeQuery created; final QueryType queryType; final String? queryText; const DocumentFilter({ - this.createdDateAfter, - this.createdDateBefore, this.documentType = const DocumentTypeQuery.unset(), this.correspondent = const CorrespondentQuery.unset(), this.storagePath = const StoragePathQuery.unset(), @@ -48,53 +45,39 @@ class DocumentFilter extends Equatable { this.sortOrder = SortOrder.descending, this.page = 1, this.pageSize = 25, - this.addedDateAfter, - this.addedDateBefore, this.queryType = QueryType.titleAndContent, this.queryText, + this.added = const UnsetDateRangeQuery(), + this.created = const UnsetDateRangeQuery(), }); - String toQueryString() { - final StringBuffer sb = StringBuffer("page=$page&page_size=$pageSize"); - sb.write(documentType.toQueryParameter()); - sb.write(correspondent.toQueryParameter()); - sb.write(tags.toQueryParameter()); - sb.write(storagePath.toQueryParameter()); - sb.write(asnQuery.toQueryParameter()); + Map toQueryParameters() { + Map params = { + 'page': page.toString(), + 'page_size': pageSize.toString(), + }; + params.addAll(documentType.toQueryParameter()); + params.addAll(correspondent.toQueryParameter()); + params.addAll(tags.toQueryParameter()); + params.addAll(storagePath.toQueryParameter()); + params.addAll(asnQuery.toQueryParameter()); + params.addAll(added.toQueryParameter()); + params.addAll(created.toQueryParameter()); + //TODO: Rework when implementing extended queries. if (queryText?.isNotEmpty ?? false) { - sb.write("&${queryType.queryParam}=$queryText"); + params.putIfAbsent(queryType.queryParam, () => queryText!); } + // Reverse ordering can also be encoded using &reverse=1 + params.putIfAbsent( + 'ordering', () => '${sortOrder.queryString}${sortField.queryString}'); - sb.write("&ordering=${sortOrder.queryString}${sortField.queryString}"); - - // Add/subtract one day in the following because paperless uses gt/lt not gte/lte - if (addedDateAfter != null) { - sb.write( - "&added__date__gt=${apiDateFormat.format(addedDateAfter!.subtract(_oneDay))}"); - } - - if (addedDateBefore != null) { - sb.write( - "&added__date__lt=${apiDateFormat.format(addedDateBefore!.add(_oneDay))}"); - } - - if (createdDateAfter != null) { - sb.write( - "&created__date__gt=${apiDateFormat.format(createdDateAfter!.subtract(_oneDay))}"); - } - - if (createdDateBefore != null) { - sb.write( - "&created__date__lt=${apiDateFormat.format(createdDateBefore!.add(_oneDay))}"); - } - - return sb.toString(); + return params; } @override String toString() { - return toQueryString(); + return toQueryParameters().toString(); } DocumentFilter copyWith({ @@ -108,10 +91,8 @@ class DocumentFilter extends Equatable { TagsQuery? tags, SortField? sortField, SortOrder? sortOrder, - DateTime? addedDateAfter, - DateTime? addedDateBefore, - DateTime? createdDateBefore, - DateTime? createdDateAfter, + DateRangeQuery? added, + DateRangeQuery? created, QueryType? queryType, String? queryText, }) { @@ -124,13 +105,11 @@ class DocumentFilter extends Equatable { tags: tags ?? this.tags, sortField: sortField ?? this.sortField, sortOrder: sortOrder ?? this.sortOrder, - addedDateAfter: addedDateAfter ?? this.addedDateAfter, - addedDateBefore: addedDateBefore ?? this.addedDateBefore, + added: added ?? this.added, queryType: queryType ?? this.queryType, queryText: queryText ?? this.queryText, - createdDateBefore: createdDateBefore ?? this.createdDateBefore, - createdDateAfter: createdDateAfter ?? this.createdDateAfter, asnQuery: asnQuery ?? this.asnQuery, + created: created ?? this.created, ); } @@ -160,10 +139,8 @@ class DocumentFilter extends Equatable { correspondent != initial.correspondent, storagePath != initial.storagePath, tags != initial.tags, - (addedDateAfter != initial.addedDateAfter || - addedDateBefore != initial.addedDateBefore), - (createdDateAfter != initial.createdDateAfter || - createdDateBefore != initial.createdDateBefore), + (added != initial.added), + (created != initial.created), asnQuery != initial.asnQuery, (queryType != initial.queryType || queryText != initial.queryText), ].fold(0, (previousValue, element) => previousValue += element ? 1 : 0); @@ -179,10 +156,8 @@ class DocumentFilter extends Equatable { tags, sortField, sortOrder, - addedDateAfter, - addedDateBefore, - createdDateAfter, - createdDateBefore, + added, + created, queryType, queryText, ]; diff --git a/packages/paperless_api/lib/src/models/filter_rule_model.dart b/packages/paperless_api/lib/src/models/filter_rule_model.dart index 5de3454..02eb374 100644 --- a/packages/paperless_api/lib/src/models/filter_rule_model.dart +++ b/packages/paperless_api/lib/src/models/filter_rule_model.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:paperless_api/src/constants.dart'; import 'package:paperless_api/src/models/document_filter.dart'; import 'package:paperless_api/src/models/query_parameters/correspondent_query.dart'; +import 'package:paperless_api/src/models/query_parameters/date_range_query.dart'; import 'package:paperless_api/src/models/query_parameters/document_type_query.dart'; import 'package:paperless_api/src/models/query_parameters/query_type.dart'; import 'package:paperless_api/src/models/query_parameters/storage_path_query.dart'; @@ -18,6 +19,8 @@ class FilterRule with EquatableMixin { static const int createdAfterRule = 9; static const int addedBeforeRule = 13; static const int addedAfterRule = 14; + static const int modifiedBeforeRule = 15; + static const int modifiedAfterRule = 16; static const int excludeTagsRule = 17; static const int titleAndContentRule = 19; static const int extendedRule = 20; @@ -28,14 +31,15 @@ class FilterRule with EquatableMixin { static const int _createdYearIs = 10; static const int _createdMonthIs = 11; static const int _createdDayIs = 12; - static const int _modifiedBefore = 15; - static const int _modifiedAfter = 16; static const int _doesNotHaveAsn = 18; static const int _moreLikeThis = 21; static const int _hasTagsIn = 22; static const int _asnGreaterThan = 23; static const int _asnLessThan = 24; + static const String _lastNDateRangeQueryRegex = + r"(?created|added|modified):\[(?-?\d+) (?day|week|month|year) to now\]"; + final int ruleType; final String? value; @@ -53,6 +57,9 @@ class FilterRule with EquatableMixin { } DocumentFilter applyToFilter(final DocumentFilter filter) { + if (value == null) { + return filter; + } //TODO: Check in profiling mode if this is inefficient enough to cause stutters... switch (ruleType) { case titleRule: @@ -94,34 +101,107 @@ class FilterRule with EquatableMixin { .withIdQueriesAdded([ExcludeTagIdQuery(int.parse(value!))]), ); case createdBeforeRule: - return filter.copyWith( - createdDateBefore: value == null ? null : DateTime.parse(value!), - ); + if (filter.created is FixedDateRangeQuery) { + return filter.copyWith( + created: (filter.created as FixedDateRangeQuery) + .copyWith(before: DateTime.parse(value!)), + ); + } else { + return filter.copyWith( + created: + FixedDateRangeQuery.created(before: DateTime.parse(value!)), + ); + } case createdAfterRule: - return filter.copyWith( - createdDateAfter: value == null ? null : DateTime.parse(value!), - ); + if (filter.created is FixedDateRangeQuery) { + return filter.copyWith( + created: (filter.created as FixedDateRangeQuery) + .copyWith(after: DateTime.parse(value!)), + ); + } else { + return filter.copyWith( + created: FixedDateRangeQuery.created(after: DateTime.parse(value!)), + ); + } case addedBeforeRule: - return filter.copyWith( - addedDateBefore: value == null ? null : DateTime.parse(value!), - ); + if (filter.added is FixedDateRangeQuery) { + return filter.copyWith( + added: (filter.added as FixedDateRangeQuery) + .copyWith(before: DateTime.parse(value!)), + ); + } else { + return filter.copyWith( + added: FixedDateRangeQuery.added(before: DateTime.parse(value!)), + ); + } case addedAfterRule: - return filter.copyWith( - addedDateAfter: value == null ? null : DateTime.parse(value!), - ); + if (filter.added is FixedDateRangeQuery) { + return filter.copyWith( + added: (filter.added as FixedDateRangeQuery) + .copyWith(after: DateTime.parse(value!)), + ); + } else { + return filter.copyWith( + added: FixedDateRangeQuery.added(after: DateTime.parse(value!)), + ); + } case titleAndContentRule: return filter.copyWith( queryText: value, queryType: QueryType.titleAndContent, ); case extendedRule: + _parseExtendedRule(filter); return filter.copyWith(queryText: value, queryType: QueryType.extended); - //TODO: Add currently unused rules default: return filter; } } + DocumentFilter _parseExtendedRule(final DocumentFilter filter) { + DocumentFilter newFilter = filter; + assert(value != null); + final dateRangeRegExp = RegExp(_lastNDateRangeQueryRegex); + if (dateRangeRegExp.hasMatch(value!)) { + final matches = dateRangeRegExp.allMatches(value!); + for (final match in matches) { + final field = match.namedGroup('field')!; + final n = int.parse(match.namedGroup('n')!); + final unit = match.namedGroup('unit')!; + switch (field) { + case 'created': + newFilter = newFilter.copyWith( + created: LastNDateRangeQuery.created( + n, + DateRangeUnit.values.byName(unit), + ), + ); + break; + case 'added': + newFilter = newFilter.copyWith( + created: LastNDateRangeQuery.added( + n, + DateRangeUnit.values.byName(unit), + ), + ); + break; + case 'modified': + newFilter = newFilter.copyWith( + created: LastNDateRangeQuery.modified( + n, + DateRangeUnit.values.byName(unit), + ), + ); + break; + } + } + return newFilter; + } else { + // Match other extended query types... currently not supported! + return filter; + } + } + /// /// Converts a [DocumentFilter] to a list of [FilterRule]s. /// @@ -179,22 +259,65 @@ class FilterRule with EquatableMixin { break; } } - if (filter.createdDateAfter != null) { - filterRules.add(FilterRule( - createdAfterRule, apiDateFormat.format(filter.createdDateAfter!))); + + // Parse created at + final created = filter.created; + if (created is FixedDateRangeQuery) { + if (created.after != null) { + filterRules.add( + FilterRule(createdAfterRule, apiDateFormat.format(created.after!)), + ); + } + if (created.before != null) { + filterRules.add( + FilterRule(createdBeforeRule, apiDateFormat.format(created.before!)), + ); + } + } else if (created is LastNDateRangeQuery) { + filterRules.add( + FilterRule(extendedRule, created.toQueryParameter().values.first), + ); } - if (filter.createdDateBefore != null) { - filterRules.add(FilterRule( - createdBeforeRule, apiDateFormat.format(filter.createdDateBefore!))); + + // Parse added at + final added = filter.added; + if (added is FixedDateRangeQuery) { + if (added.after != null) { + filterRules.add( + FilterRule(addedAfterRule, apiDateFormat.format(added.after!)), + ); + } + if (added.before != null) { + filterRules.add( + FilterRule(addedBeforeRule, apiDateFormat.format(added.before!)), + ); + } + } else if (added is LastNDateRangeQuery) { + filterRules.add( + FilterRule(extendedRule, added.toQueryParameter().values.first), + ); } - if (filter.addedDateAfter != null) { - filterRules.add(FilterRule( - addedAfterRule, apiDateFormat.format(filter.addedDateAfter!))); - } - if (filter.addedDateBefore != null) { - filterRules.add(FilterRule( - addedBeforeRule, apiDateFormat.format(filter.addedDateBefore!))); + + // Parse modified at + final modified = filter.added; + if (modified is FixedDateRangeQuery) { + if (modified.after != null) { + filterRules.add( + FilterRule(modifiedAfterRule, apiDateFormat.format(modified.after!)), + ); + } + if (modified.before != null) { + filterRules.add( + FilterRule( + modifiedBeforeRule, apiDateFormat.format(modified.before!)), + ); + } + } else if (modified is LastNDateRangeQuery) { + filterRules.add( + FilterRule(extendedRule, modified.toQueryParameter().values.first), + ); } + return filterRules; } diff --git a/packages/paperless_api/lib/src/models/query_parameters/date_range_query.dart b/packages/paperless_api/lib/src/models/query_parameters/date_range_query.dart new file mode 100644 index 0000000..076c147 --- /dev/null +++ b/packages/paperless_api/lib/src/models/query_parameters/date_range_query.dart @@ -0,0 +1,103 @@ +import 'package:equatable/equatable.dart'; +import 'package:paperless_api/src/constants.dart'; + +abstract class DateRangeQuery extends Equatable { + const DateRangeQuery(); + Map toQueryParameter(); +} + +class UnsetDateRangeQuery extends DateRangeQuery { + const UnsetDateRangeQuery(); + @override + List get props => []; + + @override + Map toQueryParameter() => const {}; +} + +class FixedDateRangeQuery extends DateRangeQuery { + final String _querySuffix; + + final DateTime? after; + final DateTime? before; + + const FixedDateRangeQuery._(this._querySuffix, {this.after, this.before}) + : assert(after != null || before != null); + + const FixedDateRangeQuery.created({DateTime? after, DateTime? before}) + : this._('created', after: after, before: before); + + const FixedDateRangeQuery.added({DateTime? after, DateTime? before}) + : this._('added', after: after, before: before); + + const FixedDateRangeQuery.modified({DateTime? after, DateTime? before}) + : this._('modified', after: after, before: before); + + @override + List get props => [_querySuffix, after, before]; + + @override + Map toQueryParameter() { + final Map params = {}; + + // Add/subtract one day in the following because paperless uses gt/lt not gte/lte + if (after != null) { + params.putIfAbsent('${_querySuffix}__date__gt', + () => apiDateFormat.format(after!.subtract(const Duration(days: 1)))); + } + + if (before != null) { + params.putIfAbsent('${_querySuffix}__date__lt', + () => apiDateFormat.format(before!.add(const Duration(days: 1)))); + } + return params; + } + + FixedDateRangeQuery copyWith({ + DateTime? before, + DateTime? after, + }) { + return FixedDateRangeQuery._( + _querySuffix, + before: before ?? this.before, + after: after ?? this.after, + ); + } +} + +class LastNDateRangeQuery extends DateRangeQuery { + final DateRangeUnit unit; + final int n; + final String _field; + + const LastNDateRangeQuery._( + this._field, { + required this.n, + required this.unit, + }); + + const LastNDateRangeQuery.created(int n, DateRangeUnit unit) + : this._('created', unit: unit, n: n); + const LastNDateRangeQuery.added(int n, DateRangeUnit unit) + : this._('added', unit: unit, n: n); + const LastNDateRangeQuery.modified(int n, DateRangeUnit unit) + : this._('modified', unit: unit, n: n); + + @override + // TODO: implement props + List get props => [_field, n, unit]; + + @override + Map toQueryParameter() { + return { + 'query': '[$_field:$n ${unit.name} to now]', + }; + } +} + +enum DateRangeUnit { + day, + week, + month, + year; +} diff --git a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart index 7e7a489..610e1e5 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/id_query_parameter.dart @@ -30,14 +30,16 @@ abstract class IdQueryParameter extends Equatable { String get queryParameterKey; - String toQueryParameter() { + Map toQueryParameter() { + final Map params = {}; if (onlyNotAssigned || onlyAssigned) { - return "&${queryParameterKey}__isnull=$_assignmentStatus"; + params.putIfAbsent( + '${queryParameterKey}__isnull', () => _assignmentStatus!.toString()); } if (isSet) { - return "&${queryParameterKey}__id=$id"; + params.putIfAbsent("${queryParameterKey}__id", () => id!.toString()); } - return ""; + return params; } @override diff --git a/packages/paperless_api/lib/src/models/query_parameters/tags_query.dart b/packages/paperless_api/lib/src/models/query_parameters/tags_query.dart index 4d49e44..f025d23 100644 --- a/packages/paperless_api/lib/src/models/query_parameters/tags_query.dart +++ b/packages/paperless_api/lib/src/models/query_parameters/tags_query.dart @@ -2,14 +2,14 @@ import 'package:equatable/equatable.dart'; abstract class TagsQuery extends Equatable { const TagsQuery(); - String toQueryParameter(); + Map toQueryParameter(); } class OnlyNotAssignedTagsQuery extends TagsQuery { const OnlyNotAssignedTagsQuery(); @override - String toQueryParameter() { - return '&is_tagged=0'; + Map toQueryParameter() { + return {'is_tagged': '0'}; } @override @@ -24,11 +24,11 @@ class AnyAssignedTagsQuery extends TagsQuery { }); @override - String toQueryParameter() { + Map toQueryParameter() { if (tagIds.isEmpty) { - return '&is_tagged=1'; + return {'is_tagged': '1'}; } - return '&tags__id__in=${tagIds.join(',')}'; + return {'tags__id__in': tagIds.join(',')}; } @override @@ -89,15 +89,15 @@ class IdsTagsQuery extends TagsQuery { Iterable get ids => [...includedIds, ...excludedIds]; @override - String toQueryParameter() { - final StringBuffer sb = StringBuffer(""); + Map toQueryParameter() { + final Map params = {}; if (includedIds.isNotEmpty) { - sb.write('&tags__id__all=${includedIds.join(',')}'); + params.putIfAbsent('tags__id__all', () => includedIds.join(',')); } if (excludedIds.isNotEmpty) { - sb.write('&tags__id__none=${excludedIds.join(',')}'); + params.putIfAbsent('tags__id__none', () => excludedIds.join(',')); } - return sb.toString(); + return params; } @override diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart index b7bd2f6..b1b2ee0 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart @@ -136,9 +136,12 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { @override Future> find(DocumentFilter filter) async { - final filterParams = filter.toQueryString(); + final filterParams = filter.toQueryParameters(); final response = await baseClient.get( - Uri.parse("/api/documents/?$filterParams"), + Uri( + path: "/api/documents/?$filterParams", + queryParameters: filterParams, + ), ); if (response.statusCode == 200) { return compute(