Skip to content

Commit

Permalink
Merge branch 'release/0.7.9'
Browse files Browse the repository at this point in the history
  • Loading branch information
KayEss committed Jan 24, 2014
2 parents ae4865a + 4066607 commit 14b360a
Show file tree
Hide file tree
Showing 47 changed files with 1,175 additions and 175 deletions.
29 changes: 29 additions & 0 deletions ChangeLog
@@ -1,3 +1,32 @@
2013-12-10 Chirapat Harigulrak <chirapat@proteus-tech.com>
make html parser support any type of object

2013-12-09 Kirit Saelensminde <kirit@felspar.com>
Allow sub-parts of the response object to be used for JSON and XML output.

2013-12-05 Kirit Saelensminde <kirit@felspar.com>
Implemented proper pagination support for the HAL instance list and broke out a re-usable API for them over any query set.
Guess at some output to linkify in the HTML output.

2013-12-04 Kirit Saelensminde <kirit@felspar.com>
Allow the removal of built-in operations from models and also changes to the URIs they are mounted at.
Make HTML the default for browsers rather than XML.
Add new APIs for building URIs for operations.

2013-12-03 Kirit Saelensminde <kirit@felspar.com>
Allow operations to be mounted at any URL below the Slumber mount point.

2013-11-26 Kirit Saelensminde <kirit@felspar.com>
Changed the way that the URLs are configured to be compatible with Django 1.6.
Other changes for Django 1.6 compatibility.

2013-11-25 Siraset Jirapatchandej <siraset@proteus-tech.com>
Merge html response handler from nattakit
Merge xml response handler chirapat

2013-11-20 Chirapat Harigulrak <chirapat@proteus-tech.com>
Create xml response handler

2013-11-12 Kirit Saelensminde <kirit@felspar.com>
Django applicatoins that are imported on the server side are now able to take a configuration that appears when the service URL is fetched.

Expand Down
2 changes: 1 addition & 1 deletion pylintrc
Expand Up @@ -40,7 +40,7 @@ disable=I0011, R0903, W0142, W0212

# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=text
output-format=colorized

# Include message's id in output
include-ids=yes
Expand Down
7 changes: 6 additions & 1 deletion runtests
Expand Up @@ -16,7 +16,7 @@ echo Django 1.3
check_worked python -tt manage.py test slumber_examples
cd ../..

check_worked pylint --rcfile=pylintrc --output-format=colorized slumber
check_worked pylint --rcfile=pylintrc slumber

# workon slumber1-3_psycopg2-4-1
# cd ./test-projects/django1_3_psycopg2_4_1
Expand All @@ -39,6 +39,11 @@ cd ../django1_5
echo Django 1.5
check_worked python manage.py test slumber_examples

workon slumber1-6
cd ../django1_6
echo Django 1.6
check_worked python manage.py test slumber_examples

workon slumber1-0
cd ../django1_0
echo Django 1.0
Expand Down
6 changes: 4 additions & 2 deletions setup.py
Expand Up @@ -10,7 +10,7 @@ def read(fname1, fname2):

setup(
name = "django_slumber",
version = "0.7.7",
version = "0.7.9",
author = "Kirit Saelensminde",
author_email = "kirit@felspar.com",
url='https://github.com/KayEss/django-slumber',
Expand All @@ -23,7 +23,9 @@ def read(fname1, fname2):
'slumber_examples', 'slumber_examples.no_models', 'slumber_examples.tests',
'slumber_examples.nested1', 'slumber_examples.nested1.nested2',
'slumber_ex_shop'],
install_requires = ['simplejson', 'httplib2', 'django-fost-authn >= 0.3.6'],
install_requires = [
'simplejson', 'httplib2', 'django-fost-authn >= 0.3.8', 'dicttoxml',
'dougrain', 'BeautifulSoup'],
classifiers = [
"Development Status :: 3 - Alpha",
"Framework :: Django",
Expand Down
9 changes: 9 additions & 0 deletions slumber/__init__.py
Expand Up @@ -24,3 +24,12 @@ def __getattr__(self, name):
# This is exactly the name we want to use here
# pylint: disable=C0103
client = _ClientProxy()


def data_link(instance, *args, **kwargs):
"""Convenience function to return the default 'data' operation link
for an instance.
"""
operation = type(instance).slumber_model.operations['data']
return operation(instance, *args, **kwargs)

5 changes: 2 additions & 3 deletions slumber/_caches.py
Expand Up @@ -12,9 +12,8 @@
# Stores the slumber models for given model URLs
MODEL_URL_TO_SLUMBER_MODEL = {}


# Stores the operations used for a given model on the server
SLUMBER_MODEL_OPERATIONS = {}
# Store the URIs for special operations
OPERATION_URIS = {}


# Add a location where we can save per thread data
Expand Down
21 changes: 16 additions & 5 deletions slumber/configuration.py
@@ -1,8 +1,10 @@
"""
Implements configuration of the Slumber models available on the server.
"""
from django.core.urlresolvers import reverse

from slumber._caches import DJANGO_MODEL_TO_SLUMBER_MODEL, \
SLUMBER_MODEL_OPERATIONS
OPERATION_URIS
from slumber.connector.configuration import INSTANCE_PROXIES, MODEL_PROXIES
from slumber.server.json import DATA_MAPPING
from slumber.server.meta import get_application
Expand Down Expand Up @@ -74,7 +76,16 @@ def _model(django_model, to_json, properties_ro, operations_extra):
for type_name, function in (to_json or {}).items():
DATA_MAPPING[type_name] = function

ops = SLUMBER_MODEL_OPERATIONS[model]
for operation, name in operations_extra or []:
ops.append(operation(model, name))

for conf in operations_extra or []:
if len(conf) == 2:
operation, name = conf
if operation is None:
del model.operations[name]
else:
model.operations[name] = operation(model, name)
elif len(conf) == 3:
operation, name, uri = conf
slumber_op = operation(model, name,
reverse('slumber.server.views.service_root') + uri + '/')
model.operations[name] = slumber_op
OPERATION_URIS[uri] = slumber_op
46 changes: 27 additions & 19 deletions slumber/connector/ua.py
Expand Up @@ -53,8 +53,7 @@ def _use_fake(url):
logging.debug("Using real HTTP for %s", url)


def _calculate_signature(authn_name, method, url, body,
username, for_fake_client):
def _calculate_signature(authn_name, method, url, body, username):
"""Do the signed request calculation.
"""
# We need all arguments and all locals
Expand Down Expand Up @@ -84,11 +83,7 @@ def _calculate_signature(authn_name, method, url, body,
headers[key] = value
logging.debug("_calculate_signature %s adding headers: %s",
method, headers)
if for_fake_client:
return dict([('HTTP_' + k.upper().replace('-', '_'), v)
for k, v in headers.items()])
else:
return headers
return headers


def for_user(name):
Expand All @@ -111,7 +106,7 @@ def wrapped(*a, **kw):
return decorator


def _sign_request(method, url, body, for_fake_client):
def _sign_request(method, url, body):
"""Calculate the request headers that need to be added so that the
request is properly signed and the Slumber server will consider
the current user to be authenticated.
Expand All @@ -122,29 +117,37 @@ def _sign_request(method, url, body, for_fake_client):
if authn_name:
name = getattr(PER_THREAD, 'username', None)
return _calculate_signature(
authn_name, method, url, body, name, for_fake_client)
authn_name, method, url, body, name)
else:
return {}


def get(url, ttl=0, codes=None):
def _fake_http_headers(headers):
"""Convert the headers into a form suitable for the Fake HTTP client.
"""
return dict([('HTTP_' + k.upper().replace('-', '_'), v)
for k, v in headers.items()])


def get(url, ttl=0, codes=None, headers=None):
"""Perform a GET request against a Slumber server.
"""
return _get(url, ttl, codes)
return _get(url, ttl, codes, headers)


def _get(url, ttl, codes):
def _get(url, ttl, codes, headers):
"""Mockable version of the user agent get.
"""
# Pylint gets confused by the fake HTTP client
# pylint: disable=E1103
url_fragment = _use_fake(url)
codes = codes or [200]
headers = headers or dict(Accept='application/json')
url_fragment = _use_fake(url)
if url_fragment:
file_spec, query = _parse_qs(url_fragment)
headers = _sign_request('GET', file_spec, query, True)
headers.update(_sign_request('GET', file_spec, query))
response = FakeClient().get(file_spec, query,
HTTP_HOST='localhost:8000', **headers)
HTTP_HOST='localhost:8000', **_fake_http_headers(headers))
if response.status_code in [301, 302] and \
response.status_code not in codes:
return get(response['location'], ttl, codes)
Expand All @@ -159,7 +162,7 @@ def _get(url, ttl, codes):
url, cache_key)
_, _, path, _, query, _ = urlparse(url)
for _ in range(0, 3):
headers = _sign_request('GET', path, query or '', False)
headers.update(_sign_request('GET', path, query or ''))
response, content = _real().request(
url, headers=headers)
if response.status in codes:
Expand All @@ -172,7 +175,10 @@ def _get(url, ttl, codes):
logging.debug("Fetched %s from cache key %s", url, cache_key)
response, content = cached
response.from_cache = True
return response, loads(content)
try:
return response, loads(content)
except JSONDecodeError:
return response, {}


def post(url, data, codes=None):
Expand All @@ -188,18 +194,20 @@ def _post(url, data, codes):
# pylint: disable=E1101
# Pylint gets confused by the fake HTTP client
# pylint: disable=E1103
headers = dict(Accept='application/json')
body = dumps(data) if data else ''
url_fragment = _use_fake(url)
if url_fragment:
headers.update(_sign_request('POST', url_fragment, body))
response = FakeClient().post(url_fragment, body,
content_type='application/json',
HTTP_HOST='localhost:8000',
**_sign_request('POST', url_fragment, body, True))
**_fake_http_headers(headers))
assert response.status_code in (codes or [200]), \
(url_fragment, response, response.content)
content = response.content
else:
headers = _sign_request('POST', urlparse(url).path, body, False)
headers.update(_sign_request('POST', urlparse(url).path, body))
headers['Content-Type'] = 'application/json'
response, content = _real().request(url, "POST", body=body,
headers = headers)
Expand Down
32 changes: 29 additions & 3 deletions slumber/operations/__init__.py
@@ -1,7 +1,12 @@
"""
Implements the server side operations on models and instances.
"""
from urllib import quote, urlencode

from django.core.urlresolvers import reverse
from django.db.models import Model

from slumber.server import get_slumber_root


def _forbidden(_request, response, *_):
Expand All @@ -16,12 +21,33 @@ class ModelOperation(object):
"""
METHODS = ['GET', 'OPTIONS', 'POST', 'PUT', 'DELETE']
model_operation = True
def __init__(self, model, name):
def __init__(self, model, name, uri = None):
self.model = model
self.name = name
self.uri = uri
self.regex = ''
self.path = model.path + name + '/'

def __call__(self, *args, **qs):
root = get_slumber_root()
uri = self.uri or (root + self.path)
for part in args:
if issubclass(type(part), Model):
part = part.pk
part = str(part)
if part.startswith('/'):
if part.startswith(root):
uri = part
else:
uri = root + part[1:]
else:
uri += quote(part)
if not uri.endswith('/'):
uri += '/'
if qs:
uri += '?' + urlencode(qs)
return uri

def headers(self, retvalue, request, response):
"""Calculate and place extra headers needed for certain types of
response.
Expand Down Expand Up @@ -57,6 +83,6 @@ class InstanceOperation(ModelOperation):
"""Base class for operations on instances.
"""
model_operation = False
def __init__(self, model, name):
super(InstanceOperation, self).__init__(model, name)
def __init__(self, model, name, uri = None):
super(InstanceOperation, self).__init__(model, name, uri)
self.regex = '([^/]+)/'
4 changes: 1 addition & 3 deletions slumber/operations/authenticate.py
Expand Up @@ -4,7 +4,6 @@
from django.contrib.auth import authenticate

from slumber.operations import ModelOperation
from slumber.server import get_slumber_root


class AuthenticateUser(ModelOperation):
Expand All @@ -19,10 +18,9 @@ def post(self, request, response, _appname, _modelname):
for k, v in request.POST.items()]))
response['authenticated'] = bool(user)
if user:
root = get_slumber_root()
response['user'] = dict(
pk = user.pk,
display_name = unicode(user),
url = root + self.model.path + 'data/%s/' % user.pk)
url = self.model.operations['data'](user))
else:
response['user'] = None
19 changes: 8 additions & 11 deletions slumber/operations/instancedata.py
@@ -1,7 +1,6 @@
"""
Implements the server side for the instance operators.
"""
from slumber._caches import DJANGO_MODEL_TO_SLUMBER_MODEL
from slumber.operations import InstanceOperation
from slumber.server import get_slumber_root
from slumber.server.http import require_user
Expand All @@ -18,8 +17,8 @@ def instance_data(into, model, instance):
'%s/%s/' % ('data', instance.pk)
into['display'] = unicode(instance)
into['operations'] = dict(
[(op.name, root + op.path + '%s/' % instance.pk)
for op in model.operations() if not op.model_operation])
[(op.name, op(instance))
for op in model.operations.values() if not op.model_operation])
into['fields'] = {}
for field, meta in model.fields.items():
into['fields'][field] = dict(
Expand Down Expand Up @@ -53,8 +52,7 @@ def _get_dataset(self, request, response, instance, dataset):
"""Return one page of the array data.
"""
root = get_slumber_root()
response['instance'] = root + self.model.path + '%s/%s/%s/' % (
self.name, str(instance.pk), dataset)
response['instance'] = self(instance, dataset)

try:
query = getattr(instance, dataset + '_set')
Expand All @@ -66,14 +64,13 @@ def _get_dataset(self, request, response, instance, dataset):

response['page'] = []
for obj in query[:10]:
model = DJANGO_MODEL_TO_SLUMBER_MODEL[type(obj)]
model = type(obj).slumber_model
response['page'].append(dict(
type=root + model.path,
pk=obj.pk, display=unicode(obj),
data=root + model.path + 'data/%s/' % obj.pk))
data=model.operations['data'](obj)))

if query.count() > len(response['page']):
response['next_page'] = root + self.model.path + \
'%s/%s/%s/?start_after=%s' % (
self.name, instance.pk, dataset,
response['page'][-1]['pk'])
response['next_page'] = self(instance, dataset,
start_after=response['page'][-1]['pk'])

0 comments on commit 14b360a

Please sign in to comment.