allura-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From hei...@apache.org
Subject [35/45] allura git commit: [#7878] Used 2to3 to see what issues would come up
Date Fri, 29 May 2015 20:40:57 GMT
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeUserStats/forgeuserstats/main.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/main.py b/ForgeUserStats/forgeuserstats/main.py
index eb25140..34dcdbf 100644
--- a/ForgeUserStats/forgeuserstats/main.py
+++ b/ForgeUserStats/forgeuserstats/main.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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
@@ -24,8 +28,8 @@ from allura.app import Application, SitemapEntry
 from allura.lib import helpers as h
 from allura import model as M
 from allura.eventslistener import EventsListener
-from model.stats import UserStats
-from controllers.userstats import ForgeUserStatsController
+from .model.stats import UserStats
+from .controllers.userstats import ForgeUserStatsController
 
 from forgeuserstats import version
 

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeUserStats/forgeuserstats/model/stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/model/stats.py b/ForgeUserStats/forgeuserstats/model/stats.py
index 2da4618..a825e4f 100644
--- a/ForgeUserStats/forgeuserstats/model/stats.py
+++ b/ForgeUserStats/forgeuserstats/model/stats.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeUserStats/forgeuserstats/tests/test_model.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/test_model.py b/ForgeUserStats/forgeuserstats/tests/test_model.py
index f741159..09e39fe 100644
--- a/ForgeUserStats/forgeuserstats/tests/test_model.py
+++ b/ForgeUserStats/forgeuserstats/tests/test_model.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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
@@ -443,12 +447,12 @@ class TestUserStats(unittest.TestCase):
                    login_datetime) < timedelta(seconds=1)
 
     def test_start_date(self):
-        stats = USM.UserStats(registration_date=datetime(2012, 04, 01))
-        self.assertEqual(stats.start_date, datetime(2012, 04, 01))
+        stats = USM.UserStats(registration_date=datetime(2012, 0o4, 0o1))
+        self.assertEqual(stats.start_date, datetime(2012, 0o4, 0o1))
         with h.push_config(config, **{'userstats.start_date': '2013-04-01'}):
-            self.assertEqual(stats.start_date, datetime(2013, 04, 01))
+            self.assertEqual(stats.start_date, datetime(2013, 0o4, 0o1))
         with h.push_config(config, **{'userstats.start_date': '2011-04-01'}):
-            self.assertEqual(stats.start_date, datetime(2012, 04, 01))
+            self.assertEqual(stats.start_date, datetime(2012, 0o4, 0o1))
 
     @mock.patch('allura.model.stats.difflib.unified_diff')
     def test_count_loc(self, unified_diff):

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeUserStats/forgeuserstats/tests/test_stats.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/tests/test_stats.py b/ForgeUserStats/forgeuserstats/tests/test_stats.py
index a13d5b4..d16e497 100644
--- a/ForgeUserStats/forgeuserstats/tests/test_stats.py
+++ b/ForgeUserStats/forgeuserstats/tests/test_stats.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeUserStats/forgeuserstats/version.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/version.py b/ForgeUserStats/forgeuserstats/version.py
index 1b493f8..8f5910c 100644
--- a/ForgeUserStats/forgeuserstats/version.py
+++ b/ForgeUserStats/forgeuserstats/version.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeUserStats/forgeuserstats/widgets/forms.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/forgeuserstats/widgets/forms.py b/ForgeUserStats/forgeuserstats/widgets/forms.py
index a807e8c..784b69c 100644
--- a/ForgeUserStats/forgeuserstats/widgets/forms.py
+++ b/ForgeUserStats/forgeuserstats/widgets/forms.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeUserStats/setup.py
----------------------------------------------------------------------
diff --git a/ForgeUserStats/setup.py b/ForgeUserStats/setup.py
index 1ad5c3a..4540297 100644
--- a/ForgeUserStats/setup.py
+++ b/ForgeUserStats/setup.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/converters.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/converters.py b/ForgeWiki/forgewiki/converters.py
index 95b88f7..5d556c7 100644
--- a/ForgeWiki/forgewiki/converters.py
+++ b/ForgeWiki/forgewiki/converters.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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
@@ -53,7 +57,7 @@ def _convert_toc(wiki_html):
     soup = BeautifulSoup(wiki_html)
     for toc_div in soup.findAll('div', id='toc'):
         toc_div.replaceWith('[TOC]')
-    return unicode(soup)
+    return str(soup)
 
 
 def mediawiki2markdown(source):

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/model/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/model/__init__.py b/ForgeWiki/forgewiki/model/__init__.py
index 02aa205..807e461 100644
--- a/ForgeWiki/forgewiki/model/__init__.py
+++ b/ForgeWiki/forgewiki/model/__init__.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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
@@ -15,4 +19,4 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from wiki import Page, WikiAttachment, Globals, PageHistory
+from .wiki import Page, WikiAttachment, Globals, PageHistory

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/model/wiki.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/model/wiki.py b/ForgeWiki/forgewiki/model/wiki.py
index 2ca4f8f..48360c0 100644
--- a/ForgeWiki/forgewiki/model/wiki.py
+++ b/ForgeWiki/forgewiki/model/wiki.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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
@@ -236,7 +240,7 @@ class Page(VersionedArtifact, ActivityObject):
             t = {}
             for user in users:
                 t[user.username] = user.id
-            return t.values()
+            return list(t.values())
         user_ids = uniq([r.author for r in self.history().all()])
         return User.query.find({
             '_id': {'$in': user_ids},

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/tests/functional/test_rest.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/tests/functional/test_rest.py b/ForgeWiki/forgewiki/tests/functional/test_rest.py
index 560503e..830fcac 100644
--- a/ForgeWiki/forgewiki/tests/functional/test_rest.py
+++ b/ForgeWiki/forgewiki/tests/functional/test_rest.py
@@ -17,6 +17,10 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 import json
 
 from nose.tools import assert_equal, assert_in
@@ -78,9 +82,9 @@ class TestWikiApi(TestRestApiBase):
             'text': 'Embrace the Dark Side',
             'labels': 'head hunting,dark side'
         }
-        r = self.api_post(u'/rest/p/test/wiki/tést/'.encode('utf-8'), **data)
+        r = self.api_post('/rest/p/test/wiki/tést/'.encode('utf-8'), **data)
         assert_equal(r.status_int, 200)
-        r = self.api_get(u'/rest/p/test/wiki/tést/'.encode('utf-8'))
+        r = self.api_get('/rest/p/test/wiki/tést/'.encode('utf-8'))
         assert_equal(r.json['text'], data['text'])
         assert_equal(r.json['labels'], data['labels'].split(','))
 
@@ -96,7 +100,7 @@ class TestWikiApi(TestRestApiBase):
 
     def test_json_encoding_directly(self):
         # used in @expose('json')
-        assert_equal(tg.jsonify.encode('<'), '"\u003C"')
+        assert_equal(tg.jsonify.encode('<'), '"\\u003C"')
         # make sure these are unchanged
         assert_equal(json.dumps('<'), '"<"')
         assert_equal(simplejson.dumps('<'), '"<"')

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/tests/functional/test_root.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/tests/functional/test_root.py b/ForgeWiki/forgewiki/tests/functional/test_root.py
index f5f49e6..295f84e 100644
--- a/ForgeWiki/forgewiki/tests/functional/test_root.py
+++ b/ForgeWiki/forgewiki/tests/functional/test_root.py
@@ -17,8 +17,12 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 import os
-import StringIO
+import io
 import allura
 
 import PIL
@@ -53,9 +57,9 @@ class TestRootController(TestController):
         pass
 
     def test_root_index(self):
-        page_url = h.urlquote(u'/wiki/tést/')
+        page_url = h.urlquote('/wiki/tést/')
         r = self.app.get(page_url).follow()
-        assert u'tést' in r
+        assert 'tést' in r
         assert 'Create Page' in r
         # No 'Create Page' button if user doesn't have 'create' perm
         r = self.app.get(page_url,
@@ -75,13 +79,13 @@ class TestRootController(TestController):
         assert 'Browse Pages' in response
 
     def test_root_new_page(self):
-        response = self.app.get('/wiki/new_page?title=' + h.urlquote(u'tést'))
-        assert u'tést' in response
+        response = self.app.get('/wiki/new_page?title=' + h.urlquote('tést'))
+        assert 'tést' in response
 
     def test_root_new_search(self):
-        self.app.get(h.urlquote(u'/wiki/tést/'))
-        response = self.app.get('/wiki/search?q=' + h.urlquote(u'tést'))
-        assert u'Search wiki: tést' in response
+        self.app.get(h.urlquote('/wiki/tést/'))
+        response = self.app.get('/wiki/search?q=' + h.urlquote('tést'))
+        assert 'Search wiki: tést' in response
 
     def test_feed(self):
         for ext in ['', '.rss', '.atom']:
@@ -489,11 +493,11 @@ class TestRootController(TestController):
 
         uploaded = PIL.Image.open(file_path)
         r = self.app.get('/wiki/TEST/attachment/' + filename)
-        downloaded = PIL.Image.open(StringIO.StringIO(r.body))
+        downloaded = PIL.Image.open(io.StringIO(r.body))
         assert uploaded.size == downloaded.size
         r = self.app.get('/wiki/TEST/attachment/' + filename + '/thumb')
 
-        thumbnail = PIL.Image.open(StringIO.StringIO(r.body))
+        thumbnail = PIL.Image.open(io.StringIO(r.body))
         assert thumbnail.size == (255, 255)
 
         # Make sure thumbnail is absent

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/tests/test_app.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/tests/test_app.py b/ForgeWiki/forgewiki/tests/test_app.py
index 484a6ee..c584657 100644
--- a/ForgeWiki/forgewiki/tests/test_app.py
+++ b/ForgeWiki/forgewiki/tests/test_app.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/tests/test_converters.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/tests/test_converters.py b/ForgeWiki/forgewiki/tests/test_converters.py
index f30bf1d..9e58398 100644
--- a/ForgeWiki/forgewiki/tests/test_converters.py
+++ b/ForgeWiki/forgewiki/tests/test_converters.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/tests/test_models.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/tests/test_models.py b/ForgeWiki/forgewiki/tests/test_models.py
index 2ab2f00..db544f5 100644
--- a/ForgeWiki/forgewiki/tests/test_models.py
+++ b/ForgeWiki/forgewiki/tests/test_models.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/tests/test_wiki_roles.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/tests/test_wiki_roles.py b/ForgeWiki/forgewiki/tests/test_wiki_roles.py
index 64b5f03..5c5bc11 100644
--- a/ForgeWiki/forgewiki/tests/test_wiki_roles.py
+++ b/ForgeWiki/forgewiki/tests/test_wiki_roles.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/version.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/version.py b/ForgeWiki/forgewiki/version.py
index 1b493f8..8f5910c 100644
--- a/ForgeWiki/forgewiki/version.py
+++ b/ForgeWiki/forgewiki/version.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/widgets/wiki.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/widgets/wiki.py b/ForgeWiki/forgewiki/widgets/wiki.py
index 7dc8b56..a889a60 100644
--- a/ForgeWiki/forgewiki/widgets/wiki.py
+++ b/ForgeWiki/forgewiki/widgets/wiki.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/forgewiki/wiki_main.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/wiki_main.py b/ForgeWiki/forgewiki/wiki_main.py
index a87ce8d..bcc60ae 100644
--- a/ForgeWiki/forgewiki/wiki_main.py
+++ b/ForgeWiki/forgewiki/wiki_main.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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
@@ -19,7 +23,7 @@
 import json
 import logging
 from pprint import pformat
-from urllib import unquote
+from urllib.parse import unquote
 
 # Non-stdlib imports
 from tg import expose, validate, redirect, flash, jsonify
@@ -106,7 +110,7 @@ class ForgeWikiApp(Application):
     default_mount_label = 'Wiki'
     default_mount_point = 'wiki'
     ordinal = 5
-    default_root_page_name = u'Home'
+    default_root_page_name = 'Home'
     icons = {
         24: 'images/wiki_24.png',
         32: 'images/wiki_32.png',
@@ -231,11 +235,8 @@ The wiki uses [Markdown](%s) syntax.
         links += [SitemapEntry('Browse Pages', self.url + 'browse_pages/'),
                   SitemapEntry('Browse Labels', self.url + 'browse_tags/')]
         discussion = c.app.config.discussion
-        pending_mod_count = M.Post.query.find({
-            'discussion_id': discussion._id,
-            'status': 'pending',
-            'deleted': False
-        }).count() if discussion else 0
+        pending_mod_count = M.Post.query.find(
+            {'discussion_id': discussion._id, 'status': 'pending'}).count() if discussion else 0
         if pending_mod_count and h.has_access(discussion, 'moderate')():
             links.append(
                 SitemapEntry(

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ForgeWiki/setup.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/setup.py b/ForgeWiki/setup.py
index 76db941..d2c86a5 100644
--- a/ForgeWiki/setup.py
+++ b/ForgeWiki/setup.py
@@ -1,3 +1,7 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
 #       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

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/LICENSE
----------------------------------------------------------------------
diff --git a/LICENSE b/LICENSE
index 71c5c69..8ec8955 100644
--- a/LICENSE
+++ b/LICENSE
@@ -248,6 +248,3 @@ For details, see AlluraTest/jslint/
 
 Modernizr, which is available under the MIT license.
 For details, see Allura/allura/public/nf/js/modernizr.js
-
-React.js, which is available under the BSD license.
-For details, see Allura/allura/public/nf/js/react.min.js

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/__init__.py
----------------------------------------------------------------------
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..ef58e09
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,27 @@
+# -*- 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.
+
+"""The allura package"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+from paste.registry import StackedObjectProxy
+
+credentials = StackedObjectProxy(name='credentials')

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/app.py
----------------------------------------------------------------------
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..e7383cb
--- /dev/null
+++ b/app.py
@@ -0,0 +1,929 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+#       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.
+
+import os
+import logging
+from urllib import basejoin
+from io import StringIO
+from collections import defaultdict
+from xml.etree import ElementTree as ET
+
+import pkg_resources
+from tg import expose, redirect, flash, validate
+from tg.decorators import without_trailing_slash
+from tg import config as tg_config
+from pylons import request, app_globals as g, tmpl_context as c
+from paste.deploy.converters import asbool, asint
+from bson import ObjectId
+from bson.errors import InvalidId
+from formencode import validators as V
+from webob import exc
+
+from ming.orm import session
+from ming.utils import LazyProperty
+
+from .allura.lib import helpers as h
+from .allura.lib.security import has_access, require_access
+from .allura import model
+from .allura.controllers import BaseController
+from .allura.lib.decorators import require_post, memoize
+from .allura.lib.utils import permanent_redirect, ConfigProxy
+from .allura import model as M
+import collections
+
+log = logging.getLogger(__name__)
+
+config = ConfigProxy(common_suffix='forgemail.domain')
+
+
+class ConfigOption(object):
+
+    """Definition of a configuration option for an :class:`Application`.
+
+    """
+
+    def __init__(self, name, ming_type, default, label=None):
+        """Create a new ConfigOption.
+
+        """
+        self.name, self.ming_type, self._default, self.label = (
+            name, ming_type, default, label or name)
+
+    @property
+    def default(self):
+        """Return the default value for this ConfigOption.
+
+        """
+        if isinstance(self._default, collections.Callable):
+            return self._default()
+        return self._default
+
+
+class SitemapEntry(object):
+
+    """A labeled URL, which may optionally have
+    :class:`children <SitemapEntry>`.
+
+    Used for generating trees of links.
+
+    """
+
+    def __init__(self, label, url=None, children=None, className=None,
+                 ui_icon=None, small=None, tool_name=None, matching_urls=None, extra_html_attrs=None):
+        """Create a new SitemapEntry.
+
+        """
+        self.label = label
+        self.className = className
+        if url is not None:
+            url = url.encode('utf-8')
+        self.url = url
+        self.small = small
+        self.ui_icon = ui_icon
+        self.children = children or []
+        self.tool_name = tool_name
+        self.matching_urls = matching_urls or []
+        self.extra_html_attrs = extra_html_attrs or {}
+
+    def __getitem__(self, x):
+        """Automatically expand the list of sitemap child entries with the
+        given items.  Example::
+
+            SitemapEntry('HelloForge')[
+                SitemapEntry('foo')[
+                    SitemapEntry('Pages')[pages]
+                ]
+            ]
+
+        TODO: deprecate this; use a more clear method of building a tree
+
+        """
+        if isinstance(x, (list, tuple)):
+            self.children.extend(list(x))
+        else:
+            self.children.append(x)
+        return self
+
+    def __repr__(self):
+        l = ['<SitemapEntry ']
+        l.append('    label=%r' % self.label)
+        l.append('    url=%r' % self.url)
+        l.append('    children=%s' %
+                 repr(self.children).replace('\n', '\n    '))
+        l.append('>')
+        return '\n'.join(l)
+
+    def bind_app(self, app):
+        """Recreate this SitemapEntry in the context of
+        :class:`app <Application>`.
+
+        :returns: :class:`SitemapEntry`
+
+        """
+        lbl = self.label
+        url = self.url
+        if isinstance(lbl, collections.Callable):
+            lbl = lbl(app)
+        if url is not None:
+            url = basejoin(app.url, url)
+        return SitemapEntry(lbl, url, [
+            ch.bind_app(app) for ch in self.children],
+            className=self.className,
+            ui_icon=self.ui_icon,
+            small=self.small,
+            tool_name=self.tool_name,
+            matching_urls=self.matching_urls)
+
+    def extend(self, sitemap_entries):
+        """Extend our children with ``sitemap_entries``.
+
+        :param sitemap_entries: list of :class:`SitemapEntry`
+
+        For each entry, if it doesn't already exist in our children, add it.
+        If it does already exist in our children, recursively extend the
+        children or our copy with the children of the new copy.
+
+        """
+        child_index = dict(
+            (ch.label, ch) for ch in self.children)
+        for e in sitemap_entries:
+            lbl = e.label
+            match = child_index.get(e.label)
+            if match and match.url == e.url:
+                match.extend(e.children)
+            else:
+                self.children.append(e)
+                child_index[lbl] = e
+
+    def matches_url(self, request):
+        """Return True if this SitemapEntry 'matches' the url of ``request``.
+
+        """
+        return self.url in request.upath_info or any([
+            url in request.upath_info for url in self.matching_urls])
+
+
+class Application(object):
+
+    """
+    The base Allura pluggable application
+
+    After extending this, expose the app by adding an entry point in your
+    setup.py::
+
+        [allura]
+        myapp = foo.bar.baz:MyAppClass
+
+    :cvar str status: One of 'production', 'beta', 'alpha', or 'user'. By
+        default, only 'production' apps are installable in projects. Default
+        is 'production'.
+    :cvar bool searchable: If True, show search box in the left menu of this
+        Application. Default is True.
+    :cvar bool exportable: Default is False, Application can't be exported to json.
+    :cvar list permissions: Named permissions used by instances of this
+        Application. Default is [].
+    :cvar dict permissions_desc: Descriptions of the named permissions.
+    :cvar int max_instances: Specifies the number of tools of this type
+        that can be added to the project. Zero indicates the system tool or one that
+        can not be added to the project by the user. Default value is float("inf").
+    :cvar bool hidden: Default is False, Application is not hidden from the
+        list of a project's installed tools.
+    :cvar str tool_description: Text description of this Application.
+    :cvar bool relaxed_mount_points: Set to True to relax the default mount point
+        naming restrictions for this Application. Default is False. See
+        :attr:`default mount point naming rules <allura.lib.helpers.re_tool_mount_point>` and
+        :attr:`relaxed mount point naming rules <allura.lib.helpers.re_relaxed_tool_mount_point>`.
+    :cvar Controller root: Serves content at
+        /<neighborhood>/<project>/<app>/. Default is None - subclasses should
+        override.
+    :cvar Controller api_root: Serves API access at
+        /rest/<neighborhood>/<project>/<app>/. Default is None - subclasses
+        should override to expose API access to the Application.
+    :cvar Controller admin_api_root: Serves Admin API access at
+        /rest/<neighborhood>/<project>/admin/<app>/. Default is None -
+        subclasses should override to expose Admin API access to the
+        Application.
+    :ivar Controller admin: Serves admin functions at
+        /<neighborhood>/<project>/<admin>/<app>/. Default is a
+        :class:`DefaultAdminController` instance.
+    :cvar dict icons: Mapping of icon sizes to application-specific icon paths.
+    """
+
+    __version__ = None
+    config_options = [
+        ConfigOption('mount_point', str, 'app'),
+        ConfigOption('mount_label', str, 'app'),
+        ConfigOption('ordinal', int, '0')]
+    status_map = ['production', 'beta', 'alpha', 'user']
+    status = 'production'
+    script_name = None
+    root = None  # root controller
+    api_root = None
+    admin_api_root = None
+    permissions = []
+    permissions_desc = {
+        'unmoderated_post': 'Post comments without moderation.',
+        'post': 'Post comments, subject to moderation.',
+        'moderate': 'Moderate comments.',
+        'configure': 'Set label and options. Requires admin permission.',
+        'admin': 'Set permissions.',
+    }
+    max_instances = float("inf")
+    searchable = False
+    exportable = False
+    DiscussionClass = model.Discussion
+    PostClass = model.Post
+    AttachmentClass = model.DiscussionAttachment
+    tool_label = 'Tool'
+    tool_description = "This is a tool for Allura forge."
+    default_mount_label = 'Tool Name'
+    default_mount_point = 'tool'
+    relaxed_mount_points = False
+    ordinal = 0
+    hidden = False
+    icons = {
+        24: 'images/admin_24.png',
+        32: 'images/admin_32.png',
+        48: 'images/admin_48.png'
+    }
+
+    def __init__(self, project, app_config_object):
+        """Create an Application instance.
+
+        :param project: Project to which this Application belongs
+        :type project: :class:`allura.model.project.Project`
+        :param app_config_object: Config describing this Application
+        :type app_config_object: :class:`allura.model.project.AppConfig`
+
+        """
+        self.project = project
+        self.config = app_config_object
+        self.admin = DefaultAdminController(self)
+
+    @LazyProperty
+    def sitemap(self):
+        """Return a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
+        describing the page hierarchy provided by this Application.
+
+        If the list is empty, the Application will not be displayed in the
+        main project nav bar.
+
+        """
+        return [SitemapEntry(self.config.options.mount_label, '.')]
+
+    @LazyProperty
+    def url(self):
+        """Return the URL for this Application.
+
+        """
+        return self.config.url(project=self.project)
+
+    @LazyProperty
+    def admin_url(self):
+        return '{}{}/{}/'.format(
+            self.project.url(), 'admin',
+            self.config.options.mount_point)
+
+    @property
+    def email_address(self):
+        """Return email address for this Application.
+
+        Email address constructed from Application's url, and looks like this:
+
+            wiki@test.p.in.domain.net
+
+        where 'wiki@test.p' comes from app url (in this case /p/test/wiki/)
+        and '.in.domain.net' comes from 'forgemail.domain' config entry.
+
+        Assumes self.url returns a url path without domain, starting with '/'
+        """
+        if self.config.options.get('AllowEmailPosting', True):
+            parts = list(reversed(self.url[1:-1].split('/')))
+            return '%s@%s%s' % (parts[0], '.'.join(parts[1:]), config.common_suffix)
+        else:
+            return tg_config.get('forgemail.return_path')
+
+    @property
+    def acl(self):
+        """Return the :class:`Access Control List <allura.model.types.ACL>`
+        for this Application.
+
+        """
+        return self.config.acl
+
+    @classmethod
+    def describe_permission(cls, permission):
+        """Return help text describing what features ``permission`` controls.
+
+        Subclasses should define :attr:`permissions_desc`,
+        a ``{permission: description}`` mapping.
+
+        Returns empty string if there is no description for ``permission``.
+
+        """
+        d = {}
+        for t in reversed(cls.__mro__):
+            d = dict(d, **getattr(t, 'permissions_desc', {}))
+        return d.get(permission, '')
+
+    def parent_security_context(self):
+        """Return the parent of this object.
+
+        Used for calculating permissions based on trees of ACLs.
+
+        """
+        return self.config.parent_security_context()
+
+    @property
+    def installable(self):
+        """Checks whether to add a tool to the project.
+
+        Return True if app can be installed.
+
+        :rtype: bool
+
+        """
+        return self._installable(self.config.tool_name,
+                                 self.project.neighborhood,
+                                 self.project.app_configs,
+                                 )
+
+    @classmethod
+    def _installable(cls, tool_name, nbhd, project_tools):
+        if tool_name.lower() in nbhd.get_prohibited_tools():
+            return False
+        tools_list = [tool.tool_name.lower() for tool in project_tools]
+        return tools_list.count(tool_name.lower()) < cls.max_instances
+
+    @classmethod
+    def validate_mount_point(cls, mount_point):
+        """Check if ``mount_point`` is valid for this Application.
+
+        In general, subclasses should not override this, but rather toggle
+        the strictness of allowed mount point names by toggling
+        :attr:`Application.relaxed_mount_points`.
+
+        :param mount_point: the mount point to validate
+        :type mount_point: str
+        :rtype: A :class:`regex Match object <_sre.SRE_Match>` if the mount
+                point is valid, else None
+
+        """
+        re = (h.re_relaxed_tool_mount_point if cls.relaxed_mount_points
+              else h.re_tool_mount_point)
+        return re.match(mount_point)
+
+    @classmethod
+    def status_int(self):
+        """Return the :attr:`status` of this Application as an int.
+
+        Used for sorting available Apps by status in the Admin interface.
+
+        """
+        return self.status_map.index(self.status)
+
+    @classmethod
+    def icon_url(cls, size):
+        """Return URL for icon of the given ``size``.
+
+        Subclasses can define their own icons by overriding
+        :attr:`icons`.
+
+        """
+        resource, url = cls.icons.get(size), ''
+        if resource:
+            resource_path = os.path.join('nf', resource)
+            url = (g.forge_static(resource) if cls.has_resource(resource_path)
+                   else g.theme_href(resource))
+        return url
+
+    @classmethod
+    @memoize
+    def has_resource(cls, resource_path):
+        """Determine whether this Application has the resource pointed to by
+        ``resource_path``.
+
+        If the resource is not found for the immediate class, its parents
+        will be searched. The return value is the class that "owns" the
+        resource, or None if the resource is not found.
+
+        """
+        for klass in [o for o in cls.__mro__ if issubclass(o, Application)]:
+            if pkg_resources.resource_exists(klass.__module__, resource_path):
+                return klass
+
+    def has_access(self, user, topic):
+        """Return True if ``user`` can send email to ``topic``.
+        Default is False.
+
+        :param user: :class:`allura.model.User` instance
+        :param topic: str
+        :rtype: bool
+
+        """
+        return False
+
+    def is_visible_to(self, user):
+        """Return True if ``user`` can view this app.
+
+        :type user: :class:`allura.model.User` instance
+        :rtype: bool
+
+        """
+        return has_access(self, 'read')(user=user)
+
+    def subscribe_admins(self):
+        """Subscribe all project Admins (for this Application's project) to the
+        :class:`allura.model.notification.Mailbox` for this Application.
+
+        """
+        for uid in g.credentials.userids_with_named_role(self.project._id, 'Admin'):
+            model.Mailbox.subscribe(
+                type='direct',
+                user_id=uid,
+                project_id=self.project._id,
+                app_config_id=self.config._id)
+
+    def subscribe(self, user):
+        """Subscribe :class:`user <allura.model.auth.User>` to the
+        :class:`allura.model.notification.Mailbox` for this Application.
+
+        """
+        if user and user != model.User.anonymous():
+            model.Mailbox.subscribe(
+                type='direct',
+                user_id=user._id,
+                project_id=self.project._id,
+                app_config_id=self.config._id)
+
+    @classmethod
+    def default_options(cls):
+        """Return a ``(name, default value)`` mapping of this Application's
+        :class:`config_options <ConfigOption>`.
+
+        :rtype: dict
+
+        """
+        return dict(
+            (co.name, co.default)
+            for co in cls.config_options)
+
+    def install(self, project):
+        'Whatever logic is required to initially set up a tool'
+        # Create the discussion object
+        discussion = self.DiscussionClass(
+            shortname=self.config.options.mount_point,
+            name='%s Discussion' % self.config.options.mount_point,
+            description='Forum for %s comments' % self.config.options.mount_point)
+        session(discussion).flush()
+        self.config.discussion_id = discussion._id
+        self.subscribe_admins()
+
+    def uninstall(self, project=None, project_id=None):
+        'Whatever logic is required to tear down a tool'
+        if project_id is None:
+            project_id = project._id
+        # De-index all the artifacts belonging to this tool in one fell swoop
+        g.solr.delete(q='project_id_s:"%s" AND mount_point_s:"%s"' % (
+            project_id, self.config.options['mount_point']))
+        for d in model.Discussion.query.find({
+                'project_id': project_id,
+                'app_config_id': self.config._id}):
+            d.delete()
+        self.config.delete()
+        session(self.config).flush()
+
+    @property
+    def uninstallable(self):
+        """Return True if this app can be uninstalled. Controls whether the
+        'Delete' option appears on the admin menu for this app.
+
+        By default, an app can be uninstalled iff it can be installed, although
+        some apps may want/need to override this (e.g. an app which can
+        not be installed directly by a user, but may be uninstalled).
+
+        """
+        return self.installable
+
+    def main_menu(self):
+        """Return a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
+        to display in the main project nav for this Application.
+
+        Default implementation returns :attr:`sitemap`.
+
+        """
+        return self.sitemap
+
+    def sidebar_menu(self):
+        """Return a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
+        to render in the left sidebar for this Application.
+
+        """
+        return []
+
+    def sidebar_menu_js(self):
+        """Return Javascript needed by the sidebar menu of this Application.
+
+        :return: a string of Javascript code
+
+        """
+        return ""
+
+    @LazyProperty
+    def _webhooks(self):
+        """A list of webhooks that can be triggered by this app.
+
+        :return: a list of :class:`WebhookSender <allura.webhooks.WebhookSender>`
+        """
+        tool_name = self.config.tool_name.lower()
+        webhooks = [w for w in list(g.entry_points['webhooks'].values())
+                    if tool_name in w.triggered_by]
+        return webhooks
+
+    def admin_menu(self, force_options=False):
+        """Return the admin menu for this Application.
+
+        Default implementation will return a menu with up to 4 links:
+
+            - 'Permissions', if the current user has admin access to the
+                project in which this Application is installed
+            - 'Options', if this Application has custom options, or
+                ``force_options`` is True
+            - 'Label', for editing this Application's label
+            - 'Webhooks', if this Application can trigger any webhooks
+
+        Subclasses should override this method to provide additional admin
+        menu items.
+
+        :param force_options: always include an 'Options' link in the menu,
+            even if this Application has no custom options
+        :return: a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
+
+        """
+        admin_url = c.project.url() + 'admin/' + \
+            self.config.options.mount_point + '/'
+        links = []
+        if self.permissions and has_access(c.project, 'admin')():
+            links.append(
+                SitemapEntry('Permissions', admin_url + 'permissions'))
+        if force_options or len(self.config_options) > 3:
+            links.append(
+                SitemapEntry('Options', admin_url + 'options', className='admin_modal'))
+        links.append(
+            SitemapEntry('Label', admin_url + 'edit_label', className='admin_modal'))
+        if len(self._webhooks) > 0:
+            links.append(SitemapEntry('Webhooks', admin_url + 'webhooks'))
+        return links
+
+    def handle_message(self, topic, message):
+        """Handle incoming email msgs addressed to this tool.
+        Default is a no-op.
+
+        :param topic: portion of destination email address preceeding the '@'
+        :type topic: str
+        :param message: parsed email message
+        :type message: dict - result of
+            :func:`allura.lib.mail_util.parse_message`
+        :rtype: None
+
+        """
+        pass
+
+    def handle_artifact_message(self, artifact, message):
+        """Handle message addressed to this Application.
+
+        :param artifact: Specific artifact to which the message is addressed
+        :type artifact: :class:`allura.model.artifact.Artifact`
+        :param message: the message
+        :type message: :class:`allura.model.artifact.Message`
+
+        Default implementation posts the message to the appropriate discussion
+        thread for the artifact.
+
+        """
+        # Find ancestor comment and thread
+        thd, parent_id = artifact.get_discussion_thread(message)
+        # Handle attachments
+        message_id = message['message_id']
+        if message.get('filename'):
+            # Special case - the actual post may not have been created yet
+            log.info('Saving attachment %s', message['filename'])
+            fp = StringIO(message['payload'])
+            self.AttachmentClass.save_attachment(
+                message['filename'], fp,
+                content_type=message.get(
+                    'content_type', 'application/octet-stream'),
+                discussion_id=thd.discussion_id,
+                thread_id=thd._id,
+                post_id=message_id,
+                artifact_id=message_id)
+            return
+        # Handle duplicates
+        post = self.PostClass.query.get(_id=message_id)
+        if post:
+            log.info(
+                'Existing message_id %s found - saving this as text attachment' %
+                message_id)
+            fp = StringIO(message['payload'])
+            post.attach(
+                'alternate', fp,
+                content_type=message.get(
+                    'content_type', 'application/octet-stream'),
+                discussion_id=thd.discussion_id,
+                thread_id=thd._id,
+                post_id=message_id)
+        else:
+            text = message['payload'] or '--no text body--'
+            post = thd.post(
+                message_id=message_id,
+                parent_id=parent_id,
+                text=text,
+                subject=message['headers'].get('Subject', 'no subject'))
+
+    def bulk_export(self, f):
+        """Export all artifacts in the tool into json file.
+
+        :param f: File Object to write to
+
+        Set exportable to True for applications implementing this.
+        """
+        raise NotImplementedError('bulk_export')
+
+    def doap(self, parent):
+        """App's representation for DOAP API.
+
+        :param parent: Element to contain the results
+        :type parent: xml.etree.ElementTree.Element or xml.etree.ElementTree.SubElement
+        """
+        feature = ET.SubElement(parent, 'sf:feature')
+        feature = ET.SubElement(feature, 'sf:Feature')
+        ET.SubElement(feature, 'name').text = self.config.options.mount_label
+        ET.SubElement(feature, 'foaf:page', {'rdf:resource': h.absurl(self.url)})
+
+    def __json__(self):
+        """App's representation for JSON API.
+
+        Returns dict that will be included in project's API under tools key.
+        """
+        return {'name': self.config.tool_name,
+                'mount_point': self.config.options.mount_point,
+                'label': self.config.options.mount_label}
+
+
+
+
+class DefaultAdminController(BaseController):
+
+    """Provides basic admin functionality for an :class:`Application`.
+
+    To add more admin functionality for your Application, extend this
+    class and then assign an instance of it to the ``admin`` attr of
+    your Application::
+
+        class MyApp(Application):
+            def __init__(self, *args):
+                super(MyApp, self).__init__(*args)
+                self.admin = MyAdminController(self)
+
+    """
+
+    def __init__(self, app):
+        """Instantiate this controller for an :class:`app <Application>`.
+
+        """
+        super(DefaultAdminController, self).__init__()
+        self.app = app
+        self.webhooks = WebhooksLookup(app)
+
+    @expose()
+    def index(self, **kw):
+        """Home page for this controller.
+
+        Redirects to the 'permissions' page by default.
+
+        """
+        permanent_redirect('permissions')
+
+    @expose('json:')
+    @require_post()
+    def block_user(self, username, perm, reason=None):
+        if not username or not perm:
+            return dict(error='Enter username')
+        user = model.User.by_username(username)
+        if not user:
+            return dict(error='User "%s" not found' % username)
+        ace = model.ACE.deny(
+            model.ProjectRole.by_user(user, upsert=True)._id, perm, reason)
+        if not model.ACL.contains(ace, self.app.acl):
+            self.app.acl.append(ace)
+            return dict(user_id=str(user._id), username=user.username, reason=reason)
+        return dict(error='User "%s" already blocked' % user.username)
+
+    @validate(dict(user_id=V.Set(),
+                   perm=V.UnicodeString()))
+    @expose('json:')
+    @require_post()
+    def unblock_user(self, user_id=None, perm=None):
+        try:
+            user_id = list(map(ObjectId, user_id))
+        except InvalidId:
+            user_id = []
+        users = model.User.query.find({'_id': {'$in': user_id}}).all()
+        if not users:
+            return dict(error='Select user to unblock')
+        unblocked = []
+        for user in users:
+            ace = model.ACE.deny(model.ProjectRole.by_user(user)._id, perm)
+            ace = model.ACL.contains(ace, self.app.acl)
+            if ace:
+                self.app.acl.remove(ace)
+                unblocked.append(str(user._id))
+        return dict(unblocked=unblocked)
+
+    @expose('jinja:allura:templates/app_admin_permissions.html')
+    @without_trailing_slash
+    def permissions(self):
+        """Render the permissions management web page.
+
+        """
+        from .ext.admin.widgets import PermissionCard, BlockUser, BlockList
+        c.card = PermissionCard()
+        c.block_user = BlockUser()
+        c.block_list = BlockList()
+        permissions = dict((p, []) for p in self.app.permissions)
+        block_list = defaultdict(list)
+        for ace in self.app.config.acl:
+            if ace.access == model.ACE.ALLOW:
+                try:
+                    permissions[ace.permission].append(ace.role_id)
+                except KeyError:
+                    # old, unknown permission
+                    pass
+            elif ace.access == model.ACE.DENY:
+                role = model.ProjectRole.query.get(_id=ace.role_id)
+                if role.name is None and role.user:
+                    block_list[ace.permission].append((role.user, ace.reason))
+        return dict(
+            app=self.app,
+            allow_config=has_access(c.project, 'admin')(),
+            permissions=permissions,
+            block_list=block_list)
+
+    @expose('jinja:allura:templates/app_admin_edit_label.html')
+    def edit_label(self):
+        """Renders form to update the Application's ``mount_label``.
+
+        """
+        return dict(
+            app=self.app,
+            allow_config=has_access(self.app, 'configure')())
+
+    @expose()
+    @require_post()
+    def update_label(self, mount_label):
+        """Handles POST to update the Application's ``mount_label``.
+
+        """
+        require_access(self.app, 'configure')
+        self.app.config.options['mount_label'] = mount_label
+        redirect(request.referer)
+
+    @expose('jinja:allura:templates/app_admin_options.html')
+    def options(self):
+        """Renders form to update the Application's ``config.options``.
+
+        """
+        return dict(
+            app=self.app,
+            allow_config=has_access(self.app, 'configure')())
+
+    @expose()
+    @require_post()
+    def configure(self, **kw):
+        """Handle POST to delete the Application or update its
+        ``config.options``.
+
+        """
+        with h.push_config(c, app=self.app):
+            require_access(self.app, 'configure')
+            is_admin = self.app.config.tool_name == 'admin'
+            if kw.pop('delete', False):
+                if is_admin:
+                    flash('Cannot delete the admin tool, sorry....')
+                    redirect('.')
+                c.project.uninstall_app(self.app.config.options.mount_point)
+                redirect('..')
+            for opt in self.app.config_options:
+                if opt in Application.config_options:
+                    # skip base options (mount_point, mount_label, ordinal)
+                    continue
+                val = kw.get(opt.name, '')
+                if opt.ming_type == bool:
+                    val = asbool(val or False)
+                elif opt.ming_type == int:
+                    val = asint(val or 0)
+                self.app.config.options[opt.name] = val
+            if is_admin:
+                # possibly moving admin mount point
+                redirect('/'
+                         + c.project._id
+                         + self.app.config.options.mount_point
+                         + '/'
+                         + self.app.config.options.mount_point
+                         + '/')
+            else:
+                redirect(request.referer)
+
+    @without_trailing_slash
+    @expose()
+    @h.vardec
+    @require_post()
+    def update(self, card=None, **kw):
+        """Handle POST to update permissions for the Application.
+
+        """
+        old_acl = self.app.config.acl
+        self.app.config.acl = []
+        for args in card:
+            perm = args['id']
+            new_group_ids = args.get('new', [])
+            del_group_ids = []
+            group_ids = args.get('value', [])
+            if isinstance(new_group_ids, str):
+                new_group_ids = [new_group_ids]
+            if isinstance(group_ids, str):
+                group_ids = [group_ids]
+
+            for acl in old_acl:
+                if (acl['permission'] == perm) and (str(acl['role_id']) not in group_ids) and acl['access'] != model.ACE.DENY:
+                    del_group_ids.append(str(acl['role_id']))
+
+            get_role = lambda _id: model.ProjectRole.query.get(_id=ObjectId(_id))
+            groups = list(map(get_role, group_ids))
+            new_groups = list(map(get_role, new_group_ids))
+            del_groups = list(map(get_role, del_group_ids))
+
+            if new_groups or del_groups:
+                model.AuditLog.log('updated "%s" permission: "%s" => "%s" for %s' % (
+                    perm,
+                    ', '.join([role.name for role in [_f for _f in groups + del_groups if _f]]),
+                    ', '.join([role.name for role in [_f for _f in groups + new_groups if _f]]),
+                    self.app.config.options['mount_point']))
+
+            role_ids = list(map(ObjectId, group_ids + new_group_ids))
+            self.app.config.acl += [
+                model.ACE.allow(r, perm) for r in role_ids]
+
+            # Add all ACEs for user roles back
+            for ace in old_acl:
+                if (ace.permission == perm) and (ace.access == model.ACE.DENY):
+                    self.app.config.acl.append(ace)
+        redirect(request.referer)
+
+
+class WebhooksLookup(BaseController):
+
+    def __init__(self, app):
+        super(WebhooksLookup, self).__init__()
+        self.app = app
+
+    @without_trailing_slash
+    @expose('jinja:allura:templates/app_admin_webhooks_list.html')
+    def index(self):
+        webhooks = self.app._webhooks
+        if len(webhooks) == 0:
+            raise exc.HTTPNotFound()
+        configured_hooks = {}
+        for hook in webhooks:
+            configured_hooks[hook.type] = M.Webhook.query.find({
+                'type': hook.type,
+                'app_config_id': self.app.config._id}
+            ).all()
+        return {'webhooks': webhooks,
+                'configured_hooks': configured_hooks,
+                'admin_url': self.app.admin_url + 'webhooks'}
+
+    @expose()
+    def _lookup(self, name, *remainder):
+        for hook in self.app._webhooks:
+            if hook.type == name and hook.controller:
+                return hook.controller(hook, self.app), remainder
+        raise exc.HTTPNotFound(name)

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/command/__init__.py
----------------------------------------------------------------------
diff --git a/command/__init__.py b/command/__init__.py
new file mode 100644
index 0000000..051d97f
--- /dev/null
+++ b/command/__init__.py
@@ -0,0 +1,28 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+#       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 .base import Command
+from .show_models import ShowModelsCommand, ReindexCommand, EnsureIndexCommand
+from .script import ScriptCommand, SetToolAccessCommand
+from .smtp_server import SMTPServerCommand
+from .create_neighborhood import CreateNeighborhoodCommand, UpdateNeighborhoodCommand
+from .create_trove_categories import CreateTroveCategoriesCommand
+from .set_neighborhood_features import SetNeighborhoodFeaturesCommand

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/command/base.py
----------------------------------------------------------------------
diff --git a/command/base.py b/command/base.py
new file mode 100644
index 0000000..48fa41d
--- /dev/null
+++ b/command/base.py
@@ -0,0 +1,131 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+#       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.
+
+import os
+import logging
+import shlex
+
+import pylons
+from paste.script import command
+from paste.deploy import appconfig
+from paste.deploy.converters import asbool
+from paste.registry import Registry
+
+import activitystream
+import ming
+from allura.config.environment import load_environment
+from allura.lib.decorators import task
+from allura.lib.helpers import iter_entry_points
+
+log = None
+
+
+@task
+def run_command(command, args):
+    """Run paster command asynchronously"""
+    mod, cls = command.rsplit('.', 1)
+    mod = __import__(mod, fromlist=[str(cls)])
+    command = getattr(mod, cls)
+    command = command(command.__name__)
+    arg_list = shlex.split(args or '')
+    try:
+        command.parser.parse_args(arg_list)
+    except SystemExit:
+        raise Exception("Error parsing args: '%s'" % args)
+    return command.run(arg_list)
+
+
+class EmptyClass(object):
+    pass
+
+
+class Command(command.Command):
+    min_args = 1
+    max_args = 1
+    usage = '[<ini file>]'
+    group_name = 'Allura'
+
+    class __metaclass__(type):
+
+        @property
+        def __doc__(cls):
+            return cls.parser.format_help()
+
+    @classmethod
+    def post(cls, *args, **kw):
+        cmd = '%s.%s' % (cls.__module__, cls.__name__)
+        return run_command.post(cmd, *args, **kw)
+
+    @ming.utils.LazyProperty
+    def registry(self):
+        return Registry()
+
+    @ming.utils.LazyProperty
+    def globals(self):
+        import allura.lib.app_globals
+        return allura.lib.app_globals.Globals()
+
+    @ming.utils.LazyProperty
+    def config(self):
+        import tg
+        return tg.config
+
+    def basic_setup(self):
+        global log, M
+        if self.args[0]:
+            # Probably being called from the command line - load the config
+            # file
+            self.config = conf = appconfig('config:%s' %
+                                           self.args[0], relative_to=os.getcwd())
+            # ... logging does not understand section#subsection syntax
+            logging_config = self.args[0].split('#')[0]
+            logging.config.fileConfig(
+                logging_config, disable_existing_loggers=False)
+            log = logging.getLogger('allura.command')
+            log.info('Initialize command with config %r', self.args[0])
+            load_environment(conf.global_conf, conf.local_conf)
+            self.setup_globals()
+            from allura import model
+            M = model
+            ming.configure(**conf)
+            if asbool(conf.get('activitystream.recording.enabled', False)):
+                activitystream.configure(**conf)
+            pylons.tmpl_context.user = M.User.anonymous()
+        else:
+            # Probably being called from another script (websetup, perhaps?)
+            log = logging.getLogger('allura.command')
+            conf = pylons.config
+        self.tools = list(pylons.app_globals.entry_points['tool'].values())
+        for ep in iter_entry_points('allura.command_init'):
+            log.info('Running command_init for %s', ep.name)
+            ep.load()(conf)
+        log.info('Loaded tools')
+
+    def setup_globals(self):
+        import allura.lib.app_globals
+        self.registry.prepare()
+        self.registry.register(pylons.tmpl_context, EmptyClass())
+        self.registry.register(pylons.app_globals, self.globals)
+        self.registry.register(
+            allura.credentials, allura.lib.security.Credentials())
+
+    def teardown_globals(self):
+        self.registry.cleanup()

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/command/create_neighborhood.py
----------------------------------------------------------------------
diff --git a/command/create_neighborhood.py b/command/create_neighborhood.py
new file mode 100644
index 0000000..4456308
--- /dev/null
+++ b/command/create_neighborhood.py
@@ -0,0 +1,107 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+#       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 . import base
+
+from ming.orm import session
+from bson import ObjectId
+
+from allura import model as M
+from allura.lib import plugin, exceptions
+
+
+class CreateNeighborhoodCommand(base.Command):
+    min_args = 3
+    max_args = None
+    usage = '<ini file> <neighborhood_shortname> <admin1> [<admin2>...]'
+    summary = 'Create a new neighborhood with the listed admins'
+    parser = base.Command.standard_parser(verbose=True)
+
+    def command(self):
+        self.basic_setup()
+        admins = [M.User.by_username(un) for un in self.args[2:]]
+        shortname = self.args[1]
+        n = M.Neighborhood(
+            name=shortname,
+            url_prefix='/' + shortname + '/',
+            features=dict(private_projects=False,
+                          max_projects=500,
+                          css='none',
+                          google_analytics=False))
+        project_reg = plugin.ProjectRegistrationProvider.get()
+        project_reg.register_neighborhood_project(n, admins)
+
+
+class UpdateNeighborhoodCommand(base.Command):
+    min_args = 3
+    max_args = None
+    usage = '<ini file> <neighborhood> <home_tool_active>'
+    summary = 'Activate Home application for neighborhood\r\n' \
+        '\t<neighborhood> - the neighborhood name or _id\r\n' \
+        '\t<value> - boolean value to install/uninstall Home tool\r\n' \
+        '\t    must be True or False\r\n\r\n' \
+        '\tExample:\r\n' \
+        '\tpaster update-neighborhood-home-tool development.ini Projects True'
+    parser = base.Command.standard_parser(verbose=True)
+
+    def command(self):
+        self.basic_setup()
+        shortname = self.args[1]
+        nb = M.Neighborhood.query.get(name=shortname)
+        if not nb:
+            nb = M.Neighborhood.query.get(_id=ObjectId(shortname))
+        if nb is None:
+            raise exceptions.NoSuchNeighborhoodError("The neighborhood %s "
+                                                     "could not be found in the database" % shortname)
+        tool_value = self.args[2].lower()
+        if tool_value[:1] == "t":
+            home_tool_active = True
+        else:
+            home_tool_active = False
+
+        if home_tool_active == nb.has_home_tool:
+            return
+
+        p = nb.neighborhood_project
+        if home_tool_active:
+            zero_position_exists = False
+            for ac in p.app_configs:
+                if ac.options['ordinal'] == 0:
+                    zero_position_exists = True
+                    break
+
+            if zero_position_exists:
+                for ac in p.app_configs:
+                    ac.options['ordinal'] = ac.options['ordinal'] + 1
+            p.install_app('home', 'home', 'Home', ordinal=0)
+        else:
+            app_config = p.app_config('home')
+            zero_position_exists = False
+            if app_config.options['ordinal'] == 0:
+                zero_position_exists = True
+
+            p.uninstall_app('home')
+            if zero_position_exists:
+                for ac in p.app_configs:
+                    ac.options['ordinal'] = ac.options['ordinal'] - 1
+
+        session(M.AppConfig).flush()
+        session(M.Neighborhood).flush()


Mime
View raw message