From 36db77cf89ab9c433ca1e36dde3fd35ee24156ce Mon Sep 17 00:00:00 2001 From: Moritz Pflanzer Date: Sat, 30 Dec 2023 11:54:19 +0100 Subject: [PATCH] Add API for django messages --- .../app-frame/app-frame.component.spec.ts | 19 +++++++ .../app-frame/app-frame.component.ts | 23 +++++++- .../src/app/services/messages.service.spec.ts | 35 ++++++++++++ src-ui/src/app/services/messages.service.ts | 25 +++++++++ src/documents/tests/test_api_messages.py | 55 +++++++++++++++++++ src/paperless/urls.py | 2 + src/paperless/views.py | 16 ++++++ 7 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 src-ui/src/app/services/messages.service.spec.ts create mode 100644 src-ui/src/app/services/messages.service.ts create mode 100644 src/documents/tests/test_api_messages.py diff --git a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts index 64877bb09..b9e036f46 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts @@ -21,6 +21,7 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { of, throwError } from 'rxjs' import { ToastService } from 'src/app/services/toast.service' +import { MessagesService } from 'src/app/services/messages.service' import { environment } from 'src/environments/environment' import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { ActivatedRoute, Router } from '@angular/router' @@ -83,6 +84,7 @@ describe('AppFrameComponent', () => { let permissionsService: PermissionsService let remoteVersionService: RemoteVersionService let toastService: ToastService + let messagesService: MessagesService let openDocumentsService: OpenDocumentsService let searchService: SearchService let documentListViewService: DocumentListViewService @@ -123,6 +125,7 @@ describe('AppFrameComponent', () => { RemoteVersionService, IfPermissionsDirective, ToastService, + MessagesService, OpenDocumentsService, SearchService, NgbModal, @@ -151,6 +154,7 @@ describe('AppFrameComponent', () => { permissionsService = TestBed.inject(PermissionsService) remoteVersionService = TestBed.inject(RemoteVersionService) toastService = TestBed.inject(ToastService) + messagesService = TestBed.inject(MessagesService) openDocumentsService = TestBed.inject(OpenDocumentsService) searchService = TestBed.inject(SearchService) documentListViewService = TestBed.inject(DocumentListViewService) @@ -393,4 +397,19 @@ describe('AppFrameComponent', () => { backdrop: 'static', }) }) + + it('should show toasts for django messages', () => { + const toastErrorSpy = jest.spyOn(toastService, 'showError') + const toastInfoSpy = jest.spyOn(toastService, 'showInfo') + jest.spyOn(messagesService, 'get').mockReturnValue( + of([ + { level: 'error', message: 'Test error', tags: '' }, + { level: 'info', message: 'Test info', tags: '' }, + ]) + ) + component.ngOnInit() + httpTestingController.expectOne(`${environment.apiBaseUrl}messages/`) + expect(toastErrorSpy).toHaveBeenCalled() + expect(toastInfoSpy).toHaveBeenCalled() + }) }) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.ts b/src-ui/src/app/components/app-frame/app-frame.component.ts index cfc9740a4..60008bad5 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.ts @@ -12,6 +12,7 @@ import { } from 'rxjs/operators' import { Document } from 'src/app/data/document' import { OpenDocumentsService } from 'src/app/services/open-documents.service' +import { MessagesService } from 'src/app/services/messages.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SearchService } from 'src/app/services/rest/search.service' import { environment } from 'src/environments/environment' @@ -73,7 +74,8 @@ export class AppFrameComponent public tasksService: TasksService, private readonly toastService: ToastService, private modalService: NgbModal, - permissionsService: PermissionsService + permissionsService: PermissionsService, + private messagesService: MessagesService ) { super() @@ -92,6 +94,25 @@ export class AppFrameComponent this.checkForUpdates() } this.tasksService.reload() + + this.messagesService + .get() + .pipe(first()) + .subscribe((msgs) => { + for (const m of msgs) { + switch (m.level) { + case 'error': + case 'warning': + this.toastService.showError(m.message) + break + case 'success': + case 'info': + case 'debug': + this.toastService.showInfo(m.message) + break + } + } + }) } toggleSlimSidebar(): void { diff --git a/src-ui/src/app/services/messages.service.spec.ts b/src-ui/src/app/services/messages.service.spec.ts new file mode 100644 index 000000000..8830c41e7 --- /dev/null +++ b/src-ui/src/app/services/messages.service.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing' + +import { MessagesService } from './messages.service' + +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing' +import { environment } from 'src/environments/environment' + +describe('MessagesService', () => { + let httpTestingController: HttpTestingController + let service: MessagesService + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [MessagesService], + imports: [HttpClientTestingModule], + }) + httpTestingController = TestBed.inject(HttpTestingController) + service = TestBed.inject(MessagesService) + }) + + afterEach(() => { + httpTestingController.verify() + }) + + it('calls get profile endpoint', () => { + service.get().subscribe() + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}messages/` + ) + expect(req.request.method).toEqual('GET') + }) +}) diff --git a/src-ui/src/app/services/messages.service.ts b/src-ui/src/app/services/messages.service.ts new file mode 100644 index 000000000..8177f52c7 --- /dev/null +++ b/src-ui/src/app/services/messages.service.ts @@ -0,0 +1,25 @@ +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { Observable } from 'rxjs' +import { environment } from 'src/environments/environment' + +export interface DjangoMessage { + level: string + message: string + tags: string +} + +@Injectable({ + providedIn: 'root', +}) +export class MessagesService { + private endpoint = 'messages' + + constructor(private http: HttpClient) {} + + get(): Observable { + return this.http.get( + `${environment.apiBaseUrl}${this.endpoint}/` + ) + } +} diff --git a/src/documents/tests/test_api_messages.py b/src/documents/tests/test_api_messages.py new file mode 100644 index 000000000..82d2390fb --- /dev/null +++ b/src/documents/tests/test_api_messages.py @@ -0,0 +1,55 @@ +from django.contrib import messages +from django.contrib.auth.models import User +from django.contrib.messages.storage.fallback import FallbackStorage +from django.test import RequestFactory +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.tests.utils import DirectoriesMixin +from paperless.views import MessagesView + + +class TestApiMessages(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/messages/" + + def setUp(self): + super().setUp() + + self.user = User.objects.create_superuser( + username="temp_admin", + first_name="firstname", + last_name="surname", + ) + self.client.force_authenticate(user=self.user) + + def test_get_django_messages(self): + """ + GIVEN: + - Configured user + - Pending django message + WHEN: + - API call is made to get the django messages + THEN: + - Pending message is returned + - No more messages are pending + """ + + factory = RequestFactory() + + request = factory.get(self.ENDPOINT) + request.user = self.user + + # Fake middleware support for RequestFactory + # See https://stackoverflow.com/a/66473588/1022690 + setattr(request, "session", "session") + setattr(request, "_messages", FallbackStorage(request)) + + msg = "Test message" + messages.error(request, msg) + + response = MessagesView.as_view()(request) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]["level"], "error") + self.assertEqual(response.data[0]["message"], msg) diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 74f6fc108..4d269fcaf 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -45,6 +45,7 @@ from paperless.views import DisconnectSocialAccountView from paperless.views import FaviconView from paperless.views import GenerateAuthTokenView from paperless.views import GroupViewSet +from paperless.views import MessagesView from paperless.views import ProfileView from paperless.views import SocialAccountProvidersView from paperless.views import UserViewSet @@ -147,6 +148,7 @@ urlpatterns = [ ProfileView.as_view(), name="profile_view", ), + path("messages/", MessagesView.as_view()), *api_router.urls, ], ), diff --git a/src/paperless/views.py b/src/paperless/views.py index 0510cce59..bf9fa8b07 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -2,6 +2,7 @@ import os from collections import OrderedDict from allauth.socialaccount.adapter import get_adapter +from django.contrib import messages from django.contrib.auth.models import Group from django.contrib.auth.models import User from django.db.models.functions import Lower @@ -222,3 +223,18 @@ class SocialAccountProvidersView(APIView): ] return Response(sorted(resp, key=lambda p: p["name"])) + + +class MessagesView(APIView): + """ + Expose django messages. This clears the messages in django + """ + + permission_classes = [IsAuthenticated] + + def get(self, request, *args, **kwargs): + data = [ + {"level": m.level_tag, "message": m.message, "tags": m.extra_tags} + for m in messages.get_messages(request) + ] + return Response(data)