feat: Rework error handling, upgrade dio, fixed bugs

- Fix grey screen bug when adding labels from documnet upload
- Add more permission checks to conditionally show widgets
This commit is contained in:
Anton Stubenbord
2023-07-22 14:17:48 +02:00
parent c4f2810974
commit 6566b2b8d7
70 changed files with 1446 additions and 1133 deletions

View File

@@ -0,0 +1,7 @@
import 'package:dio/dio.dart';
extension DioExceptionUnravelExtension on DioException {
Object unravel({Object? orElse}) {
return error ?? orElse ?? Exception("Unknown");
}
}

View File

@@ -0,0 +1,3 @@
export 'paperless_server_message_exception.dart';
export 'paperless_form_validation_exception.dart';
export 'paperless_unauthorized_exception.dart';

View File

@@ -0,0 +1,42 @@
class PaperlessFormValidationException implements Exception {
final Map<String, String> validationMessages;
PaperlessFormValidationException(this.validationMessages);
bool hasMessageForField(String formKey) {
return validationMessages.containsKey(formKey);
}
bool hasUnspecificErrorMessage() {
return validationMessages.containsKey("non_field_errors");
}
String? unspecificErrorMessage() {
return validationMessages["non_field_errors"];
}
String? messageForField(String formKey) {
return validationMessages[formKey];
}
static bool canParse(Map<String, dynamic> json) {
return json.values.every((element) => element is String);
}
factory PaperlessFormValidationException.fromJson(Map<String, dynamic> json) {
final Map<String, String> validationMessages = {};
for (final entry in json.entries) {
if (entry.value is List) {
validationMessages.putIfAbsent(
entry.key,
() => (entry.value as List).first as String,
);
} else if (entry.value is String) {
validationMessages.putIfAbsent(entry.key, () => entry.value);
} else {
validationMessages.putIfAbsent(entry.key, () => entry.value.toString());
}
}
return PaperlessFormValidationException(validationMessages);
}
}

View File

@@ -0,0 +1,17 @@
import 'package:json_annotation/json_annotation.dart';
part 'paperless_server_exception.g.dart';
@JsonSerializable(createToJson: false)
class PaperlessServerMessageException implements Exception {
final String detail;
PaperlessServerMessageException(this.detail);
static bool canParse(Map<String, dynamic> json) {
return json.containsKey('detail') && json.length == 1;
}
factory PaperlessServerMessageException.fromJson(Map<String, dynamic> json) =>
_$PaperlessServerExceptionFromJson(json);
}

View File

@@ -0,0 +1,5 @@
class PaperlessUnauthorizedException implements Exception {
final String? message;
PaperlessUnauthorizedException(this.message);
}

View File

@@ -0,0 +1 @@

View File

@@ -12,7 +12,7 @@ export 'labels/matching_algorithm.dart';
export 'labels/storage_path_model.dart';
export 'labels/tag_model.dart';
export 'paged_search_result.dart';
export 'paperless_server_exception.dart';
export 'paperless_api_exception.dart';
export 'paperless_server_information_model.dart';
export 'paperless_server_statistics_model.dart';
export 'permissions/inherited_permissions.dart';
@@ -31,3 +31,4 @@ export 'saved_view_model.dart';
export 'task/task.dart';
export 'task/task_status.dart';
export 'user_model.dart';
export 'exception/exceptions.dart';

View File

@@ -1,17 +1,17 @@
class PaperlessServerException implements Exception {
class PaperlessApiException implements Exception {
final ErrorCode code;
final String? details;
final StackTrace? stackTrace;
final int? httpStatusCode;
const PaperlessServerException(
const PaperlessApiException(
this.code, {
this.details,
this.stackTrace,
this.httpStatusCode,
});
const PaperlessServerException.unknown() : this(ErrorCode.unknown);
const PaperlessApiException.unknown() : this(ErrorCode.unknown);
@override
String toString() {
@@ -53,5 +53,6 @@ enum ErrorCode {
requestTimedOut,
unsupportedFileFormat,
missingClientCertificate,
acknowledgeTasksError;
acknowledgeTasksError,
correspondentDeleteFailed, documentTypeDeleteFailed, tagDeleteFailed, correspondentUpdateFailed, documentTypeUpdateFailed, tagUpdateFailed, storagePathDeleteFailed, storagePathUpdateFailed, serverInformationLoadFailed, serverStatisticsLoadFailed, uiSettingsLoadFailed, loadTasksError, userNotFound;
}

View File

@@ -6,13 +6,15 @@ extension UserPermissionExtension on UserModel {
v3: (user) {
final permission = [action.value, target.value].join("_");
return user.userPermissions.any((element) => element == permission) ||
user.inheritedPermissions.any((element) => element.split(".").last == permission);
user.inheritedPermissions
.any((element) => element.split(".").last == permission);
},
v2: (_) => true,
);
}
bool hasPermissions(List<PermissionAction> actions, List<PermissionTarget> targets) {
bool hasPermissions(
List<PermissionAction> actions, List<PermissionTarget> targets) {
return map(
v3: (user) {
final permissions = [
@@ -21,10 +23,62 @@ extension UserPermissionExtension on UserModel {
];
return permissions.every((requestedPermission) =>
user.userPermissions.contains(requestedPermission) ||
user.inheritedPermissions
.any((element) => element.split(".").last == requestedPermission));
user.inheritedPermissions.any(
(element) => element.split(".").last == requestedPermission));
},
v2: (_) => true,
);
}
bool get canViewDocuments =>
hasPermission(PermissionAction.view, PermissionTarget.document);
bool get canViewCorrespondents =>
hasPermission(PermissionAction.view, PermissionTarget.correspondent);
bool get canViewDocumentTypes =>
hasPermission(PermissionAction.view, PermissionTarget.documentType);
bool get canViewTags =>
hasPermission(PermissionAction.view, PermissionTarget.tag);
bool get canViewStoragePaths =>
hasPermission(PermissionAction.view, PermissionTarget.storagePath);
bool get canViewSavedViews =>
hasPermission(PermissionAction.view, PermissionTarget.savedView);
bool get canEditDocuments =>
hasPermission(PermissionAction.change, PermissionTarget.document);
bool get canEditCorrespondents =>
hasPermission(PermissionAction.change, PermissionTarget.correspondent);
bool get canEditDocumentTypes =>
hasPermission(PermissionAction.change, PermissionTarget.documentType);
bool get canEditTags =>
hasPermission(PermissionAction.change, PermissionTarget.tag);
bool get canEditStoragePaths =>
hasPermission(PermissionAction.change, PermissionTarget.storagePath);
bool get canEditavedViews =>
hasPermission(PermissionAction.change, PermissionTarget.savedView);
bool get canDeleteDocuments =>
hasPermission(PermissionAction.delete, PermissionTarget.document);
bool get canDeleteCorrespondents =>
hasPermission(PermissionAction.delete, PermissionTarget.correspondent);
bool get canDeleteDocumentTypes =>
hasPermission(PermissionAction.delete, PermissionTarget.documentType);
bool get canDeleteTags =>
hasPermission(PermissionAction.delete, PermissionTarget.tag);
bool get canDeleteStoragePaths =>
hasPermission(PermissionAction.delete, PermissionTarget.storagePath);
bool get canDeleteSavedViews =>
hasPermission(PermissionAction.delete, PermissionTarget.savedView);
bool get canCreateDocuments =>
hasPermission(PermissionAction.add, PermissionTarget.document);
bool get canCreateCorrespondents =>
hasPermission(PermissionAction.add, PermissionTarget.correspondent);
bool get canCreateDocumentTypes =>
hasPermission(PermissionAction.add, PermissionTarget.documentType);
bool get canCreateTags =>
hasPermission(PermissionAction.add, PermissionTarget.tag);
bool get canCreateStoragePaths =>
hasPermission(PermissionAction.add, PermissionTarget.storagePath);
bool get canCreateSavedViews =>
hasPermission(PermissionAction.add, PermissionTarget.savedView);
}

View File

@@ -1,4 +1,9 @@
import 'package:paperless_api/src/models/exception/exceptions.dart';
abstract class PaperlessAuthenticationApi {
///
/// @throws [PaperlessUnauthorizedException]
///
Future<String> login({
required String username,
required String password,

View File

@@ -1,6 +1,5 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/modules/authentication_api/authentication_api.dart';
class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
@@ -13,34 +12,20 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
required String username,
required String password,
}) async {
late Response response;
try {
response = await client.post(
final response = await client.post(
"/api/token/",
data: {
"username": username,
"password": password,
},
options: Options(
validateStatus: (status) => status == 200,
),
);
} on DioError catch (error) {
if (error.error is PaperlessServerException ||
error.error is Map<String, String>) {
throw error.error as Map<String, String>;
} else {
throw PaperlessServerException(
ErrorCode.authenticationFailed,
details: error.message,
);
}
}
if (response.statusCode == 200) {
return response.data['token'];
} else {
throw PaperlessServerException(
ErrorCode.authenticationFailed,
httpStatusCode: response.statusCode,
);
} on DioException catch (exception) {
throw exception.unravel();
}
}
}

View File

@@ -18,7 +18,7 @@ abstract class PaperlessDocumentsApi {
Future<DocumentModel> update(DocumentModel doc);
Future<int> findNextAsn();
Future<PagedSearchResult<DocumentModel>> findAll(DocumentFilter filter);
Future<DocumentModel?> find(int id);
Future<DocumentModel> find(int id);
Future<int> delete(DocumentModel doc);
Future<DocumentMetaData> getMetaData(DocumentModel document);
Future<Iterable<int>> bulkAction(BulkAction action);

View File

@@ -4,6 +4,8 @@ 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/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final Dio client;
@@ -55,20 +57,17 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
onSendProgress: (count, total) {
debugPrint("Uploading ${(count / total) * 100}%...");
},
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == 200) {
if (response.data is String && response.data != "OK") {
return response.data;
}
return null;
if (response.data != "OK") {
return response.data as String;
} else {
throw PaperlessServerException(
ErrorCode.documentUploadFailed,
httpStatusCode: response.statusCode,
);
return null;
}
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentUploadFailed),
);
}
}
@@ -78,14 +77,13 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final response = await client.put(
"/api/documents/${doc.id}/",
data: doc.toJson(),
options: Options(validateStatus: (status) => status == 200),
);
return DocumentModel.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentUpdateFailed),
);
if (response.statusCode == 200) {
return DocumentModel.fromJson(response.data);
} else {
throw const PaperlessServerException(ErrorCode.documentUpdateFailed);
}
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
}
}
@@ -93,39 +91,41 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
Future<PagedSearchResult<DocumentModel>> findAll(
DocumentFilter filter,
) async {
final filterParams = filter.toQueryParameters()..addAll({'truncate_content': "true"});
final filterParams = filter.toQueryParameters()
..addAll({'truncate_content': "true"});
try {
final response = await client.get(
"/api/documents/",
queryParameters: filterParams,
options: Options(validateStatus: (status) => status == 200),
);
return compute(
PagedSearchResult.fromJsonSingleParam,
PagedSearchResultJsonSerializer<DocumentModel>(
response.data,
DocumentModelJsonConverter(),
),
);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentLoadFailed),
);
if (response.statusCode == 200) {
return compute(
PagedSearchResult.fromJsonSingleParam,
PagedSearchResultJsonSerializer<DocumentModel>(
response.data,
DocumentModelJsonConverter(),
),
);
} else {
throw const PaperlessServerException(ErrorCode.documentLoadFailed);
}
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
}
}
@override
Future<int> delete(DocumentModel doc) async {
try {
final response = await client.delete("/api/documents/${doc.id}/");
await client.delete(
"/api/documents/${doc.id}/",
options: Options(validateStatus: (status) => status == 204),
);
if (response.statusCode == 204) {
return Future.value(doc.id);
}
throw const PaperlessServerException(ErrorCode.documentDeleteFailed);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
return Future.value(doc.id);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentDeleteFailed),
);
}
}
@@ -143,15 +143,16 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
try {
final response = await client.get(
getPreviewUrl(documentId),
options:
Options(responseType: ResponseType.bytes), //TODO: Check if bytes or stream is required
options: Options(
responseType: ResponseType.bytes,
validateStatus: (status) => status == 200,
), //TODO: Check if bytes or stream is required
);
return response.data;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentPreviewFailed),
);
if (response.statusCode == 200) {
return response.data;
}
throw const PaperlessServerException(ErrorCode.documentPreviewFailed);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
}
}
@@ -170,29 +171,30 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
.map((e) => e.archiveSerialNumber)
.firstWhere((asn) => asn != null, orElse: () => 0)! +
1;
} on PaperlessServerException {
throw const PaperlessServerException(ErrorCode.documentAsnQueryFailed);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on PaperlessApiException {
throw const PaperlessApiException(ErrorCode.documentAsnQueryFailed);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.documentAsnQueryFailed),
);
}
}
@override
Future<Iterable<int>> bulkAction(BulkAction action) async {
try {
final response = await client.post(
await client.post(
"/api/documents/bulk_edit/",
data: action.toJson(),
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == 200) {
return action.documentIds;
} else {
throw const PaperlessServerException(
return action.documentIds;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.documentBulkActionFailed,
);
}
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
),
);
}
}
@@ -208,8 +210,10 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
options: Options(responseType: ResponseType.bytes),
);
return response.data;
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException.unknown(),
);
}
}
@@ -224,25 +228,31 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final response = await client.download(
"/api/documents/${document.id}/download/",
localFilePath,
onReceiveProgress: (count, total) => onProgressChanged?.call(count / total),
onReceiveProgress: (count, total) =>
onProgressChanged?.call(count / total),
queryParameters: {'original': original},
);
return response.data;
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException.unknown(),
);
}
}
@override
Future<DocumentMetaData> getMetaData(DocumentModel document) async {
try {
final response = await client.get("/api/documents/${document.id}/metadata/");
final response =
await client.get("/api/documents/${document.id}/metadata/");
return compute(
DocumentMetaData.fromJson,
response.data as Map<String, dynamic>,
);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException.unknown(),
);
}
}
@@ -255,40 +265,46 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
'term': query,
'limit': limit,
},
options: Options(validateStatus: (status) => status == 200),
);
return (response.data as List).cast<String>();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.autocompleteQueryError,
),
);
if (response.statusCode == 200) {
return (response.data as List).cast<String>();
}
throw const PaperlessServerException(ErrorCode.autocompleteQueryError);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
}
}
@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).forDocumentId(document.id);
}
throw const PaperlessServerException(ErrorCode.suggestionsQueryError);
} on DioError catch (err) {
throw err.error ?? const PaperlessServerException.unknown();
final response = await client.get(
"/api/documents/${document.id}/suggestions/",
options: Options(validateStatus: (status) => status == 200),
);
return FieldSuggestions.fromJson(response.data)
.forDocumentId(document.id);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.suggestionsQueryError),
);
}
}
@override
Future<DocumentModel?> find(int id) async {
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 ?? const PaperlessServerException.unknown();
final response = await client.get(
"/api/documents/$id/",
options: Options(validateStatus: (status) => status == 200),
);
return DocumentModel.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException.unknown(),
);
}
}
}

View File

@@ -2,11 +2,12 @@ import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/labels/correspondent_model.dart';
import 'package:paperless_api/src/models/labels/document_type_model.dart';
import 'package:paperless_api/src/models/labels/storage_path_model.dart';
import 'package:paperless_api/src/models/labels/tag_model.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
import 'package:paperless_api/src/modules/labels_api/paperless_labels_api.dart';
import 'package:paperless_api/src/request_utils.dart';
@@ -94,16 +95,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.post(
'/api/correspondents/',
data: correspondent.toJson(),
options: Options(validateStatus: (status) => status == 201),
);
if (response.statusCode == HttpStatus.created) {
return Correspondent.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.correspondentCreateFailed,
httpStatusCode: response.statusCode,
return Correspondent.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.correspondentCreateFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -113,16 +113,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.post(
'/api/document_types/',
data: type.toJson(),
options: Options(
validateStatus: (status) => status == 201,
),
);
if (response.statusCode == HttpStatus.created) {
return DocumentType.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.documentTypeCreateFailed,
httpStatusCode: response.statusCode,
return DocumentType.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.documentTypeCreateFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -132,17 +133,18 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.post(
'/api/tags/',
data: tag.toJson(),
options: Options(headers: {"Accept": "application/json; version=2"}),
options: Options(
headers: {"Accept": "application/json; version=2"},
validateStatus: (status) => status == 201,
),
);
if (response.statusCode == HttpStatus.created) {
return Tag.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.tagCreateFailed,
httpStatusCode: response.statusCode,
return Tag.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.tagCreateFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -150,17 +152,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<int> deleteCorrespondent(Correspondent correspondent) async {
assert(correspondent.id != null);
try {
final response =
await _client.delete('/api/correspondents/${correspondent.id}/');
if (response.statusCode == HttpStatus.noContent) {
return correspondent.id!;
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
await _client.delete(
'/api/correspondents/${correspondent.id}/',
options: Options(validateStatus: (status) => status == 204),
);
return correspondent.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.correspondentDeleteFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -168,17 +170,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<int> deleteDocumentType(DocumentType documentType) async {
assert(documentType.id != null);
try {
final response =
await _client.delete('/api/document_types/${documentType.id}/');
if (response.statusCode == HttpStatus.noContent) {
return documentType.id!;
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
final response = await _client.delete(
'/api/document_types/${documentType.id}/',
options: Options(validateStatus: (status) => status == 204),
);
return documentType.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.documentTypeDeleteFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -186,16 +188,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<int> deleteTag(Tag tag) async {
assert(tag.id != null);
try {
final response = await _client.delete('/api/tags/${tag.id}/');
if (response.statusCode == HttpStatus.noContent) {
return tag.id!;
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
await _client.delete(
'/api/tags/${tag.id}/',
options: Options(validateStatus: (status) => status == 204),
);
return tag.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.tagDeleteFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -206,16 +209,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.put(
'/api/correspondents/${correspondent.id}/',
data: json.encode(correspondent.toJson()),
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == HttpStatus.ok) {
return Correspondent.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.unknown, //TODO: Add correct error code mapping.
httpStatusCode: response.statusCode,
return Correspondent.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.correspondentUpdateFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -226,16 +228,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.put(
'/api/document_types/${documentType.id}/',
data: documentType.toJson(),
options: Options(validateStatus: (status) => status == 200),
);
if (response.statusCode == HttpStatus.ok) {
return DocumentType.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
return DocumentType.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.documentTypeUpdateFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -245,18 +246,19 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
try {
final response = await _client.put(
'/api/tags/${tag.id}/',
options: Options(headers: {"Accept": "application/json; version=2"}),
options: Options(
headers: {"Accept": "application/json; version=2"},
validateStatus: (status) => status == 200,
),
data: tag.toJson(),
);
if (response.statusCode == HttpStatus.ok) {
return Tag.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
return Tag.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.tagUpdateFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -264,16 +266,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<int> deleteStoragePath(StoragePath path) async {
assert(path.id != null);
try {
final response = await _client.delete('/api/storage_paths/${path.id}/');
if (response.statusCode == HttpStatus.noContent) {
return path.id!;
}
throw PaperlessServerException(
ErrorCode.unknown,
httpStatusCode: response.statusCode,
final response = await _client.delete(
'/api/storage_paths/${path.id}/',
options: Options(validateStatus: (status) => status == 204),
);
return path.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.storagePathDeleteFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -307,16 +310,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.post(
'/api/storage_paths/',
data: path.toJson(),
options: Options(validateStatus: (status) => status == 201),
);
if (response.statusCode == HttpStatus.created) {
return StoragePath.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.storagePathCreateFailed,
httpStatusCode: response.statusCode,
return StoragePath.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.storagePathCreateFailed,
),
);
} on DioError catch (err) {
throw err.error!;
}
}
@@ -327,13 +329,15 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final response = await _client.put(
'/api/storage_paths/${path.id}/',
data: path.toJson(),
options: Options(validateStatus: (status) => status == 200),
);
return StoragePath.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.storagePathUpdateFailed,
),
);
if (response.statusCode == HttpStatus.ok) {
return StoragePath.fromJson(response.data);
}
throw const PaperlessServerException(ErrorCode.unknown);
} on DioError catch (err) {
throw err.error!;
}
}
}

View File

@@ -1,7 +1,8 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
import 'package:paperless_api/src/models/saved_view_model.dart';
import 'package:paperless_api/src/request_utils.dart';
@@ -30,32 +31,28 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
final response = await _client.post(
"/api/saved_views/",
data: view.toJson(),
options: Options(validateStatus: (status) => status == 201),
);
if (response.statusCode == HttpStatus.created) {
return SavedView.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.createSavedViewError,
httpStatusCode: response.statusCode,
return SavedView.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.createSavedViewError),
);
} on DioError catch (err) {
throw err.error!;
}
}
@override
Future<int> delete(SavedView view) async {
try {
final response = await _client.delete("/api/saved_views/${view.id}/");
if (response.statusCode == HttpStatus.noContent) {
return view.id!;
}
throw PaperlessServerException(
ErrorCode.deleteSavedViewError,
httpStatusCode: response.statusCode,
await _client.delete(
"/api/saved_views/${view.id}/",
options: Options(validateStatus: (status) => status == 204),
);
return view.id!;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.deleteSavedViewError),
);
} on DioError catch (err) {
throw err.error!;
}
}

View File

@@ -1,5 +1,6 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
import 'package:paperless_api/src/models/paperless_server_information_model.dart';
import 'package:paperless_api/src/models/paperless_server_statistics_model.dart';
import 'package:paperless_api/src/models/paperless_ui_settings_model.dart';
@@ -18,8 +19,11 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
@override
Future<PaperlessServerInformationModel> getServerInformation() async {
final response = await client.get("/api/remote_version/");
if (response.statusCode == 200) {
try {
final response = await client.get(
"/api/remote_version/",
options: Options(validateStatus: (status) => status == 200),
);
final version = response.data["version"] as String;
final updateAvailable = response.data["update_available"] as bool;
return PaperlessServerInformationModel(
@@ -27,25 +31,44 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
version: version,
isUpdateAvailable: updateAvailable,
);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.serverInformationLoadFailed,
),
);
}
throw const PaperlessServerException.unknown();
}
@override
Future<PaperlessServerStatisticsModel> getServerStatistics() async {
final response = await client.get('/api/statistics/');
if (response.statusCode == 200) {
try {
final response = await client.get(
'/api/statistics/',
options: Options(validateStatus: (status) => status == 200),
);
return PaperlessServerStatisticsModel.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(
ErrorCode.serverStatisticsLoadFailed,
),
);
}
throw const PaperlessServerException.unknown();
}
@override
Future<PaperlessUiSettingsModel> getUiSettings() async {
final response = await client.get("/api/ui_settings/");
if (response.statusCode == 200) {
try {
final response = await client.get(
"/api/ui_settings/",
options: Options(validateStatus: (status) => status == 200),
);
return PaperlessUiSettingsModel.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.uiSettingsLoadFailed),
);
}
throw const PaperlessServerException.unknown();
}
}

View File

@@ -2,6 +2,8 @@ import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
class PaperlessTasksApiImpl implements PaperlessTasksApi {
final Dio _client;
@@ -41,11 +43,17 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi {
@override
Future<Iterable<Task>> findAll([Iterable<int>? ids]) async {
final response = await _client.get("/api/tasks/");
if (response.statusCode == 200) {
try {
final response = await _client.get(
"/api/tasks/",
options: Options(validateStatus: (status) => status == 200),
);
return (response.data as List).map((e) => Task.fromJson(e));
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.loadTasksError),
);
}
return [];
}
@override
@@ -74,15 +82,22 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi {
@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) {
try {
final response = await _client.post(
"/api/acknowledge_tasks/",
data: {
'tasks': tasks.map((e) => e.id).toList(),
},
options: Options(validateStatus: (status) => status == 200),
);
if (response.data['result'] != tasks.length) {
throw const PaperlessServerException(ErrorCode.acknowledgeTasksError);
throw const PaperlessApiException(ErrorCode.acknowledgeTasksError);
}
return tasks.map((e) => e.copyWith(acknowledged: true)).toList();
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.acknowledgeTasksError),
);
}
throw const PaperlessServerException(ErrorCode.acknowledgeTasksError);
}
}

View File

@@ -1,5 +1,7 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
class PaperlessUserApiV2Impl implements PaperlessUserApi {
final Dio client;
@@ -8,19 +10,33 @@ class PaperlessUserApiV2Impl implements PaperlessUserApi {
@override
Future<int> findCurrentUserId() async {
final response = await client.get("/api/ui_settings/");
if (response.statusCode == 200) {
try {
final response = await client.get(
"/api/ui_settings/",
options: Options(
validateStatus: (status) => status == 200,
),
);
return response.data['user_id'];
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
@override
Future<UserModel> findCurrentUser() async {
final response = await client.get("/api/ui_settings/");
if (response.statusCode == 200) {
try {
final response = await client.get(
"/api/ui_settings/",
options: Options(validateStatus: (status) => status == 200),
);
return UserModelV2.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
}

View File

@@ -1,5 +1,7 @@
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 {
final Dio dio;
@@ -8,11 +10,17 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 {
@override
Future<UserModelV3> find(int id) async {
final response = await dio.get("/api/users/$id/");
if (response.statusCode == 200) {
try {
final response = await dio.get(
"/api/users/$id/",
options: Options(validateStatus: (status) => status == 200),
);
return UserModelV3.fromJson(response.data);
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
@override
@@ -22,40 +30,59 @@ class PaperlessUserApiV3Impl implements PaperlessUserApi, PaperlessUserApiV3 {
String contains = '',
String username = '',
}) async {
final response = await dio.get("/api/users/", queryParameters: {
"username__istartswith": startsWith,
"username__iendswith": endsWith,
"username__icontains": contains,
"username__iexact": username,
});
if (response.statusCode == 200) {
try {
final response = await dio.get(
"/api/users/",
queryParameters: {
"username__istartswith": startsWith,
"username__iendswith": endsWith,
"username__icontains": contains,
"username__iexact": username,
},
options: Options(validateStatus: (status) => status == 200),
);
return PagedSearchResult<UserModelV3>.fromJson(
response.data,
UserModelV3.fromJson as UserModelV3 Function(Object?),
).results;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
@override
Future<int> findCurrentUserId() async {
final response = await dio.get("/api/ui_settings/");
if (response.statusCode == 200) {
try {
final response = await dio.get(
"/api/ui_settings/",
options: Options(validateStatus: (status) => status == 200),
);
return response.data['user']['id'];
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
@override
Future<Iterable<UserModelV3>> findAll() async {
final response = await dio.get("/api/users/");
if (response.statusCode == 200) {
try {
final response = await dio.get(
"/api/users/",
options: Options(validateStatus: (status) => status == 200),
);
return PagedSearchResult<UserModelV3>.fromJson(
response.data,
(json) => UserModelV3.fromJson(json as dynamic),
).results;
} on DioException catch (exception) {
throw exception.unravel(
orElse: const PaperlessApiException(ErrorCode.userNotFound),
);
}
throw const PaperlessServerException.unknown();
}
@override

View File

@@ -2,7 +2,8 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/extensions/dio_exception_extension.dart';
import 'package:paperless_api/src/models/paperless_api_exception.dart';
Future<T?> getSingleResult<T>(
String url,
@@ -16,20 +17,15 @@ Future<T?> getSingleResult<T>(
url,
options: Options(
headers: {'accept': 'application/json; version=$minRequiredApiVersion'},
validateStatus: (status) => status == 200,
),
);
if (response.statusCode == HttpStatus.ok) {
return compute(
fromJson,
response.data as Map<String, dynamic>,
);
}
throw PaperlessServerException(
errorCode,
httpStatusCode: response.statusCode,
return compute(
fromJson,
response.data as Map<String, dynamic>,
);
} on DioError catch (err) {
throw err.error!;
} on DioException catch (exception) {
throw exception.unravel(orElse: PaperlessApiException(errorCode));
}
}
@@ -43,30 +39,25 @@ Future<List<T>> getCollection<T>(
try {
final response = await client.get(
url,
options: Options(headers: {
'accept': 'application/json; version=$minRequiredApiVersion'
}),
options: Options(
headers: {'accept': 'application/json; version=$minRequiredApiVersion'},
validateStatus: (status) => status == 200,
),
);
if (response.statusCode == HttpStatus.ok) {
final Map<String, dynamic> body = response.data;
if (body.containsKey('count')) {
if (body['count'] == 0) {
return <T>[];
} else {
return compute(
_collectionFromJson,
_CollectionFromJsonSerializationParams(fromJson,
(body['results'] as List).cast<Map<String, dynamic>>()),
);
}
}
final Map<String, dynamic> body = response.data;
if (body['count'] == 0) {
return <T>[];
} else {
return compute(
_collectionFromJson,
_CollectionFromJsonSerializationParams(
fromJson,
(body['results'] as List).cast<Map<String, dynamic>>(),
),
);
}
throw PaperlessServerException(
errorCode,
httpStatusCode: response.statusCode,
);
} on DioError catch (err) {
throw err.error!;
} on DioException catch (exception) {
throw exception.unravel(orElse: PaperlessApiException(errorCode));
}
}