Skip to content

Commit

Permalink
Merge branch 'feature/html' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffknupp committed Jul 25, 2013
2 parents f6a471b + 619b958 commit 41386e7
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 25 deletions.
2 changes: 1 addition & 1 deletion sandman/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def register(cls):
endpoint.
:param cls: User-defined class derived from :class:`sandman.Resource` to be
registered with the endpoint returned by :func:`endpoint()`
registered with the endpoint returned by :func:`endpoint()`
:type cls: :class:`sandman.Resource` or tuple
"""
Expand Down
73 changes: 60 additions & 13 deletions sandman/sandman.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,54 @@
"""Sandman REST API creator for Flask and SQLAlchemy"""

from flask import jsonify, request, g, current_app, Response
from flask import (jsonify, request, g,
current_app, Response, render_template,
make_response)
from . import app, db
from .exception import JSONException

JSON, HTML = range(2)

def _get_session():
"""Return (and memoize) a database session"""
session = getattr(g, '_session', None)
if session is None:
session = g._session = db.session()
return session

def _get_mimetype(current_request):
"""Return the mimetype for this request."""
if 'Accept' not in current_request.headers:
return JSON

if 'json' in current_request.headers['Accept']:
return JSON
else:
return HTML

def _single_resource_json_response(resource):
"""Return the JSON representation of *resource*"""
return jsonify(resource.as_dict())

def _single_resource_html_response(resource):
"""Return the HTML representation of *resource*"""
tablename = resource.__tablename__
resource.pk = getattr(resource, resource.primary_key())
resource.attributes = resource.as_dict()
return make_response(render_template('resource.html', resource=resource,
tablename=tablename))

def _collection_json_response(resources):
"""Return the HTML representation of the collection *resources*"""
result_list = []
for resource in resources:
result_list.append(resource.as_dict())
return jsonify(resources=result_list)

def _collection_html_response(resources):
"""Return the HTML representation of the collection *resources*"""
return make_response(render_template('collection.html',
resources=resources))

def _validate(cls, method, resource=None):
"""Return ``True`` if the the given *cls* supports the HTTP *method* found
on the incoming HTTP request.
Expand All @@ -26,26 +64,37 @@ def _validate(cls, method, resource=None):
if not method in cls.__methods__:
return False

class_validator_name = 'do_' + method
class_validator_name = 'validate_' + method

if hasattr(cls, class_validator_name):
class_validator = getattr(cls, class_validator_name)
return class_validator(resource)

return True

def resource_created_response(resource):
def resource_created_response(resource, current_request):
"""Return HTTP response with status code *201*, signaling a created *resource*
:param resource: resource created as a result of current request
:type resource: :class:`sandman.model.Resource`
:rtype: :class:`flask.Response`
"""
response = jsonify(resource.as_dict())
if _get_mimetype(current_request) == JSON:
response = _single_resource_json_response(resource)
else:
response = _single_resource_html_response(resource)
response.status_code = 201
response.headers['Location'] = 'http://localhost:5000/' + resource.resource_uri()
return response

def resource_response(resource, current_request):
result_dict = resource.as_dict()
if _get_mimetype(current_request) == JSON:
return _single_resource_json_response(resource)
else:
return _single_resource_html_response(resource)


def unsupported_method_response():
"""Return the appropriate *Response* with status code *403*, signaling the HTTP method used
in the request is not supported for the given endpoint.
Expand Down Expand Up @@ -100,14 +149,13 @@ def patch_resource(collection, lookup_id):
setattr(resource, resource.primary_key(), lookup_id)
session.add(resource)
session.commit()
return resource_created_response(resource)
return resource_created_response(resource, request)
else:
resource.from_dict(request.json)
session.merge(resource)
session.commit()
return no_content_response()


@app.route('/<collection>', methods=['POST'])
def add_resource(collection):
"""Return the appropriate *Response* based on adding a new resource to *collection*
Expand All @@ -127,7 +175,7 @@ def add_resource(collection):
session = _get_session()
session.add(resource)
session.commit()
return resource_created_response(resource)
return resource_created_response(resource, request)

@app.route('/<collection>/<lookup_id>', methods=['DELETE'])
def delete_resource(collection, lookup_id):
Expand Down Expand Up @@ -173,8 +221,7 @@ def resource_handler(collection, lookup_id):
elif not _validate(cls, request.method, resource):
return unsupported_method_response()

result_dict = resource.as_dict()
return jsonify(**result_dict)
return resource_response(resource, request)

@app.route('/<collection>', methods=['GET'])
def collection_handler(collection):
Expand All @@ -193,7 +240,7 @@ def collection_handler(collection):
if not _validate(cls, request.method, resources):
return unsupported_method_response()

result_list = []
for resource in resources:
result_list.append(resource.as_dict())
return jsonify(resources=result_list)
if _get_mimetype(request) == JSON:
return _collection_json_response(resources)
else:
return _collection_html_response(resources)
24 changes: 24 additions & 0 deletions sandman/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

</head>
<body>
<!--[if lt IE 7]>
<p class="chromeframe">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> or <a href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome Frame</a> to improve your experience.</p>
<![endif]-->

<!-- This code is taken from http://twitter.github.com/bootstrap/examples/hero.html -->

{% block body %}
{% endblock body %}
</body>
</html>
20 changes: 20 additions & 0 deletions sandman/templates/collection.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block body %}

<h3>{{ resources[0].__tablename__ }}</h3>
<ol>
{% for resource in resources %}
<li>
<ul>
{% for attribute, value in resource.as_dict().items() %}
{% if attribute == 'links' %}
<li>{{ attribute }}: <a href="{{ value[0].uri }}">{{ value[0].rel }}</a></li>
{% else %}
<li>{{ attribute }}: {{ value }}</li>
{% endif %}
{% endfor %}
</ul>
</li>
{% endfor %}
</ol>
{% endblock %}
15 changes: 15 additions & 0 deletions sandman/templates/resource.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block body %}

<h3>{{ tablename }}</h3>
<h4>{{ resource.pk }}</h4>
<ul>
{% for attribute, value in resource.attributes.items() %}
{% if attribute == 'links' %}
<li>{{ attribute }}: <a href="{{ value[0].uri }}">{{ value[0].rel }}</a></li>
{% else %}
<li>{{ attribute }}: {{ value }}</li>
{% endif %}
{% endfor %}
</ul>
{% endblock %}
4 changes: 3 additions & 1 deletion sandman/test/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ class Artist(Model):

class Album(Model):
__tablename__ = 'Album'
__methods__ = ('POST', 'PATCH', 'DELETE')

class Playlist(Model):
__tablename__ = 'Playlist'
__methods__ = ('GET', 'POST', 'PATCH')

class Genre(Model):
__tablename__ = 'Genre'
__endpoint__ = 'styles'
__methods__ = ('GET', 'DELETE')

@staticmethod
def do_GET(resource=None):
def validate_GET(resource=None):
if isinstance(resource, list):
return True
elif resource and resource.GenreId == 1:
Expand Down
51 changes: 51 additions & 0 deletions sandman/test/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/pygments.css">
<style>
body {
padding-top: 60px;
padding-bottom: 40px;
}
</style>
<link rel="stylesheet" href="/static/css/bootstrap-responsive.min.css">
<link rel="stylesheet" href="/static/css/main.css">

<script src="/static/js/vendor/modernizr-2.6.2-respond-1.1.0.min.js"></script>
</head>
<body>
<!--[if lt IE 7]>
<p class="chromeframe">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> or <a href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome Frame</a> to improve your experience.</p>
<![endif]-->

<!-- This code is taken from http://twitter.github.com/bootstrap/examples/hero.html -->

<div class="container">
<div class="row-fluid">
<div class="span8 offset2">
{% block body %}
{% endblock body %}
</div>
</div>
</div> <!-- /container -->


<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="/static/js/vendor/jquery-1.9.1.min.js"><\/script>')</script>

<script src="/static/js/vendor/bootstrap.min.js"></script>

<script src="/static/js/plugins.js"></script>
<script src="/static/js/main.js"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions sandman/test/templates/resource.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block body %}

<h3>{{ tablename }}</h3>
<h4>{{ resource.pk }}</h4>
<ul>
{% for attribute in resource.attributes %}
<li>{{ attribute }}</li>
{% endfor %}
</li>
{% endblock %}
56 changes: 46 additions & 10 deletions sandman/test/test_sandman.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ class SandmanTestCase(unittest.TestCase):
DB_LOCATION = os.path.join(os.getcwd(), 'sandman', 'test', 'chinook')
def setUp(self):

if os.path.exists(self.DB_LOCATION):
os.unlink(self.DB_LOCATION)
shutil.copy(os.path.join(os.getcwd(), 'sandman', 'test', 'data', 'chinook'), self.DB_LOCATION)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + self.DB_LOCATION
app.config['TESTING'] = True
Expand Down Expand Up @@ -54,10 +52,10 @@ def test_patch_new_resource(self):
response = self.app.patch('/artists/276',
content_type='application/json',
data=json.dumps({u'Name': u'Jeff Knupp'}))
#assert response.status_code == 201
assert response.status_code == 201
assert type(response.data) == str
#assert json.loads(response.data) == u'Jeff Knupp'
#self.assertEqual(json.loads(response.data)['links'], [{u'rel': u'self', u'uri': u'/artists/276'}])
assert json.loads(response.data)['Name'] == u'Jeff Knupp'
self.assertEqual(json.loads(response.data)['links'], [{u'rel': u'self', u'uri': u'/artists/276'}])

def test_patch_existing_resource(self):
response = self.app.patch('/artists/275',
Expand All @@ -75,11 +73,13 @@ def test_delete_resource(self):
response = self.app.get('/artists/275')
assert response.status_code == 404

def test_get_user_defined_endpoint(self):
response = self.app.get('/styles')
assert response.status_code == 200
assert response.data
assert len(json.loads(response.data)[u'resources']) == 25
def test_delete_non_existant_resource(self):
response = self.app.delete('/artists/404')
assert response.status_code == 404

def test_delete_not_supported(self):
response = self.app.delete('/playlists/1')
assert response.status_code == 403

def test_get_user_defined_methods(self):
response = self.app.post('/styles',
Expand All @@ -88,11 +88,22 @@ def test_get_user_defined_methods(self):
assert response.status_code == 403
assert not response.data

def test_get_unsupported_resource_method(self):
response = self.app.patch('/styles/26',
content_type='application/json',
data=json.dumps({u'Name': u'Hip-Hop'}))
assert response.status_code == 403

def test_get_supported_method(self):
response = self.app.get('/styles/5')
assert response.status_code == 200


def test_get_unsupported_collection_method(self):
response = self.app.get('/albums')
assert response.status_code == 403


def test_get_user_defined_endpoint(self):
response = self.app.get('/styles')
assert response.status_code == 200
Expand All @@ -102,3 +113,28 @@ def test_get_user_defined_endpoint(self):
def test_user_validation(self):
response = self.app.get('/styles/1')
assert response.status_code == 403

def test_get_html(self):
response = self.app.get('/artists/1', headers={'Accept': 'text/html'})
assert response.status_code == 200
assert '<!DOCTYPE html>' in response.data

def test_get_html_collection(self):
response = self.app.get('/artists', headers={'Accept': 'text/html'})
assert response.status_code == 200
assert '<!DOCTYPE html>' in response.data
assert 'Aerosmith' in response.data

def test_explicit_get_json(self):
response = self.app.get('/artists', headers={'Accept': 'application/json'})
assert response.status_code == 200
assert response.data
assert len(json.loads(response.data)[u'resources']) == 275

def test_post_html_response(self):
response = self.app.post('/artists',
content_type='application/json',
headers={'Accept': 'text/html'},
data=json.dumps({u'Name': u'Jeff Knupp'}))
assert response.status_code == 201
assert 'Jeff Knupp' in response.data

0 comments on commit 41386e7

Please sign in to comment.