# HG changeset patch # User Filip de Waard # Date 1293841982 -3600 # Node ID 8110a06a2e6e3078cd96387878bcd603cb376036 # Parent f127b973b6ce5338d2e0b80ed2788ee653e1fec4 prototyping entry controller create method diff -r f127b973b6ce5338d2e0b80ed2788ee653e1fec4 -r 8110a06a2e6e3078cd96387878bcd603cb376036 vix/controllers/entries.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vix/controllers/entries.py Sat Jan 01 01:33:02 2011 +0100 @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +""" +vix/controllers/entries.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 +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 +from vix.lib.auth import authorize + +import vix.model as model + +log = logging.getLogger(__name__) + +class EntriesController(BaseController): + + @restrict('POST') + @authenticate() + def create(self): + """ + Creates a Entry resource. + + Here is a sample request:: + + POST /entries HTTTP/1.1 + Host vix.example.org + Content-type: application/json + Slug: Welcome to my website + 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. + + """ + + #user is already set by the authenticate decorator + user = config['pylons.app_globals'].user + db_name = config['couchdb_database'] + + #TODO: add application/atom+xml support + if 'application/json' in request.headers.get('Content-type'): + try: + json = simplejson.loads(request.body) + except: + abort(400, 'Invalid JSON input') + + if not 'feeds' in json or len(json['feeds']) == 0: + abort(403, 'Insufficient privileges to perform this action') + + for feed in json['feeds']: + if feed == '*' or not authorize(user, db_name, feed, 'POST'): + abort(403, 'Insufficient privileges to perform action.') + + if not 'title' in json or len(json['title']) == 0: + abort(400, 'Title required for feed creation') + + slug_suggestion = request.headers.get('Slug') + + draft = False if not 'draft' in json else json['draft'] + + entry = model.Entry(feeds=json['feeds'], + title=json['title'], + subtitle=json.get('subtitle'), + content=json['content'], + summary=json.get('summary'), + draft=draft, + rights=json.get('rights'), + authors=json['authors'], + contributors=json.get('contributors'), + categories=json.get('categories') + ) + + entry.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 simplejson.dumps(entry.unwrap()) + else: + abort(415, 'Only accepting application/json.') diff -r f127b973b6ce5338d2e0b80ed2788ee653e1fec4 -r 8110a06a2e6e3078cd96387878bcd603cb376036 vix/model/__init__.py --- a/vix/model/__init__.py Thu Dec 30 01:34:53 2010 +0100 +++ b/vix/model/__init__.py Sat Jan 01 01:33:02 2011 +0100 @@ -336,6 +336,7 @@ """ + type = mapping.TextField(default=u'entry') feeds = mapping.ListField(mapping.TextField()) title = mapping.TextField() subtitle = mapping.TextField() diff -r f127b973b6ce5338d2e0b80ed2788ee653e1fec4 -r 8110a06a2e6e3078cd96387878bcd603cb376036 vix/tests/functional/test_entries.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vix/tests/functional/test_entries.py Sat Jan 01 01:33:02 2011 +0100 @@ -0,0 +1,219 @@ +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 TestEntriesController(TestController): + + def test_create_authorization(self): + """Tests the authorization for the Entry 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.set_permissions(database=u'vix_tests', feed=u'blog', admin=False, + actions=perm) + user.set_permissions(database=u'vix_tests', feed=u'pages', admin=False, + actions=perm) + user.store(model.db) + + authentication = 'Basic %s' % (base64.b64encode('fmw:@!^%$&*()_+@:')) + + #make an request for this user (returns 400) + response = self.app.post(url('create_entry'), + headers={'Authorization': authentication}, + content_type='application/json', + params=json.dumps({'feeds': ['blog', 'pages']}), + status=400) + + #passing '*' as a Feed name should make authorization fail: + response = self.app.post(url('create_entry'), + headers={'Authorization': authentication}, + content_type='application/json', + params=json.dumps({'feeds': ['blog', 'pages', '*']}), + status=403) + + #perform another request for this user, with an unauthorized feed + response = self.app.post(url('create_entry'), + headers={'Authorization': authentication}, + content_type='application/json', + params=json.dumps({'feeds': ['blog', 'pages', 'videos']}), + status=403) + + #perform another request, but this time w/o a feed + response = self.app.post(url('create_entry'), + headers={'Authorization': authentication}, + content_type='application/json', + params=json.dumps({'feeds': []}), + status=403) + + #and now ommitting the feed paramater entirely: + response = self.app.post(url('create_entry'), + headers={'Authorization': authentication}, + content_type='application/json', + params=json.dumps({}), + status=403) + + #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'blog', admin=True, + actions=perm) + user.set_permissions(database=u'vix_tests', feed=u'pages', admin=True, + actions=perm) + user.store(model.db) + + #make the unauthorized request + authentication = 'Basic %s' % (base64.b64encode('wmf:@!^%$&*()_+@:')) + response = self.app.post(url('create_entry'), + headers={'Authorization': authentication}, + content_type='application/json', + params=json.dumps({'feeds': ['blog', 'pages']}), + status=403) + + #add a user that only has access to one of the feeds + p = bcrypt.hashpw(u'@!^%$&*()_+@:', bcrypt.gensalt()) + user = model.User(username=u"fmw_", password=p) + + perm = {'GET': False, 'POST': True , 'PUT': False, 'DELETE': False} + user.set_permissions(database=u'vix_tests', feed=u'blog', admin=True, + actions=perm) + + perm['POST'] = False + user.set_permissions(database=u'vix_tests', feed=u'pages', admin=True, + actions=perm) + + user.store(model.db) + + authentication = 'Basic %s' % (base64.b64encode('fmw_:@!^%$&*()_+@:')) + response = self.app.post(url('create_entry'), + headers={'Authorization': authentication}, + content_type='application/json', + params=json.dumps({'feeds': ['blog', 'pages']}), + status=403) + + def test_create_json(self): + """Test creating Entry resources with JSON input.""" + + #add by_slug CouchDB view + views.by_slug.sync(model.db) + + blog_feed = model.Feed(title=u'blog') + blog_feed.create(model.db, slug_suggestion='blog', authority='vix.io') + + #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=blog_feed.id, + admin=True, + actions=perm) + user.store(model.db) + + headers = {} + headers['Authorization'] = 'Basic %s' % (base64.b64encode('fmw:123')) + headers['Slug'] = 'first' + + new_entry = {'feeds': [blog_feed.id], + 'title': 'Welcome to my weblog', + 'subtitle': 'This is my first post!', + 'summary': 'In this first post I explain what this site is about', + 'rights': 'All rights reserved.', + 'content': {'type': 'text/plain', + 'content': 'Hello, my dear reader...'} + } + + authors = [ + {'name': 'fmw', 'uri': 'http://vix.io/', 'email': 'fmw@vix.io'}] + + contributors = [ + {'name': 'fmw_', 'uri': 'http://vix.io', 'email': 'fmw_@vix.io'}] + + categories = [{'term': 'general', 'scheme': 'foo', 'label': 'General'}] + + new_entry['authors'] = authors + new_entry['contributors'] = contributors + new_entry['categories'] = categories + + response = self.app.post(url('create_entry'), + content_type='application/json', + params=json.dumps(new_entry), + 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['type'], u'entry') + self.assertEquals(response.json['feeds'], [blog_feed.id]) + self.assertEquals(response.json['title'], u'Welcome to my weblog') + self.assertEquals(response.json['subtitle'], u'This is my first post!') + self.assertEquals(response.json['summary'], + u'In this first post I explain what this site is about') + self.assertEquals(response.json['rights'], u'All rights reserved.') + self.assertEquals(response.json['content'], + {'type': 'text/plain', 'content': 'Hello, my dear reader...'}) + + self.assertEquals(response.json['authors'], authors) + self.assertEquals(response.json['contributors'], contributors) + self.assertEquals(response.json['categories'], categories) + + #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:/%s/first' % ( + date_str, date_str.replace('-', '/'))) + + #try sending a request with invalid JSON input + response = self.app.post(url('create_entry'), + content_type='application/json', + params=u'foobar$ "', + headers=headers, + status=400) + + #test w/o slug + del headers['Slug'] + response = self.app.post(url('create_entry'), + content_type='application/json', + params=json.dumps(new_entry), + headers=headers, + status=201) + + self.assertEquals(response.json['_id'], + u'tag:vix.io,%s:/%s/welcome-to-my-weblog' % ( + date_str, date_str.replace('-', '/'))) diff -r f127b973b6ce5338d2e0b80ed2788ee653e1fec4 -r 8110a06a2e6e3078cd96387878bcd603cb376036 vix/tests/test_models.py --- a/vix/tests/test_models.py Thu Dec 30 01:34:53 2010 +0100 +++ b/vix/tests/test_models.py Sat Jan 01 01:33:02 2011 +0100 @@ -298,6 +298,9 @@ def test_Feed(self): """Test model.Feed object.""" + + #FIXME: remove slug field, derive from id instead (like for entrties) + #for consistency (or change entries instead) #add by_slug CouchDB view views.by_slug.sync(model.db) @@ -433,6 +436,8 @@ contributors=contributors, categories=categories) + #FIXME: add draft + blog_feed = model.Feed(title=u'Vix Weblog') photo_feed = model.Feed(title=u'Vix Photos')