diff --git a/plinth/tests/test_views.py b/plinth/tests/test_views.py index 87dcfc8b9..e0092ebc0 100644 --- a/plinth/tests/test_views.py +++ b/plinth/tests/test_views.py @@ -3,10 +3,13 @@ Tests for common FreedomBox views. """ +import json + import pytest +from django.http import JsonResponse from django.urls import resolve -from plinth.views import get_breadcrumbs, is_safe_url +from plinth.views import get_breadcrumbs, is_safe_url, json_exception def test_get_breadcrumbs(rf, test_menu): @@ -82,3 +85,31 @@ def test_is_safe_url_valid_url(url): def test_is_safe_url_invalid_url(url): """Test invalid URLs for safe URL checks.""" assert not is_safe_url(url) + + +def test_json_exception(rf): + """Test handling exceptions in JSON views.""" + + @json_exception + def error_view(request): + raise Exception('exception-message') + + assert error_view.__name__ == 'error_view' + + request = rf.get('') + response = error_view(request) + assert response.status_code == 500 + json_content = json.loads(response.content) + assert not json_content['result'] + assert json_content['error_name'] == 'Exception' + assert json_content['error_string'] == 'exception-message' + + def success_view(request): + return JsonResponse({'result': True, 'error_string': None}) + + request = rf.get('') + response = success_view(request) + assert response.status_code == 200 + json_content = json.loads(response.content) + assert json_content['result'] + assert json_content['error_string'] is None diff --git a/plinth/views.py b/plinth/views.py index ca6bdd380..76d2c2c3f 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -3,10 +3,13 @@ Main FreedomBox views. """ +import functools +import logging import random import time import traceback import urllib.parse +from collections.abc import Callable from django.contrib import messages from django.core.exceptions import ImproperlyConfigured @@ -33,6 +36,8 @@ from plinth.translation import get_language_from_request, set_language from . import forms, frontpage, operation, setup +logger = logging.getLogger(__name__) + REDIRECT_FIELD_NAME = 'next' @@ -683,6 +688,7 @@ class AppLogsView(TemplateView): def notification_dismiss(request, id): """Dismiss a notification.""" from django.http import HttpResponse + from .notification import Notification notes = Notification.list(key=id, user=request.user) if notes: @@ -697,3 +703,24 @@ def notification_dismiss(request, id): return response return HttpResponseRedirect(_get_redirect_url_from_param(request)) + + +def json_exception(view_func: Callable): + """View decorator to return an view exception in JSON format.""" + + @functools.wraps(view_func) + def wrapper(*args, **kwargs): + """Wrapper over view function to handle exceptions.""" + try: + return view_func(*args, **kwargs) + except Exception as exception: + logger.exception('Error running view %s - %s', view_func.__name__, + exception) + return JsonResponse( + { + 'result': False, + 'error_name': type(exception).__name__, + 'error_string': str(exception) + }, status=500) # Internal server error + + return wrapper