REST API exception cleanup
diff --git a/planetstack/apigen/api.template.py b/planetstack/apigen/api.template.py
index b3438c0..ae67083 100644
--- a/planetstack/apigen/api.template.py
+++ b/planetstack/apigen/api.template.py
@@ -11,7 +11,7 @@
 from django.conf.urls import patterns, url
 from rest_framework.exceptions import PermissionDenied as RestFrameworkPermissionDenied
 from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
-from xosapibase import XOSRetrieveUpdateDestroyAPIView, XOSListCreateAPIView
+from xosapibase import XOSRetrieveUpdateDestroyAPIView, XOSListCreateAPIView, XOSNotAuthenticated
 
 if hasattr(serializers, "ReadOnlyField"):
     # rest_framework 3.x
@@ -181,7 +181,7 @@
 
     def get_queryset(self):
         if (not self.request.user.is_authenticated()):
-            raise RestFrameworkPermissionDenied("You must be authenticated in order to use this API")
+            raise XOSNotAuthenticated()
         return {{ object.camel }}.select_by_user(self.request.user)
 
 
@@ -201,7 +201,7 @@
 
     def get_queryset(self):
         if (not self.request.user.is_authenticated()):
-            raise RestFrameworkPermissionDenied("You must be authenticated in order to use this API")
+            raise XOSNotAuthenticated()
         return {{ object.camel }}.select_by_user(self.request.user)
 
     # update() is handled by XOSRetrieveUpdateDestroyAPIView
diff --git a/planetstack/core/xoslib/methods/sliceplus.py b/planetstack/core/xoslib/methods/sliceplus.py
index 0539e62..cdb2f73 100644
--- a/planetstack/core/xoslib/methods/sliceplus.py
+++ b/planetstack/core/xoslib/methods/sliceplus.py
@@ -7,8 +7,7 @@
 from django.forms import widgets
 from core.xoslib.objects.sliceplus import SlicePlus
 from plus import PlusSerializerMixin
-from xosapibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView
-from rest_framework.exceptions import PermissionDenied as RestFrameworkPermissionDenied
+from xosapibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
 
 if hasattr(serializers, "ReadOnlyField"):
     # rest_framework 3.x
@@ -82,7 +81,7 @@
         current_user_can_see = self.request.QUERY_PARAMS.get('current_user_can_see', False)
 
         if (not self.request.user.is_authenticated()):
-            raise RestFrameworkPermissionDenied("You must be authenticated in order to use this API")
+            raise XOSPermissionDenied("You must be authenticated in order to use this API")
 
         slices = SlicePlus.select_by_user(self.request.user)
 
@@ -108,7 +107,7 @@
 
     def get_queryset(self):
         if (not self.request.user.is_authenticated()):
-            raise RestFrameworkPermissionDenied("You must be authenticated in order to use this API")
+            raise XOSPermissionDenied("You must be authenticated in order to use this API")
         return SlicePlus.select_by_user(self.request.user)
 
 
diff --git a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
index a055093..2779cd0 100644
--- a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
+++ b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js
@@ -145,18 +145,21 @@
             parsed_error=undefined;
             width=640;    // django stacktraces like wide width
         }
+
         console.log(responseText);
         console.log(parsed_error);
 
+        if (parsed_error && ("detail" in parsed_error)) {
+            parsed_error = parsed_error["detail"];
+        }
+
         if (parsed_error && ("error" in parsed_error)) {
+            if ((!parsed_error.reasons) && (parsed_error.fields)) {
+                // deal with me renaming 'reasons' to 'fields'
+                parsed_error.reasons = parsed_error.fields;
+            }
             // this error comes from genapi views
             $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
-        } else if (parsed_error && ("detail" in parsed_error)) {
-            // this error response comes from rest_framework APIException
-            parsed_error["error"] = "API Error";
-            parsed_error["specific_error"] = parsed_error["detail"];
-            parsed_error["reasons"] = [];
-            $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
         } else {
             $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: strip_scripts(responseText)}))
         }
diff --git a/planetstack/xosapibase.py b/planetstack/xosapibase.py
index ea84d9a..c523943 100644
--- a/planetstack/xosapibase.py
+++ b/planetstack/xosapibase.py
@@ -2,9 +2,36 @@
 from rest_framework import serializers
 from rest_framework import generics
 from rest_framework import status
+from rest_framework.exceptions import APIException
 from rest_framework.exceptions import PermissionDenied as RestFrameworkPermissionDenied
 from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
 
+class XOSProgrammingError(APIException):
+    status_code=400
+    def __init__(self, why="programming error", fields={}):
+        APIException.__init__(self, {"error": "XOSProgrammingError",
+                            "specific_error": why,
+                            "fields": fields})
+
+class XOSPermissionDenied(RestFrameworkPermissionDenied):
+    def __init__(self, why="permission error", fields={}):
+        APIException.__init__(self, {"error": "XOSPermissionDenied",
+                            "specific_error": why,
+                            "fields": fields})
+
+class XOSNotAuthenticated(RestFrameworkPermissionDenied):
+    def __init__(self, why="you must be authenticated to use this api", fields={}):
+        APIException.__init__(self, {"error": "XOSNotAuthenticated",
+                            "specific_error": why,
+                            "fields": fields})
+
+class XOSValidationError(APIException):
+    status_code=403
+    def __init__(self, why="validation error", fields={}):
+        APIException.__init__(self, {"error": "XOSValidationError",
+                            "specific_error": why,
+                            "fields": fields})
+
 class XOSRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
 
     # To handle fine-grained field permissions, we have to check can_update
@@ -15,7 +42,7 @@
         self.object = self.get_object_or_none()

 

         if self.object is None:

-            raise Exception("Use the List API for creating objects")

+            raise XOSProgrammingError("Use the List API for creating objects")

 

         serializer = self.get_serializer(self.object, data=request.DATA,

                                          files=request.FILES, partial=partial)

@@ -25,27 +52,21 @@
         serializer.object.caller = request.user

 

         if not serializer.is_valid():

-            response = {"error": "validation",

-                        "specific_error": "not serializer.is_valid()",

-                        "reasons": serializer.errors}

-            return Response(response, status=status.HTTP_400_BAD_REQUEST)

-

-        try:

-            self.pre_save(serializer.object)

-        except ValidationError as err:

-            # full_clean on model instance may be called in pre_save,

-            # so we have to handle eventual errors.

-            response = {"error": "validation",

-                         "specific_error": "ValidationError in pre_save",

-                         "reasons": err.message_dict}

-            return Response(response, status=status.HTTP_400_BAD_REQUEST)

+            raise XOSValidationError(fields=serializer._errors)

 

         if not serializer.object.can_update(request.user):

-            return Response(status=status.HTTP_400_BAD_REQUEST)

+            raise XOSPermissionDenied()

 

-        self.object = serializer.save(force_update=True)

-        self.post_save(self.object, created=False)

-        return Response(serializer.data, status=status.HTTP_200_OK)
+        if (hasattr(self, "pre_save")):

+            # rest_framework 2.x
+            self.pre_save(serializer.object)
+            self.object = serializer.save(force_update=True)
+            self.post_save(self.object, created=False)
+        else:
+            # rest_framework 3.x
+            self.perform_update(serializer)

+

+        return Response(serializer.data, status=status.HTTP_200_OK)

 
     def destroy(self, request, *args, **kwargs):
         obj = self.get_object()
@@ -64,7 +85,7 @@
         # REST API drops the string attached to Django's PermissionDenied
         # exception, and replaces it with a generic "Permission Denied"
         if isinstance(exc, DjangoPermissionDenied):
-            response=Response({'detail': str(exc)}, status=status.HTTP_403_FORBIDDEN)
+            response=Response({'detail': {"error": "PermissionDenied", "specific_error": str(exc), "fields": {}}}, status=status.HTTP_403_FORBIDDEN)
             response.exception=True
             return response
         else:
@@ -73,21 +94,18 @@
 class XOSListCreateAPIView(generics.ListCreateAPIView):
     def create(self, request, *args, **kwargs):
         serializer = self.get_serializer(data=request.DATA, files=request.FILES)
-        if not (serializer.is_valid()):
-            response = {"error": "validation",
-                        "specific_error": "not serializer.is_valid()",

-                        "reasons": serializer.errors}

-            return Response(response, status=status.HTTP_400_BAD_REQUEST)
+
+        # In rest_framework 3.x: we can pass raise_exception=True instead of
+        # raising the exception ourselves

+        if not serializer.is_valid():

+            raise XOSValidationError(fields=serializer._errors)

 
         # now do XOS can_update permission checking
 
         obj = serializer.object
         obj.caller = request.user
         if not obj.can_update(request.user):
-            response = {"error": "validation",
-                        "specific_error": "failed can_update",

-                        "reasons": []}

-            return Response(response, status=status.HTTP_400_BAD_REQUEST)
+            raise XOSPermissionDenied()
 
         # stuff below is from generics.ListCreateAPIView
 
@@ -108,7 +126,7 @@
         # REST API drops the string attached to Django's PermissionDenied
         # exception, and replaces it with a generic "Permission Denied"
         if isinstance(exc, DjangoPermissionDenied):
-            response=Response({'detail': str(exc)}, status=status.HTTP_403_FORBIDDEN)
+            response=Response({'detail': {"error": "PermissionDenied", "specific_error": str(exc), "fields": {}}}, status=status.HTTP_403_FORBIDDEN)
             response.exception=True
             return response
         else: