Browse Source

Error handler boilerplate WIP.

Rafał Pitoń 11 years ago
parent
commit
4d9c9fa952

+ 0 - 29
docs/exceptions.rst

@@ -1,29 +0,0 @@
-=================
-Misago Exceptions
-=================
-
-In addition to exceptions defined by `Django <https://docs.djangoproject.com/en/dev/ref/exceptions/>`_ and standard Python exceptions, Misago raises its own exceptions.
-
-All Misago exceptions can be found in :py:mod:`misago.core.exceptions` module.
-
-
-PermissionDenied
-----------------
-
-Misago's Http404 exception subclasses `Django PermissionDenied <https://docs.djangoproject.com/en/dev/ref/exceptions/#django.core.exceptions.PermissionDenied>`_, but changes nothing about its implementation and exists simply to delegate 403 error handling to Misago's exceptions handler.
-
-
-Http404
--------
-
-Misago's Http404 exception subclasses `Django Http404 <https://docs.djangoproject.com/en/dev/topics/http/views/#the-http404-exception>`_, but changes nothing about its implementation and exists simply to delegate 404 error handling to Misago's exceptions handler.
-
-
-OutdatedUrl
-------------
-
-OutdatedLink exception is special "message" that tells Misago to return `permanent <http://en.wikipedia.org/wiki/HTTP_301>`_ redirection as response instead of intended view.
-
-This exception is raised by view utility that compares link's "slug" part against one from database. If check fails OutdatedLink exception is raised with parameter name and valid slug as message that Misago's exception handler then uses to construct redirection response to valid link.
-
-You should never raise this exception yourself, instead always return proper redirect response from your code.

+ 1 - 1
docs/index.rst

@@ -66,4 +66,4 @@ Following references cover everything you want to know about writing your own ap
    :maxdepth: 1
    :maxdepth: 1
 
 
    coding_style
    coding_style
-   exceptions
+   views_errors

+ 60 - 0
docs/views_errors.rst

@@ -0,0 +1,60 @@
+========================
+Views Errors Boilerplate
+========================
+
+Modern forum software is busy place where access content is decided by many factors. This means that your users may frequently be trying to follow links that has been outdated, deleted or simply restricted and each of those scenarios must be handled by your views.
+
+While Django provides `plenty of approaches <https://docs.djangoproject.com/en/dev/topics/http/views/#returning-errors>`_ to handling those situations, those can hardly be named fit for internet forum usecases. For example, you may want to communicate to your user a reason why he is not able to reply in selected thread at the moment. This brings need for custom error handling in your code.
+
+And what to do if user reached page using outdated link? You will have to compare link to model's slug field and return 301 redirect to valid address on every view that has friendly link.
+
+To solve this problem you would have to write custom error views and handlers that then you would have to add in every view that needs it. Depending on number of views you are writing, number of lines would quickly add up becoming annoying boilerplate.
+
+Misago views too have to solve this problem and this reason is why error handling boilerplate is part of framework.
+
+
+Http404
+=======
+
+:py:class:`misago.views.exceptions.Http404`
+
+Misago's Http404 exception subclasses `Django Http404 <https://docs.djangoproject.com/en/dev/topics/http/views/#the-http404-exception>`_, but changes nothing about its implementation and exists simply to delegate 404 error handling to Misago's exceptions handler.
+
+Raise this exception if requested page does not exists or user's permission set shows that he shouldn't know if it exists. If you want to, you can insert custom error message into exception's constructor to be displayed by error page instead of default one.
+
+
+   raise Http404("Requested thread could not be found. Perhaps it was moved or deleted?")
+
+
+OutdatedUrl
+===========
+
+:py:class:`misago.views.exceptions.OutdatedUrl`
+
+OutdatedUrl exception is special "message" that tells Misago to return `permanent <http://en.wikipedia.org/wiki/HTTP_301>`_ redirection as response instead of intended view.
+
+This exception is raised by view utility that compares link's "slug" part against one from database. If check fails OutdatedUrl exception is raised with parameter name and valid slug as message that Misago's exception handler then uses to construct redirection response to valid link.
+
+You should never raise this exception yourself, instead always return proper redirect response from your code.
+
+
+PermissionDenied
+================
+
+:py:class:`misago.views.exceptions.PermissionDenied`
+
+Misago's PermissionDenied exception subclasses `Django PermissionDenied <https://docs.djangoproject.com/en/dev/ref/exceptions/#django.core.exceptions.PermissionDenied>`_, but changes nothing about its implementation and exists simply to delegate 403 error handling to Misago's exceptions handler.
+
+Raise this exception if user knows of requested page or action but has no permission to access or perform it. If you want to, you can insert custom error message into exception's constructor to be displayed by error page instead of default one.
+
+    raise PermissionDenied("This thread is locked. You can't reply to it.")
+
+
+Exception Handler
+=================
+
+:py:mod:`misago.views.exceptions.handler`
+
+Exception handler is lightweight system that pairs exceptions with special "handler" functions that turn those exceptions into valid HTTP responses that are then served back to client.
+
+This system has been designed exlusively for handling exceptions listed in this document and is was not intended to be universal and extensible solution. If you need special handling for your own exception, depending on how wide is its usage, consider writing custom exception handler decorator or `middleware <https://docs.djangoproject.com/en/dev/topics/http/middleware/#process-exception>`_ for it.

+ 1 - 0
misago/conf/defaults.py

@@ -52,6 +52,7 @@ MIDDLEWARE_CLASSES = (
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'misago.views.exceptions.middleware.MisagoExceptionHandlerMiddleware',
 )
 )
 
 
 # Register Misago directories
 # Register Misago directories

+ 0 - 12
misago/core/exceptionhandler.py

@@ -1,12 +0,0 @@
-from misago.core import exceptions
-
-
-MISAGO_EXCEPTIONS = (
-    exceptions.PermissionDenied,
-    exceptions.Http404,
-    exceptions.OutdatedUrl,
-)
-
-
-def is_misago_exception(exception):
-    return exception.__class__ in MISAGO_EXCEPTIONS

+ 0 - 48
misago/core/tests/test_exceptionhandler_is_misago_exception.py

@@ -1,48 +0,0 @@
-from django.test import TestCase
-from django.http import Http404 as DjHttp404
-from django.core import exceptions as django_exceptions
-from misago.core import exceptionhandler, exceptions as misago_exceptions
-
-
-DJANGO_EXCEPTIONS = (
-    django_exceptions.PermissionDenied,
-    django_exceptions.ViewDoesNotExist,
-    DjHttp404,
-)
-
-PYTHON_EXCEPTIONS = (
-    TypeError,
-    ValueError,
-    KeyError,
-)
-
-
-class ExceptionHandlerIsMisagoExceptionTestCase(TestCase):
-    def test_misago_exception_list_valid(self):
-        """Misago exception handler MISAGO_EXCEPTIONS list is valid"""
-        self.assertEqual(len(exceptionhandler.MISAGO_EXCEPTIONS),
-                         len(misago_exceptions.__all__))
-
-    def test_misago_exceptions_detection(self):
-        """Misago exception handler recognizes Misago exceptions"""
-        for exception in exceptionhandler.MISAGO_EXCEPTIONS:
-            try:
-                raise exception()
-            except exception as e:
-                self.assertTrue(exceptionhandler.is_misago_exception(e))
-
-    def test_django_exceptions_detection(self):
-        """Misago exception handler fails to recognize Django exceptions"""
-        for exception in DJANGO_EXCEPTIONS:
-            try:
-                raise exception()
-            except exception as e:
-                self.assertFalse(exceptionhandler.is_misago_exception(e))
-
-    def test_python_exceptions_detection(self):
-        """Misago exception handler fails to recognize Python exceptions"""
-        for exception in PYTHON_EXCEPTIONS:
-            try:
-                raise exception()
-            except exception as e:
-                self.assertFalse(exceptionhandler.is_misago_exception(e))

+ 0 - 0
misago/core/tests/__init__.py → misago/vews/__init__.py


+ 14 - 6
misago/core/exceptions.py → misago/vews/exceptions/__init__.py

@@ -2,12 +2,7 @@ from django.core.exceptions import PermissionDenied as DjPermissionDenied
 from django.http import Http404 as DjHttp404
 from django.http import Http404 as DjHttp404
 
 
 
 
-__all__ = ["PermissionDenied", "Http404", "OutdatedUrl"]
-
-
-class PermissionDenied(DjPermissionDenied):
-    """The user did not have permission to do that"""
-    pass
+__all__ = ["Http404", "OutdatedUrl", "PermissionDenied"]
 
 
 
 
 class Http404(DjHttp404):
 class Http404(DjHttp404):
@@ -17,3 +12,16 @@ class Http404(DjHttp404):
 
 
 class OutdatedUrl(Exception):
 class OutdatedUrl(Exception):
     """The url that was used to reach view contained outdated slug"""
     """The url that was used to reach view contained outdated slug"""
+    pass
+
+
+class PermissionDenied(DjPermissionDenied):
+    """The user did not have permission to do that"""
+    pass
+
+
+MISAGO_EXCEPTIONS = (
+    Http404,
+    OutdatedUrl,
+    PermissionDenied,
+)

+ 38 - 0
misago/vews/exceptions/handler.py

@@ -0,0 +1,38 @@
+from misago.views import exceptions
+
+
+def is_misago_exception(exception):
+    return exception.__class__ in exceptions.MISAGO_EXCEPTIONS
+
+
+def handle_http404_exception(request, exception):
+    raise NotImplementedError()
+
+
+def handle_outdated_url_exception(request, exception):
+    raise NotImplementedError()
+
+
+def handle_permission_denied_exception(request, exception):
+    raise NotImplementedError()
+
+
+EXCEPTION_HANDLERS = (
+    (exceptions.Http404, handle_http404_exception),
+    (exceptions.OutdatedUrl, handle_outdated_url_exception),
+    (exceptions.PermissionDenied, handle_permission_denied_exception),
+)
+
+
+def get_exception_handler(exception):
+    for exception_type, handler in EXCEPTION_HANDLERS:
+        if isinstance(exception, exception_type):
+            return handler
+    else:
+        raise ValueError(
+            "%s is not Misago exception" % exception.__class__.__name__)
+
+
+def handle_misago_exception(request, exception):
+    handler = get_exception_handler(exception)
+    return handler(request, exception)

+ 9 - 0
misago/vews/exceptions/middleware.py

@@ -0,0 +1,9 @@
+from misago.views.exceptions import handler
+
+
+class MisagoExceptionHandlerMiddleware(object):
+    def process_exception(self, request, exception):
+        if handler.is_misago_exception(exception):
+            return handler.handle_misago_exception(request, exception)
+        else:
+            return None

+ 0 - 0
misago/vews/tests/__init__.py


+ 89 - 0
misago/vews/tests/test_exceptions.py

@@ -0,0 +1,89 @@
+from django.test import TestCase
+from django.http import Http404 as DjHttp404
+from django.core import exceptions as django_exceptions
+from misago.views import exceptions as misago_exceptions
+from misago.views.exceptions import handler
+
+
+DJANGO_EXCEPTIONS = (
+    django_exceptions.PermissionDenied,
+    django_exceptions.ViewDoesNotExist,
+    DjHttp404,
+)
+
+PYTHON_EXCEPTIONS = (
+    TypeError,
+    ValueError,
+    KeyError,
+)
+
+
+class MisagoExceptionHandlerTests(TestCase):
+    def test_misago_exception_list_valid(self):
+        """MISAGO_EXCEPTIONS list is valid"""
+        self.assertEqual(len(misago_exceptions.MISAGO_EXCEPTIONS),
+                         len(misago_exceptions.__all__))
+
+        for exception in misago_exceptions.MISAGO_EXCEPTIONS:
+            if exception.__name__ not in  misago_exceptions.__all__:
+                self.fail("%s is registered in "
+                          "misago.exceptions.MISAGO_EXCEPTIONS but not in "
+                          "misago.exceptions.__all__" % exception.__name__)
+
+    def test_misago_exceptions_detection(self):
+        """Misago exception handler recognizes Misago exceptions"""
+        for exception in misago_exceptions.MISAGO_EXCEPTIONS:
+            try:
+                raise exception()
+            except exception as e:
+                self.assertTrue(handler.is_misago_exception(e))
+
+    def test_django_exceptions_detection(self):
+        """Misago exception handler fails to recognize Django exceptions"""
+        for exception in DJANGO_EXCEPTIONS:
+            try:
+                raise exception()
+            except exception as e:
+                self.assertFalse(handler.is_misago_exception(e))
+
+    def test_python_exceptions_detection(self):
+        """Misago exception handler fails to recognize Python exceptions"""
+        for exception in PYTHON_EXCEPTIONS:
+            try:
+                raise exception()
+            except exception as e:
+                self.assertFalse(handler.is_misago_exception(e))
+
+    def test_exception_handlers_list(self):
+        """EXCEPTION_HANDLERS length matches that of MISAGO_EXCEPTIONS"""
+        self.assertEqual(len(misago_exceptions.MISAGO_EXCEPTIONS),
+                         len(handler.EXCEPTION_HANDLERS))
+
+    def test_get_exception_handler(self):
+        """Exception handler has handler for every Misago exception"""
+        for exception in misago_exceptions.MISAGO_EXCEPTIONS:
+            try:
+                handler.get_exception_handler(exception())
+            except ValueError:
+                self.fail(
+                    "%s has no exception handler defined " % exception.__name__)
+
+    def test_handle_http404_exception(self):
+        """Exception handler corrently turns Http404 exception into response"""
+        self.fail("misago.views.exceptions.handler.handle_http404_exception "
+                  "has not been implmented.")
+
+    def test_handle_outdated_url_exception(self):
+        """Exception handler corrently turns Misago exceptions into responses"""
+        self.fail("misago.views.exceptionshandler.handle_outdated_url_exception "
+                  "has not been implmented.")
+
+    def test_handle_permission_denied_exception(self):
+        """Exception handler corrently turns Misago exceptions into responses"""
+        self.fail("misago.views.exceptions.handler.handle_"
+                  "permission_denied_exception has not been implmented.")
+
+    def test_handle_misago_exception(self):
+        """Exception handler corrently turns Misago exceptions into responses"""
+        self.fail("misago.views.exceptions.handler.handle_misago_exception has "
+                  "not been implmented.")