# HG changeset patch # User Filip de Waard # Date 1292706681 -3600 # Node ID 21c3de08ce9cc97400b442e06a02f327322cf5ad # Parent 8237a0077a2dfca8d3f73a37c3106d0e56cc0a60 renamed feed controller to feeds diff -r 8237a0077a2dfca8d3f73a37c3106d0e56cc0a60 -r 21c3de08ce9cc97400b442e06a02f327322cf5ad vix/config/routing.py --- a/vix/config/routing.py Sat Dec 18 22:04:56 2010 +0100 +++ b/vix/config/routing.py Sat Dec 18 22:11:21 2010 +0100 @@ -18,7 +18,7 @@ map.connect('/error/{action}/{id}', controller='error') # Routes for 'feed' controller - map.connect('create_feed', '/feed', controller='feed', + map.connect('create_feed', '/feeds', controller='feeds', action='create', conditions=dict(method='POST')) # Routes for 'api' controller diff -r 8237a0077a2dfca8d3f73a37c3106d0e56cc0a60 -r 21c3de08ce9cc97400b442e06a02f327322cf5ad vix/controllers/feed.py --- a/vix/controllers/feed.py Sat Dec 18 22:04:56 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -vix/controllers/feed.py: Vix Feed controller - -Copyright 2009-2010, Net Collective. - -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. - -""" - -import simplejson as json -import logging - -from pylons import request, response, url, config -from pylons.controllers.util import abort -from pylons.decorators.rest import restrict - -from vix.lib.base import BaseController -from vix.lib.decorators import authenticate, authorize - -import vix.model as model - -log = logging.getLogger(__name__) - -class FeedController(BaseController): - - @restrict('POST') - @authenticate() - @authorize(config['couchdb_database'], '*', 'POST') - def create(self): - """ - Creates a Feed resource. - - This controller method accepts three different input values - through a POST request: a slug suggestion through the 'Slug' - header and the title and subtitle of the feed in JSON format. - - Here is a sample request:: - - POST /feeds HTTTP/1.1 - Host vix.example.org - Content-type: application/json - Slug: blog - Authorization: Basic ZHRhZ2dhcnQ6am9objEyMw== - - {'title': 'Taggart Transcontinental company weblog', - 'subtitle': 'From ocean to ocean.'} - - This method requires the 'application/json' HTTP Media Type - to be passed through the Content-type header (AtomPub support - will be added in the future). If called correctly the JSON - representation of the newly created CouchDB document will be - returned with a 201 status code. - - Requires HTTP Base authentication with a user that has - sufficient privileges. - - """ - - #TODO: add application/atom+xml support - if 'application/json' in request.headers.get('Content-type'): - try: - feed_json = json.loads(request.body) - except: - abort(400, 'Invalid JSON input') - - slug_suggestion = request.headers.get('Slug') - - feed = model.Feed(title=feed_json['title'], - subtitle=feed_json['subtitle']) - - feed.create(model.db, - slug_suggestion=slug_suggestion, - authority=config['tag_uri_authority']) - - response.status = 201 - response.content_type = 'application/json' - - #return a JSON representation of the CouchDB document - return json.dumps(feed.unwrap()) - else: - abort(415, 'Only accepting application/json.') diff -r 8237a0077a2dfca8d3f73a37c3106d0e56cc0a60 -r 21c3de08ce9cc97400b442e06a02f327322cf5ad vix/controllers/feeds.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vix/controllers/feeds.py Sat Dec 18 22:11:21 2010 +0100 @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +""" +vix/controllers/feed.py: Vix Feed controller + +Copyright 2009-2010, Net Collective. + +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. + +""" + +import simplejson as json +import logging + +from pylons import request, response, url, config +from pylons.controllers.util import abort +from pylons.decorators.rest import restrict + +from vix.lib.base import BaseController +from vix.lib.decorators import authenticate, authorize + +import vix.model as model + +log = logging.getLogger(__name__) + +class FeedsController(BaseController): + + @restrict('POST') + @authenticate() + @authorize(config['couchdb_database'], '*', 'POST') + def create(self): + """ + Creates a Feed resource. + + This controller method accepts three different input values + through a POST request: a slug suggestion through the 'Slug' + header and the title and subtitle of the feed in JSON format. + + Here is a sample request:: + + POST /feeds HTTTP/1.1 + Host vix.example.org + Content-type: application/json + Slug: blog + Authorization: Basic ZHRhZ2dhcnQ6am9objEyMw== + + {'title': 'Taggart Transcontinental company weblog', + 'subtitle': 'From ocean to ocean.'} + + This method requires the 'application/json' HTTP Media Type + to be passed through the Content-type header (AtomPub support + will be added in the future). If called correctly the JSON + representation of the newly created CouchDB document will be + returned with a 201 status code. + + Requires HTTP Base authentication with a user that has + sufficient privileges. + + """ + + #TODO: add application/atom+xml support + if 'application/json' in request.headers.get('Content-type'): + try: + feed_json = json.loads(request.body) + except: + abort(400, 'Invalid JSON input') + + slug_suggestion = request.headers.get('Slug') + + feed = model.Feed(title=feed_json['title'], + subtitle=feed_json['subtitle']) + + feed.create(model.db, + slug_suggestion=slug_suggestion, + authority=config['tag_uri_authority']) + + response.status = 201 + response.content_type = 'application/json' + + #return a JSON representation of the CouchDB document + return json.dumps(feed.unwrap()) + else: + abort(415, 'Only accepting application/json.') diff -r 8237a0077a2dfca8d3f73a37c3106d0e56cc0a60 -r 21c3de08ce9cc97400b442e06a02f327322cf5ad vix/tests/functional/test_feed.py --- a/vix/tests/functional/test_feed.py Sat Dec 18 22:04:56 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,141 +0,0 @@ -import re -import base64 -import bcrypt - -from datetime import datetime - -import simplejson as json -from pylons import url - -from vix.tests import TestController -from vix.lib.util import datetime_to_rfc3339 -import vix.model as model -import vix.model.views as views - -#TODO: enforce SSL with https decorator - -_couchdb_rev_re = re.compile('\A[0-9]*-[a-z0-9]{32}\Z') - -class TestFeedController(TestController): - - def test_create_authorization(self): - """Tests the authorization for the Feed controller create method.""" - - perm = {'GET': False, 'POST': True, 'PUT': False, 'DELETE': False} - - #set up a user to pass the authentication check - p = bcrypt.hashpw(u'@!^%$&*()_+@:', bcrypt.gensalt()) - user = model.User(username=u"fmw", password=p) - - #set up basic permissions - user.set_permissions(database=u'vix_tests', feed=u'*', admin=True, - actions=perm) - user.store(model.db) - - #make an request for this user (returns 415 status due to invalid - #HTTP Content-type header) - authentication = 'Basic %s' % (base64.b64encode('fmw:@!^%$&*()_+@:')) - response = self.app.post(url('create_feed'), - headers={'Authorization': authentication}, - status=415) - - #set up a user that shouldn't pass the authentication check - p = bcrypt.hashpw(u'@!^%$&*()_+@:', bcrypt.gensalt()) - user = model.User(username=u"wmf", password=p) - - #set up permissions that shouldn't allow access - perm = {'GET': False, 'POST': False, 'PUT': False, 'DELETE': False} - user.set_permissions(database=u'vix_tests', feed=u'*', admin=True, - actions=perm) - user.store(model.db) - - #make the unauthorized request - authentication = 'Basic %s' % (base64.b64encode('wmf:@!^%$&*()_+@:')) - response = self.app.post(url('create_feed'), - headers={'Authorization': authentication}, - status=403) - - #add another unauthorized user - p = bcrypt.hashpw(u'@!^%$&*()_+@:', bcrypt.gensalt()) - user = model.User(username=u"fmw_", password=p) - - #give this user POST access to a non-'*' controller - perm = {'GET': False, 'POST': True, 'PUT': False, 'DELETE': False} - user.set_permissions(database=u'vix_tests', feed=u'blog', admin=True, - actions=perm) - user.store(model.db) - - authentication = 'Basic %s' % (base64.b64encode('fmw_:@!^%$&*()_+@:')) - response = self.app.post(url('create_feed'), - headers={'Authorization': authentication}, - status=403) - - def test_create_json(self): - """Test creating Feed resources with JSON input.""" - - #add by_slug CouchDB view - views.by_slug.sync(model.db) - - #set up a user to pass the authentication check - p = bcrypt.hashpw(u'123', bcrypt.gensalt()) - user = model.User(username=u"fmw", password=p) - - #set up basic permissions - perm = {'GET': True, 'POST': True, 'PUT': True, 'DELETE': True} - user.set_permissions(database=u'vix_tests', feed=u'*', admin=True, - actions=perm) - user.store(model.db) - - headers = {} - headers['Authorization'] = 'Basic %s' % (base64.b64encode('fmw:123')) - headers['Slug'] = 'weblog' - - new_feed = {'title': u'Vix weblog', 'subtitle': u'The Vix Weblog...'} - - response = self.app.post(url('create_feed', format='json'), - content_type='application/json', - params=json.dumps(new_feed), - headers=headers, - status=201) - - response_json = json.loads(response.body) - - #test the values as they are returned by the controller - #(i.e. the JSON for the object as stored in CouchDB) - self.assertTrue(_couchdb_rev_re.match(response.json['_rev'])) - self.assertEquals(response.json['title'], u'Vix weblog') - self.assertEquals(response.json['subtitle'], u'The Vix Weblog...') - self.assertEquals(response.json['type'], u'feed') - self.assertEquals(response.json['slug'], u'/feeds/weblog') - - #seconds are excluded, but this could possibly backfire if there is - #a discrepancy between the minutes/dates of the two calls, but - #as this is rather unlikely and there is a simple recourse (i.e. - #repeating the tests) this approach will do just fine. - datetime_substr = datetime_to_rfc3339(datetime.utcnow()).rsplit(':')[0] - date_str = datetime_substr.split('T')[0] - - assert datetime_substr in response.json['published'] - assert datetime_substr in response.json['updated'] - - self.assertEquals(response.json['_id'], - u'tag:vix.io,%s:/feeds/weblog' % (date_str)) - - #try sending a request with invalid JSON input - response = self.app.post(url('create_feed', format='json'), - content_type='application/json', - params=u'foobar$ "', - headers=headers, - status=400) - - #test w/o slug - del headers['Slug'] - response = self.app.post(url('create_feed', format='json'), - content_type='application/json', - params=json.dumps(new_feed), - headers=headers, - status=201) - - self.assertEquals(response.json['slug'], u'/feeds/vix-weblog') - self.assertEquals(response.json['_id'], - u'tag:vix.io,%s:/feeds/vix-weblog' % (date_str)) diff -r 8237a0077a2dfca8d3f73a37c3106d0e56cc0a60 -r 21c3de08ce9cc97400b442e06a02f327322cf5ad vix/tests/functional/test_feeds.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vix/tests/functional/test_feeds.py Sat Dec 18 22:11:21 2010 +0100 @@ -0,0 +1,141 @@ +import re +import base64 +import bcrypt + +from datetime import datetime + +import simplejson as json +from pylons import url + +from vix.tests import TestController +from vix.lib.util import datetime_to_rfc3339 +import vix.model as model +import vix.model.views as views + +#TODO: enforce SSL with https decorator + +_couchdb_rev_re = re.compile('\A[0-9]*-[a-z0-9]{32}\Z') + +class TestFeedsController(TestController): + + def test_create_authorization(self): + """Tests the authorization for the Feed controller create method.""" + + perm = {'GET': False, 'POST': True, 'PUT': False, 'DELETE': False} + + #set up a user to pass the authentication check + p = bcrypt.hashpw(u'@!^%$&*()_+@:', bcrypt.gensalt()) + user = model.User(username=u"fmw", password=p) + + #set up basic permissions + user.set_permissions(database=u'vix_tests', feed=u'*', admin=True, + actions=perm) + user.store(model.db) + + #make an request for this user (returns 415 status due to invalid + #HTTP Content-type header) + authentication = 'Basic %s' % (base64.b64encode('fmw:@!^%$&*()_+@:')) + response = self.app.post(url('create_feed'), + headers={'Authorization': authentication}, + status=415) + + #set up a user that shouldn't pass the authentication check + p = bcrypt.hashpw(u'@!^%$&*()_+@:', bcrypt.gensalt()) + user = model.User(username=u"wmf", password=p) + + #set up permissions that shouldn't allow access + perm = {'GET': False, 'POST': False, 'PUT': False, 'DELETE': False} + user.set_permissions(database=u'vix_tests', feed=u'*', admin=True, + actions=perm) + user.store(model.db) + + #make the unauthorized request + authentication = 'Basic %s' % (base64.b64encode('wmf:@!^%$&*()_+@:')) + response = self.app.post(url('create_feed'), + headers={'Authorization': authentication}, + status=403) + + #add another unauthorized user + p = bcrypt.hashpw(u'@!^%$&*()_+@:', bcrypt.gensalt()) + user = model.User(username=u"fmw_", password=p) + + #give this user POST access to a non-'*' controller + perm = {'GET': False, 'POST': True, 'PUT': False, 'DELETE': False} + user.set_permissions(database=u'vix_tests', feed=u'blog', admin=True, + actions=perm) + user.store(model.db) + + authentication = 'Basic %s' % (base64.b64encode('fmw_:@!^%$&*()_+@:')) + response = self.app.post(url('create_feed'), + headers={'Authorization': authentication}, + status=403) + + def test_create_json(self): + """Test creating Feed resources with JSON input.""" + + #add by_slug CouchDB view + views.by_slug.sync(model.db) + + #set up a user to pass the authentication check + p = bcrypt.hashpw(u'123', bcrypt.gensalt()) + user = model.User(username=u"fmw", password=p) + + #set up basic permissions + perm = {'GET': True, 'POST': True, 'PUT': True, 'DELETE': True} + user.set_permissions(database=u'vix_tests', feed=u'*', admin=True, + actions=perm) + user.store(model.db) + + headers = {} + headers['Authorization'] = 'Basic %s' % (base64.b64encode('fmw:123')) + headers['Slug'] = 'weblog' + + new_feed = {'title': u'Vix weblog', 'subtitle': u'The Vix Weblog...'} + + response = self.app.post(url('create_feed', format='json'), + content_type='application/json', + params=json.dumps(new_feed), + headers=headers, + status=201) + + response_json = json.loads(response.body) + + #test the values as they are returned by the controller + #(i.e. the JSON for the object as stored in CouchDB) + self.assertTrue(_couchdb_rev_re.match(response.json['_rev'])) + self.assertEquals(response.json['title'], u'Vix weblog') + self.assertEquals(response.json['subtitle'], u'The Vix Weblog...') + self.assertEquals(response.json['type'], u'feed') + self.assertEquals(response.json['slug'], u'/feeds/weblog') + + #seconds are excluded, but this could possibly backfire if there is + #a discrepancy between the minutes/dates of the two calls, but + #as this is rather unlikely and there is a simple recourse (i.e. + #repeating the tests) this approach will do just fine. + datetime_substr = datetime_to_rfc3339(datetime.utcnow()).rsplit(':')[0] + date_str = datetime_substr.split('T')[0] + + assert datetime_substr in response.json['published'] + assert datetime_substr in response.json['updated'] + + self.assertEquals(response.json['_id'], + u'tag:vix.io,%s:/feeds/weblog' % (date_str)) + + #try sending a request with invalid JSON input + response = self.app.post(url('create_feed', format='json'), + content_type='application/json', + params=u'foobar$ "', + headers=headers, + status=400) + + #test w/o slug + del headers['Slug'] + response = self.app.post(url('create_feed', format='json'), + content_type='application/json', + params=json.dumps(new_feed), + headers=headers, + status=201) + + self.assertEquals(response.json['slug'], u'/feeds/vix-weblog') + self.assertEquals(response.json['_id'], + u'tag:vix.io,%s:/feeds/vix-weblog' % (date_str))