commit 381a548c4f58d731cf88f56c9c5e7ab3e8879be5
Author: Sofia Papagiannaki <spapagian@grnet.gr>
Date:   Wed Oct 30 17:20:47 2013 +0200

    snf_django: Change error status code if request method is not valid
    
    Return 405 (NotAllowed) instead of 400 (BadRequest)
    Add Allow header in the response containing the list of valid methods

diff --git a/snf-astakos-app/astakos/api/projects.py b/snf-astakos-app/astakos/api/projects.py
index fdfb983..a108a3d 100644
--- a/snf-astakos-app/astakos/api/projects.py
+++ b/snf-astakos-app/astakos/api/projects.py
@@ -266,7 +266,7 @@ def projects(request):
         return get_projects(request)
     elif method == "POST":
         return create_project(request)
-    return api.api_method_not_allowed(request)
+    return api.api_method_not_allowed(request, allowed_methods=['GET', 'POST'])
 
 
 @api.api_method(http_method="GET", token_required=True, user_required=False)
@@ -314,7 +314,7 @@ def project(request, project_id):
         return get_project(request, project_id)
     if method == "POST":
         return modify_project(request, project_id)
-    return api.api_method_not_allowed(request)
+    return api.api_method_not_allowed(request, allowed_methods=['GET', 'POST'])
 
 
 @api.api_method(http_method="GET", token_required=True, user_required=False)
@@ -483,7 +483,7 @@ def applications(request):
     method = request.method
     if method == "GET":
         return get_applications(request)
-    return api.api_method_not_allowed(request)
+    return api.api_method_not_allowed(request, allowed_methods=['GET'])
 
 
 def make_application_query(input_data):
@@ -567,7 +567,7 @@ def memberships(request):
         return get_memberships(request)
     elif method == "POST":
         return post_memberships(request)
-    return api.api_method_not_allowed(request)
+    return api.api_method_not_allowed(request, allowed_methods=['GET', 'POST'])
 
 
 def make_membership_query(input_data):
diff --git a/snf-astakos-app/astakos/api/quotas.py b/snf-astakos-app/astakos/api/quotas.py
index 1475c8d..f219ecc 100644
--- a/snf-astakos-app/astakos/api/quotas.py
+++ b/snf-astakos-app/astakos/api/quotas.py
@@ -82,7 +82,7 @@ def commissions(request):
         return get_pending_commissions(request)
     elif method == 'POST':
         return issue_commission(request)
-    return api.api_method_not_allowed(request)
+    return api.api_method_not_allowed(request, allowed_methods=['GET', 'POST'])
 
 
 @api.api_method(http_method='GET', token_required=True, user_required=False)
diff --git a/snf-astakos-app/astakos/im/tests/api.py b/snf-astakos-app/astakos/im/tests/api.py
index fcea23a..508ffcf 100644
--- a/snf-astakos-app/astakos/im/tests/api.py
+++ b/snf-astakos-app/astakos/im/tests/api.py
@@ -384,7 +384,8 @@ class QuotaAPITest(TestCase):
 
         # Bad Request
         r = client.head(u('commissions'))
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
 
 
 class TokensApiTest(TestCase):
@@ -434,7 +435,9 @@ class TokensApiTest(TestCase):
 
         # Check not allowed method
         r = client.get(url, post_data={})
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'], 'POST')
 
         # check public mode
         r = client.post(url, CONTENT_LENGTH=0)
diff --git a/snf-astakos-app/astakos/im/tests/projects.py b/snf-astakos-app/astakos/im/tests/projects.py
index e1fb61b..9d689ed 100644
--- a/snf-astakos-app/astakos/im/tests/projects.py
+++ b/snf-astakos-app/astakos/im/tests/projects.py
@@ -488,17 +488,21 @@ class ProjectAPITest(TestCase):
 
         # Bad requests
         r = client.head(reverse("api_projects"), **h_admin)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
 
         r = client.head(reverse("api_project",
                                 kwargs={"project_id": 1}), **h_admin)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
 
         r = client.head(reverse("api_applications"), **h_admin)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
 
         r = client.head(reverse("api_memberships"), **h_admin)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
 
         status = self.project_action(1, "nonex", h_owner)
         self.assertEqual(status, 400)
diff --git a/snf-cyclades-app/synnefo/api/extensions.py b/snf-cyclades-app/synnefo/api/extensions.py
index 56efd02..5959bf5 100644
--- a/snf-cyclades-app/synnefo/api/extensions.py
+++ b/snf-cyclades-app/synnefo/api/extensions.py
@@ -52,14 +52,14 @@ def demux(request):
     if request.method == 'GET':
         return list_extensions(request)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request, allowed_methods=['GET'])
 
 
 def demux_extension(request, extension_alias):
     if request.method == 'GET':
         return get_extension(request, extension_alias)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request, allowed_methods=['GET'])
 
 
 @api.api_method(http_method='GET', user_required=True, logger=log)
diff --git a/snf-cyclades-app/synnefo/api/floating_ips.py b/snf-cyclades-app/synnefo/api/floating_ips.py
index acd7b45..9565943 100644
--- a/snf-cyclades-app/synnefo/api/floating_ips.py
+++ b/snf-cyclades-app/synnefo/api/floating_ips.py
@@ -64,7 +64,8 @@ def demux(request):
     elif request.method == 'POST':
         return allocate_floating_ip(request)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET', 'POST'])
 
 
 def floating_ip_demux(request, floating_ip_id):
@@ -73,7 +74,8 @@ def floating_ip_demux(request, floating_ip_id):
     elif request.method == 'DELETE':
         return release_floating_ip(request, floating_ip_id)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET', 'DELETE'])
 
 
 def ip_to_dict(floating_ip):
diff --git a/snf-cyclades-app/synnefo/api/images.py b/snf-cyclades-app/synnefo/api/images.py
index edb8c7b..a1e4986 100644
--- a/snf-cyclades-app/synnefo/api/images.py
+++ b/snf-cyclades-app/synnefo/api/images.py
@@ -65,7 +65,8 @@ def demux(request):
     elif request.method == 'POST':
         return create_image(request)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET', 'POST'])
 
 
 def image_demux(request, image_id):
@@ -74,7 +75,8 @@ def image_demux(request, image_id):
     elif request.method == 'DELETE':
         return delete_image(request, image_id)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET', 'DELETE'])
 
 
 def metadata_demux(request, image_id):
@@ -83,7 +85,8 @@ def metadata_demux(request, image_id):
     elif request.method == 'POST':
         return update_metadata(request, image_id)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET', 'POST'])
 
 
 def metadata_item_demux(request, image_id, key):
@@ -94,7 +97,10 @@ def metadata_item_demux(request, image_id, key):
     elif request.method == 'DELETE':
         return delete_metadata_item(request, image_id, key)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET',
+                                                           'PUT',
+                                                           'DELETE'])
 
 
 def image_to_dict(image, detail=True):
diff --git a/snf-cyclades-app/synnefo/api/networks.py b/snf-cyclades-app/synnefo/api/networks.py
index b80122b..01ab01e 100644
--- a/snf-cyclades-app/synnefo/api/networks.py
+++ b/snf-cyclades-app/synnefo/api/networks.py
@@ -64,7 +64,8 @@ def demux(request):
     elif request.method == 'POST':
         return create_network(request)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET', 'POST'])
 
 
 def network_demux(request, network_id):
@@ -75,7 +76,10 @@ def network_demux(request, network_id):
     elif request.method == 'DELETE':
         return delete_network(request, network_id)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET',
+                                                           'PUT',
+                                                           'DELETE'])
 
 
 def network_to_dict(network, user_id, detail=True):
diff --git a/snf-cyclades-app/synnefo/api/servers.py b/snf-cyclades-app/synnefo/api/servers.py
index 8598251..280b43f 100644
--- a/snf-cyclades-app/synnefo/api/servers.py
+++ b/snf-cyclades-app/synnefo/api/servers.py
@@ -70,7 +70,8 @@ def demux(request):
     elif request.method == 'POST':
         return create_server(request)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET', 'POST'])
 
 
 def server_demux(request, server_id):
@@ -81,7 +82,10 @@ def server_demux(request, server_id):
     elif request.method == 'DELETE':
         return delete_server(request, server_id)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET',
+                                                           'PUT',
+                                                           'DELETE'])
 
 
 def metadata_demux(request, server_id):
@@ -90,7 +94,8 @@ def metadata_demux(request, server_id):
     elif request.method == 'POST':
         return update_metadata(request, server_id)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET', 'POST'])
 
 
 def metadata_item_demux(request, server_id, key):
@@ -101,7 +106,10 @@ def metadata_item_demux(request, server_id, key):
     elif request.method == 'DELETE':
         return delete_metadata_item(request, server_id, key)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['GET',
+                                                           'PUT',
+                                                           'DELETE'])
 
 
 def nic_to_dict(nic):
diff --git a/snf-django-lib/snf_django/lib/api/__init__.py b/snf-django-lib/snf_django/lib/api/__init__.py
index 5a413ce..a874f88 100644
--- a/snf-django-lib/snf_django/lib/api/__init__.py
+++ b/snf-django-lib/snf_django/lib/api/__init__.py
@@ -90,7 +90,8 @@ def api_method(http_method=None, token_required=True, user_required=True,
 
                 # Check HTTP method
                 if http_method and request.method != http_method:
-                    raise faults.BadRequest("Method not allowed")
+                    raise faults.NotAllowed("Method not allowed",
+                                            allowed_methods=[http_method])
 
                 # Get authentication token
                 request.x_auth_token = None
@@ -231,6 +232,8 @@ def render_fault(request, fault):
         data = json.dumps(d)
 
     response = HttpResponse(data, status=fault.code)
+    if response.status_code == 405 and hasattr(fault, 'allowed_methods'):
+        response['Allow'] = ','.join(fault.allowed_methods)
     update_response_headers(request, response)
     return response
 
@@ -241,8 +244,9 @@ def api_endpoint_not_found(request):
 
 
 @api_method(token_required=False, user_required=False)
-def api_method_not_allowed(request):
-    raise faults.BadRequest('Method not allowed')
+def api_method_not_allowed(request, allowed_methods):
+    raise faults.NotAllowed("Method not allowed",
+                            allowed_methods=allowed_methods)
 
 
 def allow_jsonp(key='callback'):
diff --git a/snf-django-lib/snf_django/lib/api/faults.py b/snf-django-lib/snf_django/lib/api/faults.py
index 5143b9a..9542dc4 100644
--- a/snf-django-lib/snf_django/lib/api/faults.py
+++ b/snf-django-lib/snf_django/lib/api/faults.py
@@ -71,6 +71,18 @@ class ResizeNotAllowed(Forbidden):
     pass
 
 
+class NotAllowed(Fault):
+    code = 405
+
+    def __init__(self, message='', details='', name='', code=500,
+                 allowed_methods=None):
+        """
+        :param allowed_methods: (list) the valid methods
+        """
+        super(NotAllowed, self).__init__(message, details, name, code)
+        self.allowed_methods = allowed_methods or []
+
+
 class ItemNotFound(Fault):
     code = 404
 
diff --git a/snf-django-lib/snf_django/utils/testing.py b/snf-django-lib/snf_django/utils/testing.py
index f6b7dd2..0d3a27d 100644
--- a/snf-django-lib/snf_django/utils/testing.py
+++ b/snf-django-lib/snf_django/utils/testing.py
@@ -256,12 +256,13 @@ class BaseAPITest(TestCase):
         self.assertFault(response, 404, 'itemNotFound')
 
     def assertMethodNotAllowed(self, response):
-        self.assertFault(response, 400, 'badRequest')
+        self.assertFault(response, 405, 'notAllowed')
+        self.assertTrue('Allow' in response)
         try:
             error = json.loads(response.content)
         except ValueError:
             self.assertTrue(False)
-        self.assertEqual(error['badRequest']['message'], 'Method not allowed')
+        self.assertEqual(error['notAllowed']['message'], 'Method not allowed')
 
 
 # Imitate unittest assertions new in Python 2.7
diff --git a/snf-pithos-app/pithos/api/functions.py b/snf-pithos-app/pithos/api/functions.py
index fc63c4d..0ccc42e 100644
--- a/snf-pithos-app/pithos/api/functions.py
+++ b/snf-pithos-app/pithos/api/functions.py
@@ -98,7 +98,7 @@ def top_demux(request):
                 return authenticate(request)
         return account_list(request)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request, allowed_methods=['GET'])
 
 
 @csrf_exempt
@@ -117,7 +117,10 @@ def account_demux(request, v_account):
     elif request.method == 'GET':
         return container_list(request, v_account)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['HEAD',
+                                                           'POST',
+                                                           'GET'])
 
 
 @csrf_exempt
@@ -140,7 +143,12 @@ def container_demux(request, v_account, v_container):
     elif request.method == 'GET':
         return object_list(request, v_account, v_container)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request,
+                                          allowed_methods=['HEAD',
+                                                           'PUT',
+                                                           'POST',
+                                                           'DELETE',
+                                                           'GET'])
 
 
 @csrf_exempt
@@ -172,7 +180,13 @@ def object_demux(request, v_account, v_container, v_object):
     elif request.method == 'DELETE':
         return object_delete(request, v_account, v_container, v_object)
     else:
-        return api.api_method_not_allowed(request)
+        return api.api_method_not_allowed(request, allowed_methods=['HEAD',
+                                                                    'GET',
+                                                                    'PUT',
+                                                                    'COPY',
+                                                                    'MOVE',
+                                                                    'POST',
+                                                                    'DELETE'])
 
 
 @api_method('GET', token_required=False, user_required=False, logger=logger)
diff --git a/snf-pithos-app/pithos/api/test/top_level.py b/snf-pithos-app/pithos/api/test/top_level.py
index 053a7bf..2c4df76 100644
--- a/snf-pithos-app/pithos/api/test/top_level.py
+++ b/snf-pithos-app/pithos/api/test/top_level.py
@@ -42,13 +42,22 @@ class TopLevel(PithosAPITest):
     def test_not_allowed_method(self):
         url = join_urls(self.pithos_path, '/')
         r = self.head(url)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'], 'GET')
         r = self.put(url, data='')
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'], 'GET')
         r = self.post(url, data='')
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'], 'GET')
         r = self.delete(url)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'], 'GET')
+
 
     def test_authenticate(self):
         url = join_urls(self.pithos_path, '/')
diff --git a/snf-pithos-app/pithos/api/test/views.py b/snf-pithos-app/pithos/api/test/views.py
index 2116dd5..58d42b5 100644
--- a/snf-pithos-app/pithos/api/test/views.py
+++ b/snf-pithos-app/pithos/api/test/views.py
@@ -92,22 +92,34 @@ class NotAllowedView(PithosAPITest):
                                   get_random_name())
 
         r = self.head(self.view_url)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'],  'GET')
 
         r = self.delete(self.view_url)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'],  'GET')
 
         r = self.post(self.view_url)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'],  'GET')
 
         r = self.put(self.view_url)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'],  'GET')
 
         r = self.copy(self.view_url)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'],  'GET')
 
         r = self.move(self.view_url)
-        self.assertEqual(r.status_code, 400)
+        self.assertEqual(r.status_code, 405)
+        self.assertTrue('Allow' in r)
+        self.assertEqual(r['Allow'],  'GET')
 
 
 class ObjectGetView(PithosAPITest):
diff --git a/snf-pithos-app/pithos/api/urls.py b/snf-pithos-app/pithos/api/urls.py
index 884b3e6..8293000 100644
--- a/snf-pithos-app/pithos/api/urls.py
+++ b/snf-pithos-app/pithos/api/urls.py
@@ -66,7 +66,7 @@ pithos_api_patterns = api_patterns(
 pithos_view_patterns = patterns(
     'pithos.api.views',
     (r'^view/(?P<v_account>.+?)/(?P<v_container>.+?)/(?P<v_object>.+?)$',
-     'object_read'))
+     'object_demux'))
 
 pithos_patterns = patterns(
     '',
diff --git a/snf-pithos-app/pithos/api/views.py b/snf-pithos-app/pithos/api/views.py
index 615b633..d4dc254 100755
--- a/snf-pithos-app/pithos/api/views.py
+++ b/snf-pithos-app/pithos/api/views.py
@@ -31,6 +31,10 @@
 # interpreted as representing official policies, either expressed
 # or implied, of GRNET S.A.
 
+from django.views.decorators.csrf import csrf_exempt
+
+from snf_django.lib import api
+
 from pithos.api.functions import _object_read
 from pithos.api.util import api_method, view_method
 
@@ -38,6 +42,14 @@ import logging
 logger = logging.getLogger(__name__)
 
 
+@csrf_exempt
+def object_demux(request, v_account, v_container, v_object):
+    if request.method == 'GET':
+        return object_read(request, v_account, v_container, v_object)
+    else:
+        return api.api_method_not_allowed(request, allowed_methods=['GET'])
+
+
 @view_method()
 @api_method('GET', format_allowed=True, user_required=True, logger=logger)
 def object_read(request, v_account, v_container, v_object):
