Created
July 18, 2009 08:22
-
-
Save cwvh/149475 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# | |
# Copyright 2009 Chris Van Horne | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
from django.http import HttpResponse, HttpResponseRedirect | |
class Handler(object): | |
def __new__(cls, *args, **kwargs): | |
obj = super(Handler, cls).__new__(cls) | |
return obj(*args, **kwargs) | |
def __call__(self, request, *args, **kwargs): | |
try: | |
self.request = request | |
self.response = HttpResponse | |
method = request.method.lower() | |
return self.__getattribute__(method)(*args, **kwargs) | |
except AttributeError: | |
return self.response(status=NOT_IMPLEMENTED) | |
except Exception: | |
return self.response(status=INTERNAL_ERROR) | |
def get(self): | |
return self.response(status=NOT_IMPLEMENTED) | |
def post(self): | |
return self.response(status=NOT_IMPLEMENTED) | |
def put(self): | |
return self.response(status=NOT_IMPLEMENTED) | |
def delete(self): | |
return self.response(status=NOT_IMPLEMENTED) | |
def head(self): | |
return self.response(status=NOT_IMPLEMENTED) | |
def trace(self): | |
return self.response(status=NOT_IMPLEMENTED) | |
# HTTP Status Codes | |
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html | |
# | |
# Informational - 1xx | |
CONTINUE = 100 | |
SWITCHING_PROTOCOLS = 101 | |
# Successful - 2xx | |
OK = 200 | |
CREATED = 201 | |
ACCEPTED = 202 | |
NONAUTHORITATIVE_INFORMATION = 203 | |
NO_CONTENT = 204 | |
RESET_CONTENT = 205 | |
PARTIAL_CONTENT = 206 | |
# Redirection - 3xx | |
MULTIPLE_CHOICES = 300 | |
MOVED_PERMANENTLY = 301 | |
FOUND = 302 | |
SEE_OTHER = 303 | |
NOT_MODIFIED = 304 | |
USE_PROXY = 305 | |
UNUSED = 306 | |
TEMPORARY_REDIRECT = 307 | |
# Client Error - 4xx | |
BAD_REQUEST = 400 | |
UNAUTHORIZED = 401 | |
PAYMENT_REQUIRED = 402 | |
FORBIDDEN = 403 | |
NOT_FOUND = 404 | |
METHOD_NOT_ALLOWED = 405 | |
NOT_ACCEPTABLE = 406 | |
PROXY_AUTH_REQUIRED = 407 | |
REQUEST_TIMEOUT = 408 | |
CONFLICT = 409 | |
GONE = 410 | |
LENGTH_REQUIRED = 411 | |
PRECONDITION_FAILED = 412 | |
REQUEST_TOO_LARGE = 413 | |
URI_TOO_LONG = 414 | |
UNSUPPORTED_MEDIA = 415 | |
RANGE_NOT_SATISFIABLE = 416 | |
EXPECTATION_FAILED = 417 | |
# Server Error - 5xx | |
INTERNAL_ERROR = 500 | |
NOT_IMPLEMENTED = 501 | |
BAD_GATEWAY = 502 | |
SERVICE_UNAVAILABLE = 503 | |
GATEWAY_TIMEOUT = 504 | |
VERSION_NOT_SUPPORTED = 505 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# | |
# Copyright 2009 Chris Van Horne | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
"""Defines a few utility decorators for restful.Handler instances.""" | |
from django.contrib.auth import authenticate, login | |
from django.contrib.auth.models import User | |
import base64 | |
import restful | |
def auth_required(func): | |
"""Require request.user to be authenticated. This is currently | |
taylored to work only with restful.Handler, but could be corrected | |
to be general to any Django view. | |
Returns: | |
func or 403 Forbidden | |
""" | |
def _wrapper(self, *args, **kwargs): | |
if self.request.user.is_authenticated(): | |
return func(self, *args, **kwargs) | |
else: | |
msg = "API requires site authentication." | |
return self.response(status=restful.FORBIDDEN, content=msg) | |
return _wrapper | |
# Decorator based on Scanner's original django snippet. | |
# See original here: http://www.djangosnippets.org/snippets/243/ | |
def basic_http_auth_allowed(func): | |
"""This will attempt to fill self.request.user with a proper | |
user account if the client has attempted a basic HTTP auth. | |
Exceptional states are simplified since a failed auth will leave | |
self.request.user as None. | |
Returns: | |
func with self.request.user filled if good auth. | |
""" | |
def _wrapper(self, *args, **kwargs): | |
if 'HTTP_AUTHORIZATION' in self.request.META: | |
auth = self.request.META['HTTP_AUTHORIZATION'].split() | |
if len(auth) == 2: | |
if auth[0].lower() == 'basic': | |
email, passwd = base64.b64decode(auth[1]).split(':') | |
uname = User.objects.get(email=email).username | |
user = authenticate(username=uname, password=passwd) | |
if user is not None: | |
if user.is_active: | |
login(self.request, user) | |
self.request.user = user | |
return func(self, *args, **kwargs) | |
return _wrapper | |
def basic_http_auth_required(func, realm=''): | |
"""Strict version basic_http_auth_allowable. The client must | |
successfully authenticate otherwise 401 "Unauthorized". | |
Returns: | |
func on success, 401 "Unauthorized" on failure. | |
""" | |
def _wrapper(self, *args, **kwargs): | |
if 'HTTP_AUTHORIZATION' in self.request.META: | |
auth = self.request.META['HTTP_AUTHORIZATION'].split() | |
if len(auth) == 2: | |
if auth[0].lower() == 'basic': | |
email, passwd = base64.b64decode(auth[1]).split(':') | |
uname = User.objects.get(email=email).username | |
user = authenticate(username=uname, password=passwd) | |
if user is not None: | |
if user.is_active: | |
login(self.request, user) | |
self.request.user = user | |
return func(self, *args, **kwargs) | |
response = self.response(status=restful.UNAUTHORIZED) | |
response['WWW-Authenticate'] = 'Basic realm="%s"' % realm | |
return response | |
return _wrapper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
... | |
url(r'^favorites/$', 'Favorites', name='favorites'), | |
url(r'^favorites/(?P<id>\d+)/$', 'Favorites', name='favorites'), | |
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import restful | |
from restful.decorators import auth_required, basic_http_auth_allowed | |
class Favorites(restful.Handler): | |
"""Favorites REST handler.""" | |
@basic_http_auth_allowed | |
@auth_required | |
def get(self, id=None): | |
try: | |
user = self.request.user | |
if id is not None: | |
# This needs to be iterable for the comprehension below. | |
# Instead of building up a Q object, use ".get" so that | |
# multiple returns will raise an appropriate exception | |
# and we can return the correct HTTP status code. | |
favorite = (Favorite.objects.get(property__id=id, user=user),) | |
else: | |
favorite = Favorite.objects.filter(user=user) | |
output = json.write([x.as_dict() for x in favorite]) | |
return self.response(status=200, content=output) | |
except Favorite.MultipleObjectsReturned, e: | |
return self.response(status=restful.CONFLICT, content=e) | |
except Favorite.DoesNotExist: | |
return self.response(status=restful.GONE) | |
@basic_http_auth_allowed | |
@auth_required | |
def put(self, id): | |
try: | |
property = Property.objects.get(id=id) | |
favorite, create = Favorite.objects.get_or_create( | |
user=self.request.user, property=property) | |
if create: | |
status = restful.CREATED | |
else: | |
status = restful.FOUND | |
r = self.response(status=status) | |
url = favorite.get_absolute_url() | |
r['Location'] = self.request.build_absolute_uri(url) | |
return r | |
except Property.DoesNotExist: | |
return self.response(status=restful.NOT_FOUND) | |
except Property.MultipleObjectsReturned, e: | |
return self.response(status=restful.CONFLICT, content=e) | |
@basic_http_auth_allowed | |
@auth_required | |
def delete(self, request, id): | |
try: | |
user = self.request.user | |
if not user.is_authenticated(): | |
msg = "API requires site authentication." | |
return self.response(status=restful.FORBIDDEN, content=msg) | |
if id is not None: | |
Favorite.objects.get(property__id=id, user=user).delete() | |
else: | |
Favorite.objects.filter(user=user).delete() | |
return self.response(status=restful.NO_CONTENT) | |
except Favorite.MultipleObjectsReturned, e: | |
return self.response(status=restful.CONFLICT, content=e) | |
except Favorite.DoesNotExist: | |
return self.response(status=restful.GONE) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment