Implemented basics of extended queries

This commit is contained in:
Anton Stubenbord
2022-12-16 00:58:53 +01:00
parent 209f692cfd
commit f77ccf50c1
6 changed files with 308 additions and 102 deletions

View File

@@ -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<String, String> toQueryParameters() {
Map<String, String> 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,
];

View File

@@ -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"(?<field>created|added|modified):\[(?<n>-?\d+) (?<unit>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;
}

View File

@@ -0,0 +1,103 @@
import 'package:equatable/equatable.dart';
import 'package:paperless_api/src/constants.dart';
abstract class DateRangeQuery extends Equatable {
const DateRangeQuery();
Map<String, String> toQueryParameter();
}
class UnsetDateRangeQuery extends DateRangeQuery {
const UnsetDateRangeQuery();
@override
List<Object?> get props => [];
@override
Map<String, String> 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<Object?> get props => [_querySuffix, after, before];
@override
Map<String, String> toQueryParameter() {
final Map<String, String> 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<Object?> get props => [_field, n, unit];
@override
Map<String, String> toQueryParameter() {
return {
'query': '[$_field:$n ${unit.name} to now]',
};
}
}
enum DateRangeUnit {
day,
week,
month,
year;
}

View File

@@ -30,14 +30,16 @@ abstract class IdQueryParameter extends Equatable {
String get queryParameterKey;
String toQueryParameter() {
Map<String, String> toQueryParameter() {
final Map<String, String> 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

View File

@@ -2,14 +2,14 @@ import 'package:equatable/equatable.dart';
abstract class TagsQuery extends Equatable {
const TagsQuery();
String toQueryParameter();
Map<String, String> toQueryParameter();
}
class OnlyNotAssignedTagsQuery extends TagsQuery {
const OnlyNotAssignedTagsQuery();
@override
String toQueryParameter() {
return '&is_tagged=0';
Map<String, String> toQueryParameter() {
return {'is_tagged': '0'};
}
@override
@@ -24,11 +24,11 @@ class AnyAssignedTagsQuery extends TagsQuery {
});
@override
String toQueryParameter() {
Map<String, String> 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<int> get ids => [...includedIds, ...excludedIds];
@override
String toQueryParameter() {
final StringBuffer sb = StringBuffer("");
Map<String, String> toQueryParameter() {
final Map<String, String> 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

View File

@@ -136,9 +136,12 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
@override
Future<PagedSearchResult<DocumentModel>> 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(