Hooked notifications to status changes on document upload - some refactorings

This commit is contained in:
Anton Stubenbord
2023-01-11 01:26:36 +01:00
parent 8cf3020335
commit a4c4726c16
55 changed files with 1128 additions and 761 deletions

View File

@@ -131,6 +131,6 @@ class DocumentModel extends Equatable {
archiveSerialNumber,
originalFileName,
archivedFileName,
storagePath
storagePath,
];
}

View File

@@ -0,0 +1,45 @@
import 'package:json_annotation/json_annotation.dart';
part 'field_suggestions.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class FieldSuggestions {
final Iterable<int> correspondents;
final Iterable<int> tags;
final Iterable<int> documentTypes;
final Iterable<int> storagePaths;
final Iterable<DateTime> dates;
const FieldSuggestions({
this.correspondents = const [],
this.tags = const [],
this.documentTypes = const [],
this.storagePaths = const [],
this.dates = const [],
});
bool get hasSuggestedCorrespondents => correspondents.isNotEmpty;
bool get hasSuggestedTags => tags.isNotEmpty;
bool get hasSuggestedDocumentTypes => documentTypes.isNotEmpty;
bool get hasSuggestedStoragePaths => storagePaths.isNotEmpty;
bool get hasSuggestedDates => dates.isNotEmpty;
bool get hasSuggestions =>
hasSuggestedCorrespondents ||
hasSuggestedDates ||
hasSuggestedTags ||
hasSuggestedStoragePaths ||
hasSuggestedDocumentTypes;
int get suggestionsCount =>
(correspondents.isNotEmpty ? 1 : 0) +
(tags.isNotEmpty ? 1 : 0) +
(documentTypes.isNotEmpty ? 1 : 0) +
(storagePaths.isNotEmpty ? 1 : 0) +
(dates.isNotEmpty ? 1 : 0);
factory FieldSuggestions.fromJson(Map<String, dynamic> json) =>
_$FieldSuggestionsFromJson(json);
Map<String, dynamic> toJson() => _$FieldSuggestionsToJson(this);
}

View File

@@ -0,0 +1,33 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'field_suggestions.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FieldSuggestions _$FieldSuggestionsFromJson(Map<String, dynamic> json) =>
FieldSuggestions(
correspondents:
(json['correspondents'] as List<dynamic>?)?.map((e) => e as int) ??
const [],
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as int) ?? const [],
documentTypes:
(json['document_types'] as List<dynamic>?)?.map((e) => e as int) ??
const [],
storagePaths:
(json['storage_paths'] as List<dynamic>?)?.map((e) => e as int) ??
const [],
dates: (json['dates'] as List<dynamic>?)
?.map((e) => DateTime.parse(e as String)) ??
const [],
);
Map<String, dynamic> _$FieldSuggestionsToJson(FieldSuggestions instance) =>
<String, dynamic>{
'correspondents': instance.correspondents.toList(),
'tags': instance.tags.toList(),
'document_types': instance.documentTypes.toList(),
'storage_paths': instance.storagePaths.toList(),
'dates': instance.dates.map((e) => e.toIso8601String()).toList(),
};

View File

@@ -346,15 +346,17 @@ 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);
//Join values of all extended filter rules if exist
if (filterRules.isNotEmpty) {
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;
}

View File

@@ -24,3 +24,4 @@ export 'saved_view_model.dart';
export 'similar_document_model.dart';
export 'task/task.dart';
export 'task/task_status.dart';
export 'field_suggestions.dart';

View File

@@ -43,6 +43,7 @@ enum ErrorCode {
deviceOffline,
serverUnreachable,
similarQueryError,
suggestionsQueryError,
autocompleteQueryError,
storagePathLoadFailed,
storagePathCreateFailed,
@@ -51,5 +52,6 @@ enum ErrorCode {
deleteSavedViewError,
requestTimedOut,
unsupportedFileFormat,
missingClientCertificate;
missingClientCertificate,
acknowledgeTasksError;
}

View File

@@ -1,3 +1,5 @@
import 'package:paperless_api/src/request_utils.dart';
class PaperlessServerInformationModel {
static const String versionHeader = 'x-version';
static const String apiVersionHeader = 'x-api-version';
@@ -13,4 +15,9 @@ class PaperlessServerInformationModel {
this.version = 'unknown',
this.apiVersion = 1,
});
int compareToOtherVersion(String? other) {
return getExtendedVersionNumber(version ?? '0.0.0')
.compareTo(getExtendedVersionNumber(other ?? '0.0.0'));
}
}

View File

@@ -17,7 +17,7 @@ class Task extends Equatable {
final String? result;
final bool acknowledged;
@JsonKey(fromJson: tryParseNullable)
final int? relatedDocumentId;
final int? relatedDocument;
const Task({
required this.id,
@@ -28,7 +28,7 @@ class Task extends Equatable {
this.type,
this.status,
this.acknowledged = false,
this.relatedDocumentId,
this.relatedDocument,
this.result,
});
@@ -47,6 +47,32 @@ class Task extends Equatable {
status,
result,
acknowledged,
relatedDocumentId,
relatedDocument,
];
Task copyWith({
int? id,
String? taskId,
String? taskFileName,
DateTime? dateCreated,
DateTime? dateDone,
String? type,
TaskStatus? status,
String? result,
bool? acknowledged,
int? relatedDocument,
}) {
return Task(
id: id ?? this.id,
taskId: taskId ?? this.taskId,
dateCreated: dateCreated ?? this.dateCreated,
acknowledged: acknowledged ?? this.acknowledged,
dateDone: dateDone ?? this.dateDone,
relatedDocument: relatedDocument ?? this.relatedDocument,
result: result ?? this.result,
status: status ?? this.status,
taskFileName: taskFileName ?? this.taskFileName,
type: type ?? this.type,
);
}
}

View File

@@ -17,8 +17,7 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
type: json['type'] as String?,
status: $enumDecodeNullable(_$TaskStatusEnumMap, json['status']),
acknowledged: json['acknowledged'] as bool? ?? false,
relatedDocumentId:
tryParseNullable(json['related_document_id'] as String?),
relatedDocument: tryParseNullable(json['related_document'] as String?),
result: json['result'] as String?,
);
@@ -32,7 +31,7 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
'status': _$TaskStatusEnumMap[instance.status],
'result': instance.result,
'acknowledged': instance.acknowledged,
'related_document_id': instance.relatedDocumentId,
'related_document': instance.relatedDocument,
};
const _$TaskStatusEnumMap = {

View File

@@ -1,11 +1,6 @@
import 'dart:typed_data';
import 'package:paperless_api/src/models/bulk_edit_model.dart';
import 'package:paperless_api/src/models/document_filter.dart';
import 'package:paperless_api/src/models/document_meta_data_model.dart';
import 'package:paperless_api/src/models/document_model.dart';
import 'package:paperless_api/src/models/paged_search_result.dart';
import 'package:paperless_api/src/models/similar_document_model.dart';
import 'package:paperless_api/src/models/models.dart';
abstract class PaperlessDocumentsApi {
/// Uploads a document using a form data request and from server version 1.11.3
@@ -21,18 +16,16 @@ abstract class PaperlessDocumentsApi {
});
Future<DocumentModel> update(DocumentModel doc);
Future<int> findNextAsn();
Future<PagedSearchResult<DocumentModel>> find(DocumentFilter filter);
Future<PagedSearchResult<DocumentModel>> findAll(DocumentFilter filter);
Future<DocumentModel?> find(int id);
Future<List<SimilarDocumentModel>> findSimilar(int docId);
Future<int> delete(DocumentModel doc);
Future<DocumentMetaData> getMetaData(DocumentModel document);
Future<Iterable<int>> bulkAction(BulkAction action);
Future<Uint8List> getPreview(int docId);
String getThumbnailUrl(int docId);
Future<DocumentModel> waitForConsumptionFinished(
String filename,
String title,
);
Future<Uint8List> download(DocumentModel document);
Future<FieldSuggestions> findSuggestions(DocumentModel document);
Future<List<String>> autocomplete(String query, [int limit = 10]);
}

View File

@@ -1,13 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/constants.dart';
import 'package:paperless_api/src/converters/document_model_json_converter.dart';
import 'package:paperless_api/src/converters/similar_document_model_json_converter.dart';
import 'package:paperless_api/src/request_utils.dart';
class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final Dio client;
@@ -82,8 +78,11 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
}
@override
Future<PagedSearchResult<DocumentModel>> find(DocumentFilter filter) async {
final filterParams = filter.toQueryParameters();
Future<PagedSearchResult<DocumentModel>> findAll(
DocumentFilter filter,
) async {
final filterParams = filter.toQueryParameters()
..addAll({'truncate_content': "true"});
try {
final response = await client.get(
"/api/documents/",
@@ -156,7 +155,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
pageSize: 1,
);
try {
final result = await find(asnQueryFilter);
final result = await findAll(asnQueryFilter);
return result.results
.map((e) => e.archiveSerialNumber)
.firstWhere((asn) => asn != null, orElse: () => 0)! +
@@ -187,26 +186,6 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
}
}
@override
Future<DocumentModel> waitForConsumptionFinished(
String fileName, String title) async {
PagedSearchResult<DocumentModel> results =
await find(DocumentFilter.latestDocument);
while ((results.results.isEmpty ||
(results.results[0].originalFileName != fileName &&
results.results[0].title != title))) {
//TODO: maybe implement more intelligent retry logic or find workaround for websocket authentication...
await Future.delayed(const Duration(seconds: 2));
results = await find(DocumentFilter.latestDocument);
}
try {
return results.results.first;
} on StateError {
throw const PaperlessServerException(ErrorCode.documentUploadFailed);
}
}
@override
Future<Uint8List> download(DocumentModel document) async {
try {
@@ -273,4 +252,32 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
throw err.error;
}
}
@override
Future<FieldSuggestions> findSuggestions(DocumentModel document) async {
try {
final response =
await client.get("/api/documents/${document.id}/suggestions/");
if (response.statusCode == 200) {
return FieldSuggestions.fromJson(response.data);
}
throw const PaperlessServerException(ErrorCode.suggestionsQueryError);
} on DioError catch (err) {
throw err.error;
}
}
@override
Future<DocumentModel?> find(int id) async {
try {
final response = await client.get("/api/documents/$id/");
if (response.statusCode == 200) {
return DocumentModel.fromJson(response.data);
} else {
return null;
}
} on DioError catch (err) {
throw err.error;
}
}
}

View File

@@ -4,4 +4,6 @@ abstract class PaperlessTasksApi {
Future<Task?> find({int? id, String? taskId});
Future<Iterable<Task>> findAll([Iterable<int>? ids]);
Stream<Task> listenForTaskChanges(String taskId);
Future<Task> acknowledgeTask(Task task);
Future<Iterable<Task>> acknowledgeTasks(Iterable<Task> tasks);
}

View File

@@ -1,34 +1,48 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/src/models/task/task.dart';
import 'package:paperless_api/src/models/task/task_status.dart';
import 'dart:developer';
import 'paperless_tasks_api.dart';
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/request_utils.dart';
class PaperlessTasksApiImpl implements PaperlessTasksApi {
final Dio client;
final Dio _client;
const PaperlessTasksApiImpl(this.client);
PaperlessTasksApiImpl(this._client);
@override
Future<Task?> find({int? id, String? taskId}) async {
assert(id != null || taskId != null);
String url = "/api/tasks/";
if (taskId != null) {
url += "?task_id=$taskId";
} else {
url += "$id/";
assert((id != null) != (taskId != null));
if (id != null) {
return _findById(id);
} else if (taskId != null) {
return _findByTaskId(taskId);
}
return null;
}
final response = await client.get(url);
/// API response returns List with single item
Future<Task?> _findById(int id) async {
final response = await _client.get("/api/tasks/$id/");
if (response.statusCode == 200) {
return Task.fromJson(response.data);
}
return null;
}
/// API response returns List with single item
Future<Task?> _findByTaskId(String taskId) async {
final response = await _client.get("/api/tasks/?task_id=$taskId");
if (response.statusCode == 200) {
if ((response.data as List).isNotEmpty) {
return Task.fromJson((response.data as List).first);
}
}
return null;
}
@override
Future<Iterable<Task>> findAll([Iterable<int>? ids]) async {
final response = await client.get("/api/tasks/");
final response = await _client.get("/api/tasks/");
if (response.statusCode == 200) {
return (response.data as List).map((e) => Task.fromJson(e));
}
@@ -37,17 +51,39 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi {
@override
Stream<Task> listenForTaskChanges(String taskId) async* {
bool isSuccess = false;
while (!isSuccess) {
bool isCompleted = false;
while (!isCompleted) {
final task = await find(taskId: taskId);
if (task == null) {
throw Exception("Task with taskId $taskId does not exist.");
}
log("Found new task: ${task.taskId}, ${task.id}, ${task.status}");
yield task;
if (task.status == TaskStatus.success) {
isSuccess = true;
if (task.status == TaskStatus.success ||
task.status == TaskStatus.failure) {
isCompleted = true;
}
await Future.delayed(const Duration(seconds: 1));
}
}
@override
Future<Task> acknowledgeTask(Task task) async {
final acknowledgedTasks = await acknowledgeTasks([task]);
return acknowledgedTasks.first.copyWith(acknowledged: true);
}
@override
Future<Iterable<Task>> acknowledgeTasks(Iterable<Task> tasks) async {
final response = await _client.post("/api/acknowledge_tasks/", data: {
'tasks': tasks.map((e) => e.id).toList(),
});
if (response.statusCode == 200) {
if (response.data['result'] != tasks.length) {
throw const PaperlessServerException(ErrorCode.acknowledgeTasksError);
}
return tasks.map((e) => e.copyWith(acknowledged: true)).toList();
}
throw const PaperlessServerException(ErrorCode.acknowledgeTasksError);
}
}