Pass Django messages to frontend without API endpoint

This commit is contained in:
shamoon 2024-01-05 10:48:12 -08:00 committed by Moritz Pflanzer
parent 24ec256580
commit 93750663aa
11 changed files with 67 additions and 134 deletions

View File

@ -21,7 +21,10 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { MessagesService } from 'src/app/services/messages.service' import {
DjangoMessageLevel,
MessagesService,
} from 'src/app/services/messages.service'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
@ -401,17 +404,14 @@ describe('AppFrameComponent', () => {
it('should show toasts for django messages', () => { it('should show toasts for django messages', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError') const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
jest.spyOn(messagesService, 'get').mockReturnValue( jest.spyOn(messagesService, 'get').mockReturnValue([
of([ { level: DjangoMessageLevel.WARNING, message: 'Test warning' },
{ level: 'warning', message: 'Test warning', tags: '' }, { level: DjangoMessageLevel.ERROR, message: 'Test error' },
{ level: 'error', message: 'Test error', tags: '' }, { level: DjangoMessageLevel.SUCCESS, message: 'Test success' },
{ level: 'success', message: 'Test success', tags: '' }, { level: DjangoMessageLevel.INFO, message: 'Test info' },
{ level: 'info', message: 'Test info', tags: '' }, { level: DjangoMessageLevel.DEBUG, message: 'Test debug' },
{ level: 'debug', message: 'Test debug', tags: '' }, ])
])
)
component.ngOnInit() component.ngOnInit()
httpTestingController.expectOne(`${environment.apiBaseUrl}messages/`)
expect(toastErrorSpy).toHaveBeenCalledTimes(2) expect(toastErrorSpy).toHaveBeenCalledTimes(2)
expect(toastInfoSpy).toHaveBeenCalledTimes(3) expect(toastInfoSpy).toHaveBeenCalledTimes(3)
}) })

View File

@ -12,7 +12,10 @@ import {
} from 'rxjs/operators' } from 'rxjs/operators'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { MessagesService } from 'src/app/services/messages.service' import {
DjangoMessageLevel,
MessagesService,
} from 'src/app/services/messages.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SearchService } from 'src/app/services/rest/search.service' import { SearchService } from 'src/app/services/rest/search.service'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
@ -95,24 +98,19 @@ export class AppFrameComponent
} }
this.tasksService.reload() this.tasksService.reload()
this.messagesService this.messagesService.get().forEach((message) => {
.get() switch (message.level) {
.pipe(first()) case DjangoMessageLevel.ERROR:
.subscribe((msgs) => { case DjangoMessageLevel.WARNING:
for (const m of msgs) { this.toastService.showError(message.message)
switch (m.level) { break
case 'error': case DjangoMessageLevel.SUCCESS:
case 'warning': case DjangoMessageLevel.INFO:
this.toastService.showError(m.message) case DjangoMessageLevel.DEBUG:
break this.toastService.showInfo(message.message)
case 'success': break
case 'info': }
case 'debug': })
this.toastService.showInfo(m.message)
break
}
}
})
} }
toggleSlimSidebar(): void { toggleSlimSidebar(): void {

View File

@ -1,35 +1,29 @@
import { TestBed } from '@angular/core/testing' import { TestBed } from '@angular/core/testing'
import { MessagesService } from './messages.service' import { DjangoMessageLevel, MessagesService } from './messages.service'
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
const messages = [
{ level: DjangoMessageLevel.ERROR, message: 'Error Message' },
{ level: DjangoMessageLevel.INFO, message: 'Info Message' },
]
describe('MessagesService', () => { describe('MessagesService', () => {
let httpTestingController: HttpTestingController
let service: MessagesService let service: MessagesService
beforeEach(() => { beforeEach(() => {
window['DJANGO_MESSAGES'] = messages
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [MessagesService], providers: [MessagesService],
imports: [HttpClientTestingModule],
}) })
httpTestingController = TestBed.inject(HttpTestingController)
service = TestBed.inject(MessagesService) service = TestBed.inject(MessagesService)
}) })
afterEach(() => { it('calls retrieves global django messages if present', () => {
httpTestingController.verify() expect(service.get()).toEqual(messages)
})
it('calls messages endpoint', () => { window['DJANGO_MESSAGES'] = undefined
service.get().subscribe() expect(service.get()).toEqual([])
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}messages/`
)
expect(req.request.method).toEqual('GET')
}) })
}) })

View File

@ -1,25 +1,27 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { environment } from 'src/environments/environment' // see https://docs.djangoproject.com/en/5.0/ref/contrib/messages/#message-tags
export enum DjangoMessageLevel {
DEBUG = 'debug',
INFO = 'info',
SUCCESS = 'success',
WARNING = 'warning',
ERROR = 'error',
}
export interface DjangoMessage { export interface DjangoMessage {
level: string level: DjangoMessageLevel
message: string message: string
tags: string
} }
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class MessagesService { export class MessagesService {
private endpoint = 'messages' constructor() {}
constructor(private http: HttpClient) {} get(): DjangoMessage[] {
// These are embedded in the HTML as raw JS, kept as a service for convenience
get(): Observable<DjangoMessage[]> { return window['DJANGO_MESSAGES'] ?? []
return this.http.get<DjangoMessage[]>(
`${environment.apiBaseUrl}${this.endpoint}/`
)
} }
} }

View File

@ -39,6 +39,11 @@
<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/> <path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
</g> </g>
</svg> </svg>
{% for message in messages %}
<div class="alert alert-{{ message.level_tag }}" role="alert">
{{ message }}
</div>
{% endfor %}
<p>{% translate "Please sign in." %}</p> <p>{% translate "Please sign in." %}</p>
{% if form.errors %} {% if form.errors %}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">

View File

@ -80,6 +80,13 @@
<p class="warning m-auto mt-3 small fade hide">{% translate "Still here?! Hmm, something might be wrong." %} <a href="https://docs.paperless-ngx.com">{% translate "Here's a link to the docs." %}</a></p> <p class="warning m-auto mt-3 small fade hide">{% translate "Still here?! Hmm, something might be wrong." %} <a href="https://docs.paperless-ngx.com">{% translate "Here's a link to the docs." %}</a></p>
</div> </div>
</div> </div>
<script type="text/javascript">{# Pass Django messages to Angular frontend #}
window.DJANGO_MESSAGES = [
{% for message in messages %}
{ level: "{{ message.level_tag | escapejs }}", message: "{{ message | escapejs }}" },
{% endfor %}
]
</script>
</pngx-root> </pngx-root>
<script src="{% static runtime_js %}" defer></script> <script src="{% static runtime_js %}" defer></script>
<script src="{% static polyfills_js %}" defer></script> <script src="{% static polyfills_js %}" defer></script>

View File

@ -12,7 +12,7 @@
<meta name="author" content="Paperless-ngx project and contributors"> <meta name="author" content="Paperless-ngx project and contributors">
<meta name="robots" content="noindex,nofollow"> <meta name="robots" content="noindex,nofollow">
<title>{% translate "Paperless-ngx sign in" %}</title> <title>{% translate "Paperless-ngx social account sign in" %}</title>
<link href="{% static 'bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'signin.css' %}" rel="stylesheet"> <link href="{% static 'signin.css' %}" rel="stylesheet">

View File

@ -11,7 +11,7 @@
<meta name="author" content="Paperless-ngx project and contributors"> <meta name="author" content="Paperless-ngx project and contributors">
<meta name="robots" content="noindex,nofollow"> <meta name="robots" content="noindex,nofollow">
<title>{% translate "Paperless-ngx sign up" %}</title> <title>{% translate "Paperless-ngx social account sign up" %}</title>
<link href="{% static 'bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'signin.css' %}" rel="stylesheet"> <link href="{% static 'signin.css' %}" rel="stylesheet">

View File

@ -1,55 +0,0 @@
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)

View File

@ -45,7 +45,6 @@ from paperless.views import DisconnectSocialAccountView
from paperless.views import FaviconView from paperless.views import FaviconView
from paperless.views import GenerateAuthTokenView from paperless.views import GenerateAuthTokenView
from paperless.views import GroupViewSet from paperless.views import GroupViewSet
from paperless.views import MessagesView
from paperless.views import ProfileView from paperless.views import ProfileView
from paperless.views import SocialAccountProvidersView from paperless.views import SocialAccountProvidersView
from paperless.views import UserViewSet from paperless.views import UserViewSet
@ -148,7 +147,6 @@ urlpatterns = [
ProfileView.as_view(), ProfileView.as_view(),
name="profile_view", name="profile_view",
), ),
path("messages/", MessagesView.as_view()),
*api_router.urls, *api_router.urls,
], ],
), ),

View File

@ -3,7 +3,6 @@ from collections import OrderedDict
from allauth.socialaccount.adapter import get_adapter from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.models import SocialAccount from allauth.socialaccount.models import SocialAccount
from django.contrib import messages
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models.functions import Lower from django.db.models.functions import Lower
@ -227,18 +226,3 @@ class SocialAccountProvidersView(APIView):
] ]
return Response(sorted(resp, key=lambda p: p["name"])) 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)