mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 04:07:51 -06:00
Implemented extended query constraints
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:paperless_api/src/models/query_parameters/text_query.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class DocumentFilter extends Equatable {
|
||||
static const _oneDay = Duration(days: 1);
|
||||
static const DocumentFilter initial = DocumentFilter();
|
||||
|
||||
static const DocumentFilter latestDocument = DocumentFilter(
|
||||
@@ -26,8 +27,7 @@ class DocumentFilter extends Equatable {
|
||||
final DateRangeQuery created;
|
||||
final DateRangeQuery added;
|
||||
final DateRangeQuery modified;
|
||||
final QueryType queryType;
|
||||
final String? queryText;
|
||||
final TextQuery query;
|
||||
|
||||
const DocumentFilter({
|
||||
this.documentType = const IdQueryParameter.unset(),
|
||||
@@ -39,47 +39,52 @@ class DocumentFilter extends Equatable {
|
||||
this.sortOrder = SortOrder.descending,
|
||||
this.page = 1,
|
||||
this.pageSize = 25,
|
||||
this.queryType = QueryType.titleAndContent,
|
||||
this.queryText,
|
||||
this.query = const TextQuery(),
|
||||
this.added = const UnsetDateRangeQuery(),
|
||||
this.created = const UnsetDateRangeQuery(),
|
||||
this.modified = const UnsetDateRangeQuery(),
|
||||
});
|
||||
|
||||
Map<String, String> toQueryParameters() {
|
||||
Map<String, String> params = {
|
||||
'page': page.toString(),
|
||||
'page_size': pageSize.toString(),
|
||||
};
|
||||
bool get forceExtendedQuery {
|
||||
return added is RelativeDateRangeQuery ||
|
||||
created is RelativeDateRangeQuery ||
|
||||
modified is RelativeDateRangeQuery;
|
||||
}
|
||||
|
||||
params.addAll(documentType.toQueryParameter('document_type'));
|
||||
params.addAll(correspondent.toQueryParameter('correspondent'));
|
||||
params.addAll(storagePath.toQueryParameter('storage_path'));
|
||||
params.addAll(asnQuery.toQueryParameter('archive_serial_number'));
|
||||
params.addAll(tags.toQueryParameter());
|
||||
params.addAll(added.toQueryParameter(DateRangeQueryField.added));
|
||||
params.addAll(created.toQueryParameter(DateRangeQueryField.created));
|
||||
params.addAll(modified.toQueryParameter(DateRangeQueryField.modified));
|
||||
//TODO: Rework when implementing extended queries.
|
||||
if (queryText?.isNotEmpty ?? false) {
|
||||
params.putIfAbsent(queryType.queryParam, () => queryText!);
|
||||
}
|
||||
Map<String, dynamic> toQueryParameters() {
|
||||
List<MapEntry<String, dynamic>> params = [
|
||||
MapEntry('page', '$page'),
|
||||
MapEntry('page_size', '$pageSize'),
|
||||
MapEntry('ordering', '${sortOrder.queryString}${sortField.queryString}'),
|
||||
...documentType.toQueryParameter('document_type').entries,
|
||||
...correspondent.toQueryParameter('correspondent').entries,
|
||||
...storagePath.toQueryParameter('storage_path').entries,
|
||||
...asnQuery.toQueryParameter('archive_serial_number').entries,
|
||||
...tags.toQueryParameter().entries,
|
||||
...added.toQueryParameter(DateRangeQueryField.added).entries,
|
||||
...created.toQueryParameter(DateRangeQueryField.created).entries,
|
||||
...modified.toQueryParameter(DateRangeQueryField.modified).entries,
|
||||
...query.toQueryParameter().entries,
|
||||
];
|
||||
// Reverse ordering can also be encoded using &reverse=1
|
||||
params.putIfAbsent(
|
||||
'ordering', () => '${sortOrder.queryString}${sortField.queryString}');
|
||||
|
||||
return params;
|
||||
// Merge query params
|
||||
final queryParams = groupBy(params, (e) => e.key).map(
|
||||
(key, entries) => MapEntry(
|
||||
key,
|
||||
entries.length == 1
|
||||
? entries.first.value
|
||||
: entries.map((e) => e.value).toList(),
|
||||
),
|
||||
);
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return toQueryParameters().toString();
|
||||
}
|
||||
String toString() => toQueryParameters().toString();
|
||||
|
||||
DocumentFilter copyWith({
|
||||
int? pageSize,
|
||||
int? page,
|
||||
bool? onlyNoDocumentType,
|
||||
IdQueryParameter? documentType,
|
||||
IdQueryParameter? correspondent,
|
||||
IdQueryParameter? storagePath,
|
||||
@@ -90,10 +95,9 @@ class DocumentFilter extends Equatable {
|
||||
DateRangeQuery? added,
|
||||
DateRangeQuery? created,
|
||||
DateRangeQuery? modified,
|
||||
QueryType? queryType,
|
||||
String? queryText,
|
||||
TextQuery? query,
|
||||
}) {
|
||||
return DocumentFilter(
|
||||
final newFilter = DocumentFilter(
|
||||
pageSize: pageSize ?? this.pageSize,
|
||||
page: page ?? this.page,
|
||||
documentType: documentType ?? this.documentType,
|
||||
@@ -102,34 +106,20 @@ class DocumentFilter extends Equatable {
|
||||
tags: tags ?? this.tags,
|
||||
sortField: sortField ?? this.sortField,
|
||||
sortOrder: sortOrder ?? this.sortOrder,
|
||||
queryType: queryType ?? this.queryType,
|
||||
queryText: queryText ?? this.queryText,
|
||||
asnQuery: asnQuery ?? this.asnQuery,
|
||||
query: query ?? this.query,
|
||||
added: added ?? this.added,
|
||||
created: created ?? this.created,
|
||||
modified: modified ?? this.modified,
|
||||
);
|
||||
}
|
||||
|
||||
String? get titleOnlyMatchString {
|
||||
if (queryType == QueryType.title) {
|
||||
return queryText?.isEmpty ?? true ? null : queryText;
|
||||
if (query?.queryType != QueryType.extended &&
|
||||
newFilter.forceExtendedQuery) {
|
||||
//Prevents infinite recursion
|
||||
return newFilter.copyWith(
|
||||
query: newFilter.query.copyWith(queryType: QueryType.extended),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? get titleAndContentMatchString {
|
||||
if (queryType == QueryType.titleAndContent) {
|
||||
return queryText?.isEmpty ?? true ? null : queryText;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? get extendedMatchString {
|
||||
if (queryType == QueryType.extended) {
|
||||
return queryText?.isEmpty ?? true ? null : queryText;
|
||||
}
|
||||
return null;
|
||||
return newFilter;
|
||||
}
|
||||
|
||||
int get appliedFiltersCount => [
|
||||
@@ -141,7 +131,7 @@ class DocumentFilter extends Equatable {
|
||||
created != initial.created,
|
||||
modified != initial.modified,
|
||||
asnQuery != initial.asnQuery,
|
||||
(queryType != initial.queryType || queryText != initial.queryText),
|
||||
(query.queryText != initial.query.queryText),
|
||||
].fold(0, (previousValue, element) => previousValue += element ? 1 : 0);
|
||||
|
||||
@override
|
||||
@@ -158,7 +148,6 @@ class DocumentFilter extends Equatable {
|
||||
added,
|
||||
created,
|
||||
modified,
|
||||
queryType,
|
||||
queryText,
|
||||
query,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_api/src/constants.dart';
|
||||
import 'package:paperless_api/src/models/query_parameters/text_query.dart';
|
||||
|
||||
class FilterRule with EquatableMixin {
|
||||
static const int titleRule = 0;
|
||||
@@ -32,7 +33,7 @@ class FilterRule with EquatableMixin {
|
||||
static const int _asnLessThan = 24;
|
||||
|
||||
static const String _lastNDateRangeQueryRegex =
|
||||
r"(?<field>created|added|modified):\[(?<n>-?\d+) (?<unit>day|week|month|year) to now\]";
|
||||
r"(?<field>created|added|modified):\[-?(?<n>\d+) (?<unit>day|week|month|year) to now\]";
|
||||
|
||||
final int ruleType;
|
||||
final String? value;
|
||||
@@ -54,7 +55,7 @@ class FilterRule with EquatableMixin {
|
||||
//TODO: Check in profiling mode if this is inefficient enough to cause stutters...
|
||||
switch (ruleType) {
|
||||
case titleRule:
|
||||
return filter.copyWith(queryText: value, queryType: QueryType.title);
|
||||
return filter.copyWith(query: TextQuery.title(value));
|
||||
case documentTypeRule:
|
||||
return filter.copyWith(
|
||||
documentType: value == null
|
||||
@@ -158,60 +159,69 @@ class FilterRule with EquatableMixin {
|
||||
);
|
||||
}
|
||||
case titleAndContentRule:
|
||||
return filter.copyWith(
|
||||
queryText: value,
|
||||
queryType: QueryType.titleAndContent,
|
||||
);
|
||||
return filter.copyWith(query: TextQuery.titleAndContent(value));
|
||||
case extendedRule:
|
||||
_parseExtendedRule(filter);
|
||||
return filter.copyWith(queryText: value, queryType: QueryType.extended);
|
||||
return _parseExtendedRule(filter);
|
||||
default:
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
|
||||
DocumentFilter _parseExtendedRule(final DocumentFilter filter) {
|
||||
DocumentFilter newFilter = filter;
|
||||
DocumentFilter _parseExtendedRule(DocumentFilter 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: RelativeDateRangeQuery(
|
||||
n,
|
||||
DateRangeUnit.values.byName(unit),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'added':
|
||||
newFilter = newFilter.copyWith(
|
||||
added: RelativeDateRangeQuery(
|
||||
n,
|
||||
DateRangeUnit.values.byName(unit),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'modified':
|
||||
newFilter = newFilter.copyWith(
|
||||
modified: RelativeDateRangeQuery(
|
||||
n,
|
||||
DateRangeUnit.values.byName(unit),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
final extendedQueryValues = value!.split(",").reversed;
|
||||
|
||||
for (final query in extendedQueryValues) {
|
||||
if (RegExp(_lastNDateRangeQueryRegex).hasMatch(query)) {
|
||||
filter = _parseRelativeDateRangeQuery(query, filter);
|
||||
} else {
|
||||
filter = filter.copyWith(query: TextQuery.extended(query));
|
||||
}
|
||||
return newFilter;
|
||||
} else {
|
||||
// Match other extended query types... currently not supported!
|
||||
return filter;
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
DocumentFilter _parseRelativeDateRangeQuery(
|
||||
String query,
|
||||
final DocumentFilter filter,
|
||||
) {
|
||||
DocumentFilter newFilter = filter;
|
||||
final matches = RegExp(_lastNDateRangeQueryRegex).allMatches(query);
|
||||
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: RelativeDateRangeQuery(
|
||||
n,
|
||||
DateRangeUnit.values.byName(unit),
|
||||
),
|
||||
query: newFilter.query.copyWith(queryType: QueryType.extended),
|
||||
);
|
||||
break;
|
||||
case 'added':
|
||||
newFilter = newFilter.copyWith(
|
||||
added: RelativeDateRangeQuery(
|
||||
n,
|
||||
DateRangeUnit.values.byName(unit),
|
||||
),
|
||||
query: newFilter.query.copyWith(queryType: QueryType.extended),
|
||||
);
|
||||
break;
|
||||
case 'modified':
|
||||
newFilter = newFilter.copyWith(
|
||||
modified: RelativeDateRangeQuery(
|
||||
n,
|
||||
DateRangeUnit.values.byName(unit),
|
||||
),
|
||||
query: newFilter.query.copyWith(queryType: QueryType.extended),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return newFilter;
|
||||
}
|
||||
|
||||
///
|
||||
@@ -254,20 +264,20 @@ class FilterRule with EquatableMixin {
|
||||
.excludedIds
|
||||
.map((id) => FilterRule(excludeTagsRule, id.toString())));
|
||||
}
|
||||
|
||||
if (filter.queryText != null) {
|
||||
switch (filter.queryType) {
|
||||
if (filter.query.queryText != null) {
|
||||
switch (filter.query.queryType) {
|
||||
case QueryType.title:
|
||||
filterRules.add(FilterRule(titleRule, filter.queryText!));
|
||||
filterRules.add(FilterRule(titleRule, filter.query.queryText!));
|
||||
break;
|
||||
case QueryType.titleAndContent:
|
||||
filterRules.add(FilterRule(titleAndContentRule, filter.queryText!));
|
||||
filterRules
|
||||
.add(FilterRule(titleAndContentRule, filter.query.queryText!));
|
||||
break;
|
||||
case QueryType.extended:
|
||||
filterRules.add(FilterRule(extendedRule, filter.queryText!));
|
||||
filterRules.add(FilterRule(extendedRule, filter.query.queryText!));
|
||||
break;
|
||||
case QueryType.asn:
|
||||
filterRules.add(FilterRule(asnRule, filter.queryText!));
|
||||
filterRules.add(FilterRule(asnRule, filter.query.queryText!));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -337,9 +347,25 @@ class FilterRule with EquatableMixin {
|
||||
);
|
||||
}
|
||||
|
||||
//Join values of all extended filter rules
|
||||
final FilterRule extendedFilterRule = filterRules
|
||||
.where((r) => r.ruleType == extendedRule)
|
||||
.reduce((previousValue, element) => previousValue.copyWith(
|
||||
value: previousValue.value! + element.value!,
|
||||
));
|
||||
filterRules
|
||||
..removeWhere((element) => element.ruleType == extendedRule)
|
||||
..add(extendedFilterRule);
|
||||
return filterRules;
|
||||
}
|
||||
|
||||
FilterRule copyWith({int? ruleType, String? value}) {
|
||||
return FilterRule(
|
||||
ruleType ?? this.ruleType,
|
||||
value ?? this.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [ruleType, value];
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export 'query_parameters/sort_field.dart';
|
||||
export 'query_parameters/sort_order.dart';
|
||||
export 'query_parameters/tags_query.dart';
|
||||
export 'query_parameters/date_range_query.dart';
|
||||
export 'query_parameters/text_query.dart';
|
||||
export 'bulk_edit_model.dart';
|
||||
export 'document_filter.dart';
|
||||
export 'document_meta_data_model.dart';
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'query_type.dart';
|
||||
|
||||
class TextQuery {
|
||||
final QueryType queryType;
|
||||
final String? queryText;
|
||||
|
||||
const TextQuery({
|
||||
this.queryType = QueryType.titleAndContent,
|
||||
this.queryText,
|
||||
});
|
||||
|
||||
const TextQuery.title(this.queryText) : queryType = QueryType.title;
|
||||
|
||||
const TextQuery.titleAndContent(this.queryText)
|
||||
: queryType = QueryType.titleAndContent;
|
||||
|
||||
const TextQuery.extended(this.queryText) : queryType = QueryType.extended;
|
||||
|
||||
TextQuery copyWith({QueryType? queryType, String? queryText}) {
|
||||
return TextQuery(
|
||||
queryType: queryType ?? this.queryType,
|
||||
queryText: queryText ?? this.queryText,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, String> toQueryParameter() {
|
||||
final params = <String, String>{};
|
||||
if (queryText != null) {
|
||||
params.addAll({queryType.queryParam: queryText!});
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
String? get titleOnlyMatchString {
|
||||
if (queryType == QueryType.title) {
|
||||
return queryText?.isEmpty ?? true ? null : queryText;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? get titleAndContentMatchString {
|
||||
if (queryType == QueryType.titleAndContent) {
|
||||
return queryText?.isEmpty ?? true ? null : queryText;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? get extendedMatchString {
|
||||
if (queryType == QueryType.extended) {
|
||||
return queryText?.isEmpty ?? true ? null : queryText;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_api/src/models/query_parameters/text_query.dart';
|
||||
|
||||
void main() {
|
||||
group('Validate parsing logic from [SavedView] to [DocumentFilter]:', () {
|
||||
@@ -86,8 +87,7 @@ void main() {
|
||||
),
|
||||
sortField: SortField.created,
|
||||
sortOrder: SortOrder.descending,
|
||||
queryText: "Never gonna give you up",
|
||||
queryType: QueryType.extended,
|
||||
query: const TextQuery.extended("Never gonna give you up"),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -173,8 +173,7 @@ void main() {
|
||||
before: DateTime.parse("2020-03-01"),
|
||||
after: DateTime.parse("2020-01-01"),
|
||||
),
|
||||
queryText: "Never gonna let you down",
|
||||
queryType: QueryType.title,
|
||||
query: const TextQuery.title("Never gonna let you down"),
|
||||
),
|
||||
name: "test_name",
|
||||
showInSidebar: false,
|
||||
@@ -219,7 +218,7 @@ void main() {
|
||||
sortOrder: SortOrder.descending,
|
||||
added: UnsetDateRangeQuery(),
|
||||
created: UnsetDateRangeQuery(),
|
||||
queryText: null,
|
||||
query: TextQuery(),
|
||||
),
|
||||
name: "test_name",
|
||||
showInSidebar: false,
|
||||
|
||||
Reference in New Issue
Block a user