mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 20:07:48 -06:00
Hooked notifications to status changes on document upload - some refactorings
This commit is contained in:
@@ -131,6 +131,6 @@ class DocumentModel extends Equatable {
|
||||
archiveSerialNumber,
|
||||
originalFileName,
|
||||
archivedFileName,
|
||||
storagePath
|
||||
storagePath,
|
||||
];
|
||||
}
|
||||
|
||||
45
packages/paperless_api/lib/src/models/field_suggestions.dart
Normal file
45
packages/paperless_api/lib/src/models/field_suggestions.dart
Normal 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);
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user