Return-Path: X-Original-To: apmail-incubator-allura-commits-archive@minotaur.apache.org Delivered-To: apmail-incubator-allura-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id C8B7810D3A for ; Wed, 7 Aug 2013 01:12:10 +0000 (UTC) Received: (qmail 3083 invoked by uid 500); 7 Aug 2013 01:12:10 -0000 Delivered-To: apmail-incubator-allura-commits-archive@incubator.apache.org Received: (qmail 3063 invoked by uid 500); 7 Aug 2013 01:12:10 -0000 Mailing-List: contact allura-commits-help@incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: allura-dev@incubator.apache.org Delivered-To: mailing list allura-commits@incubator.apache.org Received: (qmail 3030 invoked by uid 99); 7 Aug 2013 01:12:10 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 07 Aug 2013 01:12:10 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 71C211BCF0; Wed, 7 Aug 2013 01:12:10 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: johnsca@apache.org To: allura-commits@incubator.apache.org Date: Wed, 07 Aug 2013 01:12:11 -0000 Message-Id: In-Reply-To: <926149429d0e4571a7e6b155aa429d92@git.apache.org> References: <926149429d0e4571a7e6b155aa429d92@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [2/8] git commit: [#3154] ticket:391 ForgeBlog REST API [#3154] ticket:391 ForgeBlog REST API Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/af5c8e68 Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/af5c8e68 Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/af5c8e68 Branch: refs/heads/cj/6464 Commit: af5c8e683f96007b64108a0e89846c949e96ad3e Parents: 0065ba1 Author: Yuriy Arhipov Authored: Wed Jul 10 14:57:48 2013 +0400 Committer: Dave Brondsema Committed: Wed Jul 31 21:00:24 2013 +0000 ---------------------------------------------------------------------- ForgeBlog/forgeblog/main.py | 87 +++++++++- ForgeBlog/forgeblog/model/blog.py | 9 + .../forgeblog/tests/functional/test_rest.py | 169 +++++++++++++++++++ 3 files changed, 264 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/af5c8e68/ForgeBlog/forgeblog/main.py ---------------------------------------------------------------------- diff --git a/ForgeBlog/forgeblog/main.py b/ForgeBlog/forgeblog/main.py index 5f54a38..0f4e7e6 100644 --- a/ForgeBlog/forgeblog/main.py +++ b/ForgeBlog/forgeblog/main.py @@ -30,6 +30,7 @@ from paste.deploy.converters import asbool import formencode from formencode import validators from webob import exc +from urllib import unquote from ming.orm import session @@ -45,7 +46,7 @@ from allura.lib.widgets.subscriptions import SubscribeForm from allura.lib.widgets import form_fields as ffw from allura.lib.widgets.search import SearchResults, SearchHelp from allura import model as M -from allura.controllers import BaseController, AppDiscussionController +from allura.controllers import BaseController, AppDiscussionController, AppDiscussionRestController from allura.controllers.feed import FeedArgs, FeedController # Local imports @@ -101,6 +102,7 @@ class ForgeBlogApp(Application): Application.__init__(self, project, config) self.root = RootController() self.admin = BlogAdminController(self) + self.api_root = RootRestController() @Property def external_feeds_list(): @@ -439,3 +441,86 @@ class BlogAdminController(DefaultAdminController): flash('Invalid link(s): %s' % ','.join(link for link in invalid_list), 'error') redirect(c.project.url()+'admin/tools') + + +class RootRestController(BaseController): + def __init__(self): + self._discuss = AppDiscussionRestController() + + def _check_security(self): + require_access(c.app, 'read') + + @expose('json:') + def index(self, title='', text='', state='draft', labels='', **kw): + if request.method == 'POST': + require_access(c.app, 'write') + post = BM.BlogPost() + post.title = title + post.state = state + post.text = text + post.labels = labels.split(',') + post.neighborhood_id = c.project.neighborhood_id + post.make_slug() + M.Thread.new(discussion_id=post.app_config.discussion_id, + ref_id=post.index_id(), + subject='%s discussion' % post.title) + + post.viewable_by = ['all'] + post.commit() + return post.__json__() + else: + post_titles = [] + query_filter = dict(app_config_id=c.app.config._id, deleted=False) + if not has_access(c.app, 'write')(): + query_filter['state'] = 'published' + posts = BM.BlogPost.query.find(query_filter) + for post in posts: + if has_access(post, 'read')(): + post_titles.append({'title': post.title, 'url': h.absurl('/rest' + post.url())}) + return dict(posts=post_titles) + + @expose() + def _lookup(self, year=None, month=None, title=None, *rest): + if not (year and month and title): + raise exc.HTTPNotFound() + slug = '/'.join((year, month, urllib2.unquote(title).decode('utf-8'))) + post = BM.BlogPost.query.get(slug=slug, app_config_id=c.app.config._id) + if not post: + raise exc.HTTPNotFound() + return PostRestController(post), rest + + +class PostRestController(BaseController): + + def __init__(self, post): + self.post = post + + def _check_security(self): + if self.post: + require_access(self.post, 'read') + + @h.vardec + @expose('json:') + def index(self, **kw): + if request.method == 'POST': + return self._update_post(**kw) + else: + if self.post.state == 'draft': + require_access(self.post, 'write') + return self.post.__json__() + + def _update_post(self, **post_data): + require_access(self.post, 'write') + if 'delete' in post_data: + self.post.delete() + return {} + if 'title' in post_data: + self.post.title = post_data['title'] + if 'text' in post_data: + self.post.text = post_data['text'] + if 'state' in post_data: + self.post.state = post_data['state'] + if 'labels' in post_data: + self.post.labels = post_data['labels'].split(',') + self.post.commit() + return self.post.__json__() \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/af5c8e68/ForgeBlog/forgeblog/model/blog.py ---------------------------------------------------------------------- diff --git a/ForgeBlog/forgeblog/model/blog.py b/ForgeBlog/forgeblog/model/blog.py index b43cb8e..07983a0 100644 --- a/ForgeBlog/forgeblog/model/blog.py +++ b/ForgeBlog/forgeblog/model/blog.py @@ -256,6 +256,15 @@ class BlogPost(M.VersionedArtifact, ActivityObject): M.Notification.post( artifact=self, topic='metadata', text=description, subject=subject) + def __json__(self): + return dict(super(BlogPost, self).__json__(), + title=self.title, + url=h.absurl('/rest' + self.url()), + text=self.text, + labels=self.labels, + state=self.state) + + class Attachment(M.BaseAttachment): ArtifactClass=BlogPost class __mongometa__: http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/af5c8e68/ForgeBlog/forgeblog/tests/functional/test_rest.py ---------------------------------------------------------------------- diff --git a/ForgeBlog/forgeblog/tests/functional/test_rest.py b/ForgeBlog/forgeblog/tests/functional/test_rest.py new file mode 100644 index 0000000..5addcde --- /dev/null +++ b/ForgeBlog/forgeblog/tests/functional/test_rest.py @@ -0,0 +1,169 @@ +# coding: utf-8 + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 nose.tools import assert_equal +from allura.lib import helpers as h +from allura.tests import decorators as td +from allura import model as M +from alluratest.controller import TestRestApiBase +from forgeblog import model as BM + + +class TestBlogApi(TestRestApiBase): + + def setUp(self): + super(TestBlogApi, self).setUp() + self.setup_with_tools() + + @td.with_tool('test', 'Blog', 'blog') + def setup_with_tools(self): + h.set_context('test', 'blog', neighborhood='Projects') + + def test_create_post(self): + data = { + 'title': 'test', + 'text': 'test text', + 'state': 'published', + 'labels': 'label1, label2' + } + r = self.api_post('/rest/p/test/blog/', **data) + assert_equal(r.status_int, 200) + url = '/rest' + BM.BlogPost.query.find().first().url() + r = self.api_get('/rest/p/test/blog/') + assert_equal(r.json['posts'][0]['title'], 'test') + assert_equal(r.json['posts'][0]['url'], h.absurl(url)) + + r = self.api_get(url) + assert_equal(r.json['title'], 'test') + assert_equal(r.json['text'], data['text']) + assert_equal(r.json['state'], data['state']) + assert_equal(r.json['labels'], data['labels'].split(',')) + + def test_update_post(self): + data = { + 'title': 'test', + 'text': 'test text', + 'state': 'published', + 'labels': 'label1, label2' + } + r = self.api_post('/rest/p/test/blog/', **data) + assert_equal(r.status_int, 200) + url = '/rest' + BM.BlogPost.query.find().first().url() + data = { + 'text': 'test text2', + 'state': 'draft', + 'labels': 'label3' + } + self.api_post(url, **data) + r = self.api_get(url) + assert_equal(r.json['title'], 'test') + assert_equal(r.json['text'], data['text']) + assert_equal(r.json['state'], data['state']) + assert_equal(r.json['labels'], data['labels'].split(',')) + + def test_delete_post(self): + data = { + 'title': 'test', + 'state': 'published', + 'labels': 'label1, label2' + } + r = self.api_post('/rest/p/test/blog/', **data) + assert_equal(r.status_int, 200) + url = '/rest' + BM.BlogPost.query.find().first().url() + self.api_post(url, delete='') + r = self.api_get(url) + assert_equal(r.status_int, 404) + + def test_post_does_not_exist(self): + r = self.api_get('/rest/p/test/blog/2013/07/fake/') + assert_equal(r.status_int, 404) + + def test_read_permissons(self): + self.api_post('/rest/p/test/blog/', title='test', text='test text', state='published') + self.app.get('/rest/p/test/blog/', extra_environ={'username': '*anonymous'}, status=200) + p = M.Project.query.get(shortname='test') + acl = p.app_instance('blog').config.acl + anon = M.ProjectRole.by_name('*anonymous')._id + anon_read = M.ACE.allow(anon, 'read') + acl.remove(anon_read) + self.app.get('/rest/p/test/blog/', + extra_environ={'username': '*anonymous'}, + status=401) + + def test_new_post_permissons(self): + self.app.post('/rest/p/test/blog/', + params=dict(title='test', text='test text', state='published'), + extra_environ={'username': '*anonymous'}, + status=401) + p = M.Project.query.get(shortname='test') + acl = p.app_instance('blog').config.acl + anon = M.ProjectRole.by_name('*anonymous')._id + anon_write = M.ACE.allow(anon, 'write') + acl.append(anon_write) + self.app.post('/rest/p/test/blog/', + params=dict(title='test', text='test text', state='published'), + extra_environ={'username': '*anonymous'}, + status=200) + + def test_update_post_permissons(self): + self.api_post('/rest/p/test/blog/', title='test', text='test text', state='published') + url = '/rest' + BM.BlogPost.query.find().first().url() + self.app.post(url.encode('utf-8'), + params=dict(title='test2', text='test text2', state='published'), + extra_environ={'username': '*anonymous'}, + status=401) + p = M.Project.query.get(shortname='test') + acl = p.app_instance('blog').config.acl + anon = M.ProjectRole.by_name('*anonymous')._id + anon_write = M.ACE.allow(anon, 'write') + acl.append(anon_write) + self.app.post(url.encode('utf-8'), + params=dict(title='test2', text='test text2', state='published'), + extra_environ={'username': '*anonymous'}, + status=200) + r = self.api_get(url) + assert_equal(r.json['title'], 'test2') + assert_equal(r.json['text'], 'test text2') + assert_equal(r.json['state'], 'published') + + def test_permission_draft_post(self): + self.api_post('/rest/p/test/blog/', title='test', text='test text', state='draft') + r = self.app.get('/rest/p/test/blog/', extra_environ={'username': '*anonymous'}) + assert_equal(r.json, {'posts': []}) + url = '/rest' + BM.BlogPost.query.find().first().url() + self.app.post(url.encode('utf-8'), + params=dict(title='test2', text='test text2', state='published'), + extra_environ={'username': '*anonymous'}, + status=401) + p = M.Project.query.get(shortname='test') + acl = p.app_instance('blog').config.acl + anon = M.ProjectRole.by_name('*anonymous')._id + anon_write = M.ACE.allow(anon, 'write') + acl.append(anon_write) + r = self.app.get('/rest/p/test/blog/', extra_environ={'username': '*anonymous'}) + assert_equal(r.json['posts'][0]['title'], 'test') + + def test_draft_post(self): + self.api_post('/rest/p/test/blog/', title='test', text='test text', state='draft') + r = self.app.get('/rest/p/test/blog/', extra_environ={'username': '*anonymous'}) + assert_equal(r.json, {'posts': []}) + url = '/rest' + BM.BlogPost.query.find().first().url() + self.api_post(url, state='published') + r = self.app.get('/rest/p/test/blog/', extra_environ={'username': '*anonymous'}) + assert_equal(r.json['posts'][0]['title'], 'test')