You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by jo...@apache.org on 2013/07/31 21:14:21 UTC

[01/23] git commit: [#6139] ticket:399 Skip pages that can't be parsed (non-wiki)

Updated Branches:
  refs/heads/cj/6461 1f16cbab0 -> c7fd78768 (forced update)


[#6139] ticket:399 Skip pages that can't be parsed (non-wiki)


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/be9d8225
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/be9d8225
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/be9d8225

Branch: refs/heads/cj/6461
Commit: be9d8225fc07c1021f631b320df78270f8392787
Parents: 24d39a7
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Jul 26 08:22:51 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Tue Jul 30 19:29:23 2013 +0000

----------------------------------------------------------------------
 ForgeWiki/forgewiki/scripts/wiki_from_trac/extractors.py | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/be9d8225/ForgeWiki/forgewiki/scripts/wiki_from_trac/extractors.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/scripts/wiki_from_trac/extractors.py b/ForgeWiki/forgewiki/scripts/wiki_from_trac/extractors.py
index ef931b3..0038dd9 100644
--- a/ForgeWiki/forgewiki/scripts/wiki_from_trac/extractors.py
+++ b/ForgeWiki/forgewiki/scripts/wiki_from_trac/extractors.py
@@ -18,6 +18,7 @@
 import re
 import sys
 import json
+import traceback
 from urllib import quote, unquote
 from urlparse import urljoin, urlsplit
 
@@ -101,7 +102,14 @@ class WikiExporter(object):
         self.options = options
 
     def export(self, out):
-        pages = [self.get_page(title) for title in self.page_list()]
+        pages = []
+        for title in self.page_list():
+            try:
+                pages.append(self.get_page(title))
+            except:
+                self.log('Cannot fetch page %s. Skipping' % title)
+                self.log(traceback.format_exc())
+                continue
         out.write(json.dumps(pages, indent=2, sort_keys=True))
         out.write('\n')
 


[17/23] git commit: [#6446] ticket:400 do not show not ok posts for other apis

Posted by jo...@apache.org.
[#6446] ticket:400 do not show not ok posts for other apis


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/0877d5c3
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/0877d5c3
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/0877d5c3

Branch: refs/heads/cj/6461
Commit: 0877d5c3e1dd3140229f36e7100b1d346b08ffea
Parents: a7e25cc
Author: Anton Kasyanov <mi...@gmail.com>
Authored: Wed Jul 24 11:47:10 2013 +0300
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 14:55:49 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/discuss.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/0877d5c3/Allura/allura/model/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/discuss.py b/Allura/allura/model/discuss.py
index 97d19d0..ee62778 100644
--- a/Allura/allura/model/discuss.py
+++ b/Allura/allura/model/discuss.py
@@ -169,7 +169,7 @@ class Thread(Artifact, ActivityObject):
                         subject=p.subject,
                         attachments=[dict(bytes=attach.length,
                                           url=h.absurl(attach.url())) for attach in p.attachments])
-                   for p in self.posts])
+                   for p in self.posts if p.status == 'ok'])
 
     @property
     def activity_name(self):


[14/23] git commit: [#6446] ticket:400 fixed topics display in forum api

Posted by jo...@apache.org.
[#6446] ticket:400 fixed topics display in forum 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/a7e25cc0
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/a7e25cc0
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/a7e25cc0

Branch: refs/heads/cj/6461
Commit: a7e25cc06604cecac7d3e3e268aae66364494bdc
Parents: 24cd410
Author: Anton Kasyanov <mi...@gmail.com>
Authored: Wed Jul 24 11:24:58 2013 +0300
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 14:55:49 2013 +0000

----------------------------------------------------------------------
 ForgeDiscussion/forgediscussion/model/forum.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/a7e25cc0/ForgeDiscussion/forgediscussion/model/forum.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/model/forum.py b/ForgeDiscussion/forgediscussion/model/forum.py
index b5206c6..42d1dc0 100644
--- a/ForgeDiscussion/forgediscussion/model/forum.py
+++ b/ForgeDiscussion/forgediscussion/model/forum.py
@@ -161,8 +161,8 @@ class ForumThread(M.Thread):
 
     @property
     def status(self):
-        if self.first_post:
-            return self.first_post.status
+        if len(self.posts) == 1:
+            return self.posts[0].status
         else:
             return 'ok'
 


[08/23] git commit: [#6441] ScriptTask doc parser chokes on %d

Posted by jo...@apache.org.
[#6441] ScriptTask doc parser chokes on %d

Signed-off-by: Tim Van Steenburgh <tv...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/53e35eb9
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/53e35eb9
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/53e35eb9

Branch: refs/heads/cj/6461
Commit: 53e35eb9fcc2ac8b9bdc6541404c0392414966cb
Parents: 7975c7c
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Wed Jul 31 13:17:36 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 13:17:36 2013 +0000

----------------------------------------------------------------------
 ForgeTracker/forgetracker/scripts/import_tracker.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/53e35eb9/ForgeTracker/forgetracker/scripts/import_tracker.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/scripts/import_tracker.py b/ForgeTracker/forgetracker/scripts/import_tracker.py
index a84bede..506e771 100644
--- a/ForgeTracker/forgetracker/scripts/import_tracker.py
+++ b/ForgeTracker/forgetracker/scripts/import_tracker.py
@@ -106,7 +106,7 @@ class ImportTracker(ScriptTask):
         parser.add_argument('-s', '--secret-key', action='store', dest='secret_key', help='Secret key')
         parser.add_argument('-p', '--project', action='store', dest='project', help='Project to import to')
         parser.add_argument('-t', '--tracker', action='store', dest='tracker', help='Tracker to import to')
-        parser.add_argument('-u', '--base-url', dest='base_url', default='https://sourceforge.net', help='Base Allura URL (%default)')
+        parser.add_argument('-u', '--base-url', dest='base_url', default='https://sourceforge.net', help='Base Allura URL (https://sourceforge.net)')
         parser.add_argument('-o', dest='import_opts', default=[], action='store',  help='Specify import option(s)', metavar='opt=val')
         parser.add_argument('--user-map', dest='user_map_file', help='Map original users to SF.net users', metavar='JSON_FILE')
         parser.add_argument('--file_data', dest='file_data', help='json file', metavar='JSON_FILE')


[13/23] git commit: [#6446] ticket:400 added filtering for topics in api, test fail

Posted by jo...@apache.org.
[#6446] ticket:400 added filtering for topics in api, test fail


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/dbb1cb19
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/dbb1cb19
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/dbb1cb19

Branch: refs/heads/cj/6461
Commit: dbb1cb191586af7293303208115a83efb1e07829
Parents: 625bf2e
Author: Anton Kasyanov <mi...@gmail.com>
Authored: Mon Jul 22 14:09:15 2013 +0300
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 14:55:48 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/discuss.py                  |  4 +++-
 .../forgediscussion/controllers/root.py         |  2 +-
 .../tests/functional/test_rest.py               | 21 ++++++++++++++++++++
 3 files changed, 25 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/dbb1cb19/Allura/allura/model/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/discuss.py b/Allura/allura/model/discuss.py
index 76cf615..97d19d0 100644
--- a/Allura/allura/model/discuss.py
+++ b/Allura/allura/model/discuss.py
@@ -328,13 +328,15 @@ class Thread(Artifact, ActivityObject):
         return result
 
     def query_posts(self, page=None, limit=None,
-                    timestamp=None, style='threaded'):
+                    timestamp=None, style='threaded', status=None):
         if timestamp:
             terms = dict(discussion_id=self.discussion_id, thread_id=self._id,
                     status={'$in': ['ok', 'pending']}, timestamp=timestamp)
         else:
             terms = dict(discussion_id=self.discussion_id, thread_id=self._id,
                     status={'$in': ['ok', 'pending']})
+        if status:
+            terms['status'] = status       
         q = self.post_class().query.find(terms)
         if style == 'threaded':
             q = q.sort('full_slug')

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/dbb1cb19/ForgeDiscussion/forgediscussion/controllers/root.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/controllers/root.py b/ForgeDiscussion/forgediscussion/controllers/root.py
index 67e51ca..7dd18c7 100644
--- a/ForgeDiscussion/forgediscussion/controllers/root.py
+++ b/ForgeDiscussion/forgediscussion/controllers/root.py
@@ -334,7 +334,7 @@ class ForumTopicRestController(BaseController):
     @expose('json:')
     def index(self, limit=100, page=0, **kw):
         limit, page, start = g.handle_paging(int(limit), int(page))
-        posts = self.topic.query_posts(page=page, limit=limit, style='')
+        posts = self.topic.query_posts(page=page, limit=limit, style='', status='ok')
         json = {}
         json['topic'] = self.topic.__json__()
         json['count'] = posts.count()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/dbb1cb19/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py b/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
index 56c01ff..7fc3499 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
@@ -168,6 +168,27 @@ class TestRootRestController(TestDiscussionApiBase):
         assert_equal(resp.json['page'], 1)
         assert_equal(resp.json['limit'], 1)
 
+    def test_topic_show_ok_only(self):
+        # import logging
+        # log = logging.getLogger(__name__)
+
+        thread = ForumThread.query.find({'subject': 'Hi guys'}).first()        
+        url = '/rest/p/test/discussion/general/thread/%s/' % thread._id
+        resp = self.app.get(url)
+        posts = resp.json['topic']['posts']
+        assert_equal(len(posts), 1)
+        thread.post('Hello', 'I am not ok post')
+        last_post = thread.last_post
+        last_post.status = 'pending'
+        last_post.commit()
+
+        resp = self.app.get(url)
+        posts = resp.json['topic']['posts']        
+
+        # log.info('ready to debug')
+        # log.info(posts)
+        assert_equal(len(posts), 1)
+
     def test_security(self):
         p = M.Project.query.get(shortname='test')
         acl = p.app_instance('discussion').config.acl


[11/23] git commit: [#6446] ticket:400 fixed test for showing only ok posts

Posted by jo...@apache.org.
[#6446] ticket:400 fixed test for showing only ok posts


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/24cd410c
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/24cd410c
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/24cd410c

Branch: refs/heads/cj/6461
Commit: 24cd410cfe4d9a89541faa1ed4c6204b394cc081
Parents: 58a2727
Author: Anton Kasyanov <mi...@gmail.com>
Authored: Mon Jul 22 16:31:34 2013 +0300
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 14:55:48 2013 +0000

----------------------------------------------------------------------
 .../forgediscussion/tests/functional/test_rest.py           | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/24cd410c/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py b/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
index 3aca6e0..6c45a2a 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
@@ -24,6 +24,7 @@ from allura.tests import decorators as td
 from allura import model as M
 from alluratest.controller import TestRestApiBase
 from forgediscussion.model import ForumThread
+from ming.orm import ThreadLocalORMSession
 
 
 class TestDiscussionApiBase(TestRestApiBase):
@@ -186,9 +187,6 @@ class TestRootRestController(TestDiscussionApiBase):
         assert_equal(resp.json['limit'], 1)
 
     def test_topic_show_ok_only(self):
-        # import logging
-        # log = logging.getLogger(__name__)
-
         thread = ForumThread.query.find({'subject': 'Hi guys'}).first()        
         url = '/rest/p/test/discussion/general/thread/%s/' % thread._id
         resp = self.app.get(url)
@@ -198,12 +196,9 @@ class TestRootRestController(TestDiscussionApiBase):
         last_post = thread.last_post
         last_post.status = 'pending'
         last_post.commit()
-
+        ThreadLocalORMSession.flush_all()
         resp = self.app.get(url)
         posts = resp.json['topic']['posts']        
-
-        # log.info('ready to debug')
-        # log.info(posts)
         assert_equal(len(posts), 1)
 
     def test_security(self):


[20/23] git commit: [#6456] Added documentation for ForgeImporters package

Posted by jo...@apache.org.
[#6456] Added documentation for ForgeImporters package

Signed-off-by: Cory Johns <cj...@slashdotmedia.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/227396fa
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/227396fa
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/227396fa

Branch: refs/heads/cj/6461
Commit: 227396fa98c0b7d00fffc52552e30a1198273116
Parents: 4ed7af8
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Tue Jul 23 22:34:53 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Jul 31 18:44:35 2013 +0000

----------------------------------------------------------------------
 .gitignore                                      |   2 +-
 ForgeImporters/docs/Makefile                    | 105 +++++++++
 ForgeImporters/docs/conf.py                     | 215 +++++++++++++++++++
 ForgeImporters/docs/framework.rst               |  46 ++++
 ForgeImporters/docs/importers/google.rst        |  30 +++
 ForgeImporters/docs/index.rst                   |  50 +++++
 ForgeImporters/docs/make.bat                    | 130 +++++++++++
 ForgeImporters/forgeimporters/base.py           |  60 +++++-
 ForgeImporters/forgeimporters/google/project.py |   7 +
 9 files changed, 642 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/227396fa/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 7b76704..658b4d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,7 +12,7 @@ tags
 *~
 *.swp
 .dbshell
-Allura/docs/_build/*
+*/docs/_build/*
 mail/logs/*
 sandbox-env/*
 download/*

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/227396fa/ForgeImporters/docs/Makefile
----------------------------------------------------------------------
diff --git a/ForgeImporters/docs/Makefile b/ForgeImporters/docs/Makefile
new file mode 100644
index 0000000..b3ca8af
--- /dev/null
+++ b/ForgeImporters/docs/Makefile
@@ -0,0 +1,105 @@
+#       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.
+
+# Makefile for Sphinx documentation
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html      to make standalone HTML files"
+	@echo "  dirhtml   to make HTML files named index.html in directories"
+	@echo "  pickle    to make pickle files"
+	@echo "  json      to make JSON files"
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  qthelp    to make HTML files and a qthelp project"
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck to check all external links for integrity"
+	@echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/allura.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/allura.qhc"
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/227396fa/ForgeImporters/docs/conf.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/docs/conf.py b/ForgeImporters/docs/conf.py
new file mode 100644
index 0000000..07430a5
--- /dev/null
+++ b/ForgeImporters/docs/conf.py
@@ -0,0 +1,215 @@
+# -*- 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.
+
+# allura documentation build configuration file, created by
+# sphinx-quickstart on Tue Nov 10 15:32:38 2009.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'Apache Allura (incubating)'
+copyright = '2012-2013 The Apache Software Foundation'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+#version = '0.1'
+# The full version, including alpha/beta/rc tags.
+#release = '0.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'sphinxdoc'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+html_title = 'ForgeImporters for Allura documentation'
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'alluradoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'allura.tex', u'allura Documentation',
+   u'Cory Johns, Tim Van Steenburgh, Dave Brondsema', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'http://docs.python.org/': None}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/227396fa/ForgeImporters/docs/framework.rst
----------------------------------------------------------------------
diff --git a/ForgeImporters/docs/framework.rst b/ForgeImporters/docs/framework.rst
new file mode 100644
index 0000000..496f59d
--- /dev/null
+++ b/ForgeImporters/docs/framework.rst
@@ -0,0 +1,46 @@
+..     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.
+
+:mod:`forgeimporters.base`
+==========================
+
+The following classes make up the base framework for
+importers.
+
+These can be used to create additional importers
+for Allura, which can be made available by creating an
+appropriate entry-point under `allura.project_importers` or
+`allura.importers` for project importers or tool importers,
+respectively.
+
+:class:`~forgeimporters.base.ProjectImporter`
+---------------------------------------------
+
+.. autoclass:: forgeimporters.base.ProjectImporter
+   :members:
+
+:class:`~forgeimporters.base.ToolImporter`
+------------------------------------------
+
+.. autoclass:: forgeimporters.base.ToolImporter
+   :members:
+
+:class:`~forgeimporters.base.ToolsValidator`
+--------------------------------------------
+
+.. autoclass:: forgeimporters.base.ToolsValidator
+   :members:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/227396fa/ForgeImporters/docs/importers/google.rst
----------------------------------------------------------------------
diff --git a/ForgeImporters/docs/importers/google.rst b/ForgeImporters/docs/importers/google.rst
new file mode 100644
index 0000000..5ed7306
--- /dev/null
+++ b/ForgeImporters/docs/importers/google.rst
@@ -0,0 +1,30 @@
+..     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.
+
+Google Code
+===========
+
+This importer imports projects and tools from Google Code.
+
+:mod:`forgeimporters.google`
+----------------------------
+
+.. autoclass:: forgeimporters.google.project.GoogleCodeProjectImporter
+   :members:
+
+.. autoclass:: forgeimporters.google.tracker.GoogleCodeTrackerImporter
+   :members:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/227396fa/ForgeImporters/docs/index.rst
----------------------------------------------------------------------
diff --git a/ForgeImporters/docs/index.rst b/ForgeImporters/docs/index.rst
new file mode 100644
index 0000000..37cfdcb
--- /dev/null
+++ b/ForgeImporters/docs/index.rst
@@ -0,0 +1,50 @@
+..     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 *ForgeImporters* Package
+============================
+
+This package contains the base framework for project and
+tool importers, as well as the core importers, for the
+Allura platform.
+
+Project importers will be available at
+:file:`/{nbhd-prefix}/import_project/{importer-name}/`,
+while individual tool importers will be available under the
+Import sidebar entry on the project admin page.
+
+Available Importers
+===================
+
+The following importers are available in this package for
+use with an Allura system.
+
+.. toctree::
+   :maxdepth: 1
+   :glob:
+
+   importers/*
+
+Importer Framework
+==================
+
+The following classes make up the base framework for
+importers.
+
+.. toctree::
+
+   framework

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/227396fa/ForgeImporters/docs/make.bat
----------------------------------------------------------------------
diff --git a/ForgeImporters/docs/make.bat b/ForgeImporters/docs/make.bat
new file mode 100644
index 0000000..611d010
--- /dev/null
+++ b/ForgeImporters/docs/make.bat
@@ -0,0 +1,130 @@
+@ECHO OFF
+
+REM    Licensed to the Apache Software Foundation (ASF) under one
+REM    or more contributor license agreements.  See the NOTICE file
+REM    distributed with this work for additional information
+REM    regarding copyright ownership.  The ASF licenses this file
+REM    to you under the Apache License, Version 2.0 (the
+REM    "License"); you may not use this file except in compliance
+REM    with the License.  You may obtain a copy of the License at
+
+REM      http://www.apache.org/licenses/LICENSE-2.0
+
+REM    Unless required by applicable law or agreed to in writing,
+REM    software distributed under the License is distributed on an
+REM    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+REM    KIND, either express or implied.  See the License for the
+REM    specific language governing permissions and limitations
+REM    under the License.
+
+REM Command file for Sphinx documentation
+
+set SPHINXBUILD=sphinx-build
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+	:help
+	echo.Please use `make ^<target^>` where ^<target^> is one of
+	echo.  html      to make standalone HTML files
+	echo.  dirhtml   to make HTML files named index.html in directories
+	echo.  pickle    to make pickle files
+	echo.  json      to make JSON files
+	echo.  htmlhelp  to make HTML files and a HTML help project
+	echo.  qthelp    to make HTML files and a qthelp project
+	echo.  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+	echo.  changes   to make an overview over all changed/added/deprecated items
+	echo.  linkcheck to check all external links for integrity
+	echo.  doctest   to run all doctests embedded in the documentation if enabled
+	goto end
+)
+
+if "%1" == "clean" (
+	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+	del /q /s %BUILDDIR%\*
+	goto end
+)
+
+if "%1" == "html" (
+	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+	goto end
+)
+
+if "%1" == "dirhtml" (
+	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+	goto end
+)
+
+if "%1" == "pickle" (
+	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+	echo.
+	echo.Build finished; now you can process the pickle files.
+	goto end
+)
+
+if "%1" == "json" (
+	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+	echo.
+	echo.Build finished; now you can process the JSON files.
+	goto end
+)
+
+if "%1" == "htmlhelp" (
+	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+	echo.
+	echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+	goto end
+)
+
+if "%1" == "qthelp" (
+	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+	echo.
+	echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\allura.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\allura.ghc
+	goto end
+)
+
+if "%1" == "latex" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	echo.
+	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "changes" (
+	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+	echo.
+	echo.The overview file is in %BUILDDIR%/changes.
+	goto end
+)
+
+if "%1" == "linkcheck" (
+	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+	echo.
+	echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+	goto end
+)
+
+if "%1" == "doctest" (
+	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+	echo.
+	echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+	goto end
+)
+
+:end

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/227396fa/ForgeImporters/forgeimporters/base.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/base.py b/ForgeImporters/forgeimporters/base.py
index bf22f8a..034a580 100644
--- a/ForgeImporters/forgeimporters/base.py
+++ b/ForgeImporters/forgeimporters/base.py
@@ -27,11 +27,19 @@ from allura.controllers import BaseController
 
 class ProjectImporter(BaseController):
     """
+    Base class for project importers.
+
+    Subclases are required to implement the :meth:`index()` and
+    :meth:`process()` views described below.
     """
     source = None
 
     @LazyProperty
     def tool_importers(self):
+        """
+        List of all tool importers that import from the same source
+        as this project importer.
+        """
         tools = {}
         for ep in iter_entry_points('allura.importers'):
             epv = ep.load()
@@ -43,7 +51,7 @@ class ProjectImporter(BaseController):
         """
         Override and expose this view to present the project import form.
 
-        The template used by this view should extend the base template in:
+        The template used by this view should extend the base template in::
 
             jinja:forgeimporters:templates/project_base.html
 
@@ -65,6 +73,29 @@ class ProjectImporter(BaseController):
 
 class ToolImporter(object):
     """
+    Base class for tool importers.
+
+    Subclasses are required to implement :meth:`import_tool()` described
+    below and define the following attributes:
+
+    .. py:attribute:: target_app
+
+       A reference or list of references to the tool(s) that this imports
+       to.  E.g.::
+
+            target_app = [forgegit.ForgeGitApp, forgehg.ForgeHgApp]
+
+    .. py:attribute:: source
+
+       A string indicating where this imports from.  This must match the
+       `source` value of the :class:`ProjectImporter` for this importer to
+       be discovered during full-project imports.  E.g.::
+
+            source = 'Google Code'
+
+    .. py:attribute:: controller
+
+       The controller for this importer, to handle single tool imports.
     """
     target_app = None  # app or list of apps
     source = None  # string description of source, must match project importer
@@ -72,11 +103,17 @@ class ToolImporter(object):
 
     @classmethod
     def by_name(self, name):
+        """
+        Return a ToolImporter subclass instance given its entry-point name.
+        """
         for ep in iter_entry_points('allura.importers', name):
             return ep.load()()
 
     @classmethod
     def by_app(self, app):
+        """
+        Return a ToolImporter subclass instance given its target_app class.
+        """
         importers = {}
         for ep in iter_entry_points('allura.importers'):
             importer = ep.load()
@@ -84,18 +121,31 @@ class ToolImporter(object):
                 importers[ep.name] = importer()
         return importers
 
-    def import_tool(self, project=None, mount_point=None):
+    def import_tool(self, project, project_name, mount_point=None, mount_label=None):
         """
         Override this method to perform the tool import.
+
+        :param project: the Allura project to import to
+        :param project_name: the name of the remote project to import from
+        :param mount_point: the mount point name, to override the default
+        :param mount_label: the mount label name, to override the default
         """
         raise NotImplementedError
 
     @property
     def tool_label(self):
+        """
+        The label for this tool importer.  Defaults to the `tool_label` from
+        the `target_app`.
+        """
         return getattr(aslist(self.target_app)[0], 'tool_label', None)
 
     @property
     def tool_description(self):
+        """
+        The description for this tool importer.  Defaults to the `tool_description`
+        from the `target_app`.
+        """
         return getattr(aslist(self.target_app)[0], 'tool_description', None)
 
     def tool_icon(self, theme, size):
@@ -103,6 +153,12 @@ class ToolImporter(object):
 
 
 class ToolsValidator(fev.Set):
+    """
+    Validates the list of tool importers during a project import.
+
+    This verifies that the tools selected are available and valid
+    for this source.
+    """
     def __init__(self, source, *a, **kw):
         super(ToolsValidator, self).__init__(*a, **kw)
         self.source = source

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/227396fa/ForgeImporters/forgeimporters/google/project.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/project.py b/ForgeImporters/forgeimporters/google/project.py
index 32b1976..d579606 100644
--- a/ForgeImporters/forgeimporters/google/project.py
+++ b/ForgeImporters/forgeimporters/google/project.py
@@ -47,6 +47,13 @@ class GoogleCodeProjectForm(schema.Schema):
 
 
 class GoogleCodeProjectImporter(base.ProjectImporter):
+    """
+    Project importer for Google Code.
+
+    This imports project metadata, including summary, icon, and license,
+    as well as providing the UI for importing individual tools during project
+    import.
+    """
     source = 'Google Code'
 
     def __init__(self, neighborhood, *a, **kw):


[04/23] git commit: [#6504] Refactored all project name validation into validator class

Posted by jo...@apache.org.
[#6504] Refactored all project name validation into validator class

Signed-off-by: Cory Johns <cj...@slashdotmedia.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/fff526fe
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/fff526fe
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/fff526fe

Branch: refs/heads/cj/6461
Commit: fff526fe7cbb03cacc941872731f61f0dd84225e
Parents: be9d822
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Tue Jul 30 18:21:34 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Tue Jul 30 20:12:00 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/project.py |  5 +--
 Allura/allura/lib/exceptions.py      |  5 ++-
 Allura/allura/lib/plugin.py          | 52 +++++--------------------------
 Allura/allura/lib/widgets/forms.py   | 39 ++++++++++++++++++-----
 Allura/allura/tests/test_plugin.py   | 46 ++++++++++-----------------
 5 files changed, 62 insertions(+), 85 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fff526fe/Allura/allura/controllers/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/project.py b/Allura/allura/controllers/project.py
index 91d5a4f..8af4396 100644
--- a/Allura/allura/controllers/project.py
+++ b/Allura/allura/controllers/project.py
@@ -83,8 +83,9 @@ class NeighborhoodController(object):
     def _lookup(self, pname, *remainder):
         pname = unquote(pname)
         provider = plugin.ProjectRegistrationProvider.get()
-        valid, reason = provider.valid_project_shortname(pname, self.neighborhood)
-        if not valid:
+        try:
+            provider.shortname_validator.to_python(pname, check_allowed=False, neighborhood=self.neighborhood)
+        except Invalid as e:
             raise exc.HTTPNotFound, pname
         project = M.Project.query.get(shortname=self.prefix + pname, neighborhood_id=self.neighborhood._id)
         if project is None and self.prefix == 'u/':

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fff526fe/Allura/allura/lib/exceptions.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/exceptions.py b/Allura/allura/lib/exceptions.py
index e222cf5..4c33991 100644
--- a/Allura/allura/lib/exceptions.py
+++ b/Allura/allura/lib/exceptions.py
@@ -15,8 +15,11 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
+from formencode import Invalid
+
 class ForgeError(Exception): pass
-class ProjectConflict(ForgeError): pass
+class ProjectConflict(ForgeError, Invalid): pass
+class ProjectShortnameInvalid(ForgeError, Invalid): pass
 class ProjectOverlimitError(ForgeError): pass
 class ProjectRatelimitError(ForgeError): pass
 class ToolError(ForgeError): pass

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fff526fe/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 7626bdb..4afd9fc 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -352,11 +352,18 @@ class ProjectRegistrationProvider(object):
         myprovider = foo.bar:MyAuthProvider
 
     Then in your .ini file, set registration.method=myprovider
+
+    The provider should expose an attribute, `shortname_validator` which is
+    an instance of a FormEncode validator that validates project shortnames.
+    The `to_python()` method of the validator should accept a `check_allowed`
+    argument to indicate whether additional checks beyond correctness of the
+    name should be done, such as whether the name is already in use.
     '''
 
     def __init__(self):
         from allura.lib.widgets import forms
         self.add_project_widget = forms.NeighborhoodAddProjectForm
+        self.shortname_validator = forms.NeighborhoodProjectShortNameValidator()
 
     @classmethod
     def get(cls):
@@ -364,15 +371,6 @@ class ProjectRegistrationProvider(object):
         method = config.get('registration.method', 'local')
         return app_globals.Globals().entry_points['registration'][method]()
 
-    def _name_taken(self, project_name, neighborhood):
-        """Return False if ``project_name`` is available in ``neighborhood``.
-        If unavailable, return an error message (str) explaining why.
-
-        """
-        from allura import model as M
-        p = M.Project.query.get(shortname=project_name, neighborhood_id=neighborhood._id)
-        return bool(p)
-
     def suggest_name(self, project_name, neighborhood):
         """Return a suggested project shortname for the full ``project_name``.
 
@@ -467,46 +465,12 @@ class ProjectRegistrationProvider(object):
             check_shortname = shortname.replace('u/', '', 1)
         else:
             check_shortname = shortname
-        allowed, err = self.allowed_project_shortname(check_shortname, neighborhood)
-        if not allowed:
-            raise ValueError('Invalid project shortname: %s error: %s' % (shortname, err))
+        self.shortname_validator.to_python(check_shortname, neighborhood=neighborhood)
 
         p = M.Project.query.get(shortname=shortname, neighborhood_id=neighborhood._id)
         if p:
             raise forge_exc.ProjectConflict('%s already exists in nbhd %s' % (shortname, neighborhood._id))
 
-    def valid_project_shortname(self, shortname, neighborhood):
-        """Determine if the project shortname appears to be valid.
-
-        Returns a pair of values, the first being a bool indicating whether
-        the name appears to be valid, and the second being a message indicating
-        the reason, if any, why the name is invalid.
-
-        NB: Even if a project shortname is valid, it might still not be
-        allowed (it could already be taken, for example).  Use the method
-        ``allowed_project_shortname`` instead to check if the shortname can
-        actually be used.
-        """
-        if not h.re_project_name.match(shortname):
-            return (False, 'Please use only letters, numbers, and dashes 3-15 characters long.')
-        return (True, None)
-
-    def allowed_project_shortname(self, shortname, neighborhood):
-        """Determine if a project shortname can be used.
-
-        A shortname can be used if it is valid and is not already taken.
-
-        Returns a pair of values, the first being a bool indicating whether
-        the name can be used, and the second being a message indicating the
-        reason, if any, why the name cannot be used.
-        """
-        valid, reason = self.valid_project_shortname(shortname, neighborhood)
-        if not valid:
-            return (False, reason)
-        if self._name_taken(shortname, neighborhood):
-            return (False, 'This project name is taken.')
-        return (True, None)
-
     def _create_project(self, neighborhood, shortname, project_name, user, user_project, private_project, apps):
         '''
         Actually create the project, no validation.  This should not be called directly

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fff526fe/Allura/allura/lib/widgets/forms.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py
index a5ebe15..ae90b26 100644
--- a/Allura/allura/lib/widgets/forms.py
+++ b/Allura/allura/lib/widgets/forms.py
@@ -22,6 +22,7 @@ from allura.lib import validators as V
 from allura.lib import helpers as h
 from allura.lib import plugin
 from allura.lib.widgets import form_fields as ffw
+from allura.lib import exceptions as forge_exc
 from allura import model as M
 from datetime import datetime
 
@@ -46,14 +47,33 @@ class _HTMLExplanation(ew.InputField):
         'jinja2')
 
 class NeighborhoodProjectShortNameValidator(fev.FancyValidator):
-
-    def to_python(self, value, state):
+    def _validate_shortname(self, shortname, neighborhood, state):
+        if not h.re_project_name.match(shortname):
+            raise forge_exc.ProjectShortnameInvalid(
+                    'Please use only letters, numbers, and dashes 3-15 characters long.',
+                    shortname, state)
+
+    def _validate_allowed(self, shortname, neighborhood, state):
+        p = M.Project.query.get(shortname=shortname, neighborhood_id=neighborhood._id)
+        if p:
+            raise forge_exc.ProjectConflict(
+                    'This project name is taken.',
+                    shortname, state)
+
+    def to_python(self, value, state=None, check_allowed=True, neighborhood=None):
+        """
+        Validate a project shortname.
+
+        If check_allowed is False, the shortname will only be checked for
+        correctness.  Otherwise, it will be rejected if already in use or
+        otherwise disallowed.
+        """
+        if neighborhood is None:
+            neighborhood = M.Neighborhood.query.get(name=state.full_dict['neighborhood'])
         value = h.really_unicode(value or '').encode('utf-8').lower()
-        neighborhood = M.Neighborhood.query.get(name=state.full_dict['neighborhood'])
-        provider = plugin.ProjectRegistrationProvider.get()
-        allowed, message = provider.allowed_project_shortname(value, neighborhood)
-        if not allowed:
-            raise formencode.Invalid(message, value, state)
+        self._validate_shortname(value, neighborhood, state)
+        if check_allowed:
+            self._validate_allowed(value, neighborhood, state)
         return value
 
 class ForgeForm(ew.SimpleForm):
@@ -780,7 +800,7 @@ class NeighborhoodAddProjectForm(ForgeForm):
                 V.MaxBytesValidator(max=40)))
         project_unixname = ew.InputField(
             label='Short Name', field_type='text',
-            validator=NeighborhoodProjectShortNameValidator())
+            validator=None)  # will be set in __init__
         tools = ew.CheckboxSet(name='tools', options=[
             ## Required for Neighborhood functional tests to pass
             ew.Option(label='Wiki', html_value='wiki', selected=True)
@@ -788,6 +808,9 @@ class NeighborhoodAddProjectForm(ForgeForm):
 
     def __init__(self, *args, **kwargs):
         super(NeighborhoodAddProjectForm, self).__init__(*args, **kwargs)
+        # get the shortname validator from the provider
+        provider = plugin.ProjectRegistrationProvider.get()
+        self.fields.project_unixname.validator = provider.shortname_validator
         ## Dynamically generating CheckboxSet of installable tools
         from allura.lib.widgets import forms
         self.fields.tools.options = [

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fff526fe/Allura/allura/tests/test_plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_plugin.py b/Allura/allura/tests/test_plugin.py
index 712fe52..57f8dbf 100644
--- a/Allura/allura/tests/test_plugin.py
+++ b/Allura/allura/tests/test_plugin.py
@@ -15,12 +15,15 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
-from nose.tools import assert_equals
+from functools import partial
+from nose.tools import assert_equals, assert_raises
 from mock import Mock, MagicMock, patch
+from formencode import Invalid
 
 from allura import model as M
 from allura.lib.utils import TruthyCallable
 from allura.lib.plugin import ProjectRegistrationProvider
+from allura.lib.exceptions import ProjectConflict, ProjectShortnameInvalid
 
 
 class TestProjectRegistrationProvider(object):
@@ -46,32 +49,15 @@ class TestProjectRegistrationProvider(object):
         assert_equals(f('A More Than Fifteen Character Name', Mock()),
                 'amorethanfifteencharactername')
 
-    def test_valid_project_shortname(self):
-        f = self.provider.valid_project_shortname
-        p = Mock()
-        valid = (True, None)
-        invalid = (False,
-                'Please use only letters, numbers, and dashes '
-                '3-15 characters long.')
-        assert_equals(f('thisislegit', p), valid)
-        assert_equals(f('not valid', p), invalid)
-        assert_equals(f('this-is-valid-but-too-long', p), invalid)
-        assert_equals(f('this is invalid and too long', p), invalid)
-
-    def test_allowed_project_shortname(self):
-        allowed = valid = (True, None)
-        invalid = (False, 'invalid')
-        taken = (False, 'This project name is taken.')
-        cases = [
-                (valid, False, allowed),
-                (invalid, False, invalid),
-                (valid, True, taken),
-            ]
-        p = Mock()
-        vps = self.provider.valid_project_shortname = Mock()
-        nt = self.provider._name_taken = Mock()
-        f = self.provider.allowed_project_shortname
-        for vps_v, nt_v, result in cases:
-            vps.return_value = vps_v
-            nt.return_value = nt_v
-            assert_equals(f('project', p), result)
+    @patch('allura.model.Project')
+    def test_shortname_validator(self, Project):
+        Project.query.get.return_value = None
+        nbhd = Mock()
+        v = self.provider.shortname_validator.to_python
+
+        v('thisislegit', neighborhood=nbhd)
+        assert_raises(ProjectShortnameInvalid, v, 'not valid', neighborhood=nbhd)
+        assert_raises(ProjectShortnameInvalid, v, 'this-is-valid-but-too-long', neighborhood=nbhd)
+        assert_raises(ProjectShortnameInvalid, v, 'this is invalid and too long', neighborhood=nbhd)
+        Project.query.get.return_value = Mock()
+        assert_raises(ProjectConflict, v, 'thisislegit', neighborhood=nbhd)


[03/23] git commit: [#6139] ticket:399 Improved Trac Wiki import scripts

Posted by jo...@apache.org.
[#6139] ticket:399 Improved Trac Wiki import scripts

* Copy the contents of the trac wiki home page to the Home page in the Allura wiki.
* Replace the wiki-toc div from trac with a Markdown [TOC] tag.
* Added the 'forgewiki.wiki_from_trac' ScriptTask that can do the export/import in one step


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/fcf24fc2
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/fcf24fc2
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/fcf24fc2

Branch: refs/heads/cj/6461
Commit: fcf24fc2e4fd7aa2bbfbf70cd17d9ba4f9b351da
Parents: 70d4cdb
Author: Vlad Glushchuk <vg...@gmail.com>
Authored: Fri Jul 26 10:15:21 2013 +0300
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Tue Jul 30 19:29:23 2013 +0000

----------------------------------------------------------------------
 .../scripts/wiki_from_trac/__init__.py          |  18 ++
 .../scripts/wiki_from_trac/extractors.py        | 226 +++++++++++++++++++
 .../forgewiki/scripts/wiki_from_trac/loaders.py |  72 ++++++
 .../scripts/wiki_from_trac/wiki_from_trac.py    |  69 ++++++
 requirements-optional.txt                       |   3 +
 scripts/allura_import.py                        |   6 +-
 scripts/wiki-export.py                          |  58 +++++
 7 files changed, 450 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fcf24fc2/ForgeWiki/forgewiki/scripts/wiki_from_trac/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/scripts/wiki_from_trac/__init__.py b/ForgeWiki/forgewiki/scripts/wiki_from_trac/__init__.py
new file mode 100644
index 0000000..8d3f8b7
--- /dev/null
+++ b/ForgeWiki/forgewiki/scripts/wiki_from_trac/__init__.py
@@ -0,0 +1,18 @@
+#       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 .wiki_from_trac import WikiFromTrac
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fcf24fc2/ForgeWiki/forgewiki/scripts/wiki_from_trac/extractors.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/scripts/wiki_from_trac/extractors.py b/ForgeWiki/forgewiki/scripts/wiki_from_trac/extractors.py
new file mode 100644
index 0000000..ef931b3
--- /dev/null
+++ b/ForgeWiki/forgewiki/scripts/wiki_from_trac/extractors.py
@@ -0,0 +1,226 @@
+#       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 re
+import sys
+import json
+from urllib import quote, unquote
+from urlparse import urljoin, urlsplit
+
+try:
+    import requests
+except:
+    # Ignore this import if the requests package is not installed
+    pass
+
+try:
+    # Ignore this import if the html2text package is not installed
+    import html2text
+except:
+    pass
+
+from BeautifulSoup import BeautifulSoup
+
+
+class WikiExporter(object):
+
+    PAGE_LIST_URL = 'wiki/TitleIndex'
+    PAGE_URL = 'wiki/%s'
+    CONTENT_DIV_ATTRS = {'class': 'wikipage searchable'}
+    EXCLUDE_PAGES = [
+        'CamelCase',
+        'InterMapTxt',
+        'InterTrac',
+        'InterWiki',
+        'PageTemplates',
+        'SandBox',
+        'TitleIndex',
+        'TracAccessibility',
+        'TracAdmin',
+        'TracBackup',
+        'TracBrowser',
+        'TracChangeset',
+        'TracEnvironment',
+        'TracFineGrainedPermissions',
+        'TracGuide',
+        'TracImport',
+        'TracIni',
+        'TracInterfaceCustomization',
+        'TracLinks',
+        'TracLogging',
+        'TracNavigation',
+        'TracNotification',
+        'TracPermissions',
+        'TracPlugins',
+        'TracQuery',
+        'TracReports',
+        'TracRevisionLog',
+        'TracRoadmap',
+        'TracRss',
+        'TracSearch',
+        'TracSupport',
+        'TracSyntaxColoring',
+        'TracTickets',
+        'TracTicketsCustomFields',
+        'TracTimeline',
+        'TracUnicode',
+        'TracWiki',
+        'TracWorkflow',
+        'WikiDeletePage',
+        'WikiFormatting',
+        'WikiHtml',
+        'WikiMacros',
+        'WikiNewPage',
+        'WikiPageNames',
+        'WikiProcessors',
+        'WikiRestructuredText',
+        'WikiRestructuredTextLinks',
+        'RecentChanges',
+    ]
+    RENAME_PAGES = {
+        'WikiStart': 'Home',  # Change the start page name to Home
+        'Home': 'WikiStart',  # Rename the Home page to WikiStart
+    }
+
+    def __init__(self, base_url, options):
+        self.base_url = base_url
+        self.options = options
+
+    def export(self, out):
+        pages = [self.get_page(title) for title in self.page_list()]
+        out.write(json.dumps(pages, indent=2, sort_keys=True))
+        out.write('\n')
+
+    def log(self, msg):
+        if self.options.verbose:
+            print >>sys.stderr, msg
+
+    def url(self, suburl, type=None):
+        url = urljoin(self.base_url, suburl)
+        if type is None:
+            return url
+        glue = '&' if '?' in suburl else '?'
+        return  url + glue + 'format=' + type
+
+    def fetch(self, url, **kwargs):
+        return requests.get(url, **kwargs)
+
+    def page_list(self):
+        url = urljoin(self.base_url, self.PAGE_LIST_URL)
+        self.log('Fetching list of pages from %s' % url)
+        r = self.fetch(url)
+        html = BeautifulSoup(r.content)
+        pages = html.find('div', attrs=self.CONTENT_DIV_ATTRS) \
+                    .find('ul').findAll('li')
+        pages = [page.find('a').text
+                 for page in pages
+                 if page.find('a')
+                 and page.find('a').text not in self.EXCLUDE_PAGES]
+        # Remove duplicate entries by converting page list to a set.
+        # As we're going to fetch all listed pages,
+        # it's safe to destroy the original order of pages.
+        return set(pages)
+
+    def get_page(self, title):
+        title = quote(title)
+        convert_method = '_get_page_' + self.options.converter
+        content = getattr(self, convert_method)(title)
+        page = {
+            'title': self.convert_title(title),
+            'text': self.convert_content(content),
+            'labels': '',
+        }
+        return page
+
+    def _get_page_html2text(self, title):
+        url = self.url(self.PAGE_URL % title)
+        self.log('Fetching page %s' % url)
+        r = self.fetch(url)
+        html = BeautifulSoup(r.content)
+        return html.find('div', attrs=self.CONTENT_DIV_ATTRS)
+
+    def _get_page_regex(self, title):
+        url = self.url(self.PAGE_URL % title, 'txt')
+        self.log('Fetching page %s' % url)
+        r = self.fetch(url)
+        return r.content
+
+    def convert_title(self, title):
+        title = self.RENAME_PAGES.get(title, title)
+        title = title.replace('/', '-')  # Handle subpages
+        title = title.rstrip('?')  # Links to non-existent pages ends with '?'
+        return title
+
+    def convert_content(self, content):
+        convert_method = '_convert_content_' + self.options.converter
+        return getattr(self, convert_method)(content)
+
+    def _convert_wiki_toc_to_markdown(self, content):
+        """
+        Removes contents of div.wiki-toc elements and replaces them with
+        the '[TOC]' markdown macro.
+        """
+        for toc in content('div', attrs={'class': 'wiki-toc'}):
+            toc.string = '[TOC]'
+        return content
+
+    def _convert_content_html2text(self, content):
+        html2text.BODY_WIDTH = 0  # Don't wrap lines
+        content = self._convert_wiki_toc_to_markdown(content)
+        content = html2text.html2text(unicode(content))
+        # Convert internal links
+        internal_url = urlsplit(self.base_url).path + 'wiki/'
+        internal_link_re = r'\[([^]]+)\]\(%s([^)]*)\)' % internal_url
+        internal_link = re.compile(internal_link_re, re.UNICODE)
+        def sub(match):
+            caption = match.group(1)
+            page = self.convert_title(match.group(2))
+            if caption == page:
+                link = '[%s]' % unquote(page)
+            else:
+                link = '[%s](%s)' % (caption, page)
+            return link
+        return internal_link.sub(sub, content)
+
+    def _convert_content_regex(self, text):
+        # https://gist.github.com/sgk/1286682
+        text = re.sub('\r\n', '\n', text)
+        text = re.sub(r'{{{(.*?)}}}', r'`\1`', text)
+
+        def indent4(m):
+            return '\n    ' + m.group(1).replace('\n', '\n    ')
+
+        text = re.sub(r'(?sm){{{\n(.*?)\n}}}', indent4, text)
+        text = re.sub(r'(?m)^====\s+(.*?)\s+====$', r'#### \1', text)
+        text = re.sub(r'(?m)^===\s+(.*?)\s+===$', r'### \1', text)
+        text = re.sub(r'(?m)^==\s+(.*?)\s+==$', r'## \1', text)
+        text = re.sub(r'(?m)^=\s+(.*?)\s+=$', r'# \1', text)
+        text = re.sub(r'^       * ', r'****', text)
+        text = re.sub(r'^     * ', r'***', text)
+        text = re.sub(r'^   * ', r'**', text)
+        text = re.sub(r'^ * ', r'*', text)
+        text = re.sub(r'^ \d+. ', r'1.', text)
+        a = []
+        for line in text.split('\n'):
+            if not line.startswith('    '):
+                line = re.sub(r'\[(https?://[^\s\[\]]+)\s([^\[\]]+)\]', r'[\2](\1)', line)
+                line = re.sub(r'\[(wiki:[^\s\[\]]+)\s([^\[\]]+)\]', r'[\2](/\1/)', line)
+                line = re.sub(r'\!(([A-Z][a-z0-9]+){2,})', r'\1', line)
+                line = re.sub(r'\'\'\'(.*?)\'\'\'', r'*\1*', line)
+                line = re.sub(r'\'\'(.*?)\'\'', r'_\1_', line)
+            a.append(line)
+        return '\n'.join(a)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fcf24fc2/ForgeWiki/forgewiki/scripts/wiki_from_trac/loaders.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/scripts/wiki_from_trac/loaders.py b/ForgeWiki/forgewiki/scripts/wiki_from_trac/loaders.py
new file mode 100644
index 0000000..55e4480
--- /dev/null
+++ b/ForgeWiki/forgewiki/scripts/wiki_from_trac/loaders.py
@@ -0,0 +1,72 @@
+#       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 json
+from optparse import OptionParser
+
+from allura.lib.import_api import AlluraImportApiClient
+
+
+def load_data(doc_file_name=None, optparser=None, options=None):
+    import_options = {}
+    for s in options.import_opts:
+        k, v = s.split('=', 1)
+        if v == 'false':
+            v = False
+        import_options[k] = v
+
+    user_map = {}
+    if options.user_map_file:
+        f = open(options.user_map_file)
+        try:
+            user_map = json.load(f)
+            if type(user_map) is not type({}):
+                raise ValueError
+            for k, v in user_map.iteritems():
+                print k, v
+                if not isinstance(k, basestring) or not isinstance(v, basestring):
+                    raise ValueError
+        except ValueError:
+            optparser.error('--user-map should specify JSON file with format {"original_user": "sf_user", ...}')
+        finally:
+            f.close()
+
+    import_options['user_map'] = user_map
+
+    cli = AlluraImportApiClient(options.base_url, options.api_key, options.secret_key, options.verbose)
+    doc_txt = open(doc_file_name).read()
+
+    if options.wiki:
+        import_wiki(cli, options.project, options.wiki, options, doc_txt)
+
+
+def import_wiki(cli, project, tool, options, doc_txt):
+    url = '/rest/p/' + project + '/' + tool
+    doc = json.loads(doc_txt)
+    if 'wiki' in doc and 'default' in doc['wiki'] and 'artifacts' in doc['wiki']['default']:
+        pages = doc['trackers']['default']['artifacts']
+    else:
+        pages = doc
+    if options.verbose:
+        print "Processing %d pages" % len(pages)
+    for page in pages:
+        title = page.pop('title').encode('utf-8')
+        page['text'] = page['text'].encode('utf-8')
+        page['labels'] = page['labels'].encode('utf-8')
+        r = cli.call(url + '/' + title, **page)
+        assert r == {}
+        print 'Imported wiki page %s' % title

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fcf24fc2/ForgeWiki/forgewiki/scripts/wiki_from_trac/wiki_from_trac.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/scripts/wiki_from_trac/wiki_from_trac.py b/ForgeWiki/forgewiki/scripts/wiki_from_trac/wiki_from_trac.py
new file mode 100644
index 0000000..b08df4f
--- /dev/null
+++ b/ForgeWiki/forgewiki/scripts/wiki_from_trac/wiki_from_trac.py
@@ -0,0 +1,69 @@
+#       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 argparse
+import logging
+from tempfile import NamedTemporaryFile
+from tg.decorators import cached_property
+
+from extractors import WikiExporter
+from loaders import load_data
+
+from allura.scripts import ScriptTask
+
+
+log = logging.getLogger(__name__)
+
+
+class WikiFromTrac(ScriptTask):
+    """Import Trac Wiki to Allura Wiki"""
+    @classmethod
+    def parser(cls):
+        parser = argparse.ArgumentParser(description='Import wiki from'
+            'Trac to allura wiki')
+
+        parser.add_argument('trac_url', type=str, help='Trac URL')
+        parser.add_argument('-a', '--api-ticket', dest='api_key', help='API ticket')
+        parser.add_argument('-s', '--secret-key', dest='secret_key', help='Secret key')
+        parser.add_argument('-p', '--project', dest='project', help='Project to import to')
+        parser.add_argument('-t', '--tracker', dest='tracker', help='Tracker to import to')
+        parser.add_argument('-f', '--forum', dest='forum', help='Forum tool to import to')
+        parser.add_argument('-w', '--wiki', dest='wiki', help='Wiki tool to import to')
+        parser.add_argument('-u', '--base-url', dest='base_url', default='https://sourceforge.net', help='Base Allura (%(default)s for default)')
+        parser.add_argument('-o', dest='import_opts', default=[], action='append', help='Specify import option(s)', metavar='opt=val')
+        parser.add_argument('--user-map', dest='user_map_file', help='Map original users to SF.net users', metavar='JSON_FILE')
+        parser.add_argument('--validate', dest='validate', action='store_true', help='Validate import data')
+        parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='Verbose operation')
+        parser.add_argument('-c', '--continue', dest='cont', action='store_true', help='Continue import into existing tracker')
+        parser.add_argument('-C', '--converter', dest='converter',
+                            default='html2text',
+                            help='Converter to use on wiki text. '
+                                 'Available options: '
+                                 'html2text (default) or regex')
+
+        return parser
+
+    @classmethod
+    def execute(cls, options):
+        with NamedTemporaryFile() as f:
+            WikiExporter(options.trac_url, options).export(f)
+            f.flush()
+            load_data(f.name, cls.parser(), options)
+
+
+if __name__ == '__main__':
+    WikiFromTrac.main()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fcf24fc2/requirements-optional.txt
----------------------------------------------------------------------
diff --git a/requirements-optional.txt b/requirements-optional.txt
index accdc43..da1f205 100644
--- a/requirements-optional.txt
+++ b/requirements-optional.txt
@@ -15,3 +15,6 @@ MySQL-python  # GPL
 # One or the other is required to enable spam checking
 akismet==0.2.0
 PyMollom==0.1  # GPL
+
+# For wiki-export script
+requests

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fcf24fc2/scripts/allura_import.py
----------------------------------------------------------------------
diff --git a/scripts/allura_import.py b/scripts/allura_import.py
index ddcd588..b05d524 100644
--- a/scripts/allura_import.py
+++ b/scripts/allura_import.py
@@ -17,10 +17,10 @@
 
 import json
 from optparse import OptionParser
-from datetime import datetime
 
 from allura.lib.import_api import AlluraImportApiClient
 from forgetracker.scripts.import_tracker import import_tracker
+from forgewiki.scripts.wiki_from_trac.loaders import import_wiki
 
 
 def main():
@@ -61,6 +61,8 @@ def main():
                        verbose=options.verbose)
     elif options.forum:
         import_forum(cli, options.project, options.forum, user_map, doc_txt, validate=options.validate)
+    elif options.wiki:
+        import_wiki(cli, options.project, options.wiki, options, doc_txt)
 
 
 def import_forum(cli, project, tool, user_map, doc_txt, validate=True):
@@ -82,6 +84,7 @@ Import project data dump in JSON format into an Allura project.''')
     optparser.add_option('-p', '--project', dest='project', help='Project to import to')
     optparser.add_option('-t', '--tracker', dest='tracker', help='Tracker to import to')
     optparser.add_option('-f', '--forum', dest='forum', help='Forum tool to import to')
+    optparser.add_option('-w', '--wiki', dest='wiki', help='Wiki tool to import to')
     optparser.add_option('-u', '--base-url', dest='base_url', default='https://sourceforge.net', help='Base Allura URL (%default)')
     optparser.add_option('-o', dest='import_opts', default=[], action='append', help='Specify import option(s)', metavar='opt=val')
     optparser.add_option('--user-map', dest='user_map_file', help='Map original users to SF.net users', metavar='JSON_FILE')
@@ -100,4 +103,3 @@ Import project data dump in JSON format into an Allura project.''')
 
 if __name__ == '__main__':
     main()
-

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/fcf24fc2/scripts/wiki-export.py
----------------------------------------------------------------------
diff --git a/scripts/wiki-export.py b/scripts/wiki-export.py
new file mode 100755
index 0000000..55baa04
--- /dev/null
+++ b/scripts/wiki-export.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+
+#       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 json
+import sys
+from optparse import OptionParser
+
+from forgewiki.scripts.wiki_from_trac.extractors import WikiExporter
+
+
+def parse_options():
+    parser = OptionParser(
+        usage='%prog <Trac URL>\n\nExport wiki pages from a trac instance')
+
+    parser.add_option('-o', '--out-file', dest='out_filename',
+                      help='Write to file (default stdout)')
+    parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
+                      help='Verbose operation')
+    parser.add_option('-c', '--converter', dest='converter',
+                      default='html2text',
+                      help='Converter to use on wiki text. '
+                           'Available options: html2text (default) or regex')
+    options, args = parser.parse_args()
+    if len(args) != 1:
+        parser.error('Wrong number of arguments.')
+    converters = ['html2text', 'regex']
+    if options.converter not in converters:
+        parser.error('Wrong converter. Available options: ' +
+                     ', '.join(converters))
+    return options, args
+
+
+if __name__ == '__main__':
+    options, args = parse_options()
+    exporter = WikiExporter(args[0], options)
+
+    out = sys.stdout
+    if options.out_filename:
+        out = open(options.out_filename, 'w')
+
+    exporter.export(out)
\ No newline at end of file


[18/23] git commit: Move requests pkg to common dependencies

Posted by jo...@apache.org.
Move requests pkg to common dependencies

Signed-off-by: Tim Van Steenburgh <tv...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/868694ec
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/868694ec
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/868694ec

Branch: refs/heads/cj/6461
Commit: 868694ecb4c4471741693e13c6b1033aa6151d79
Parents: 8b994ee
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Wed Jul 31 16:40:53 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 16:40:53 2013 +0000

----------------------------------------------------------------------
 requirements-common.txt   | 1 +
 requirements-optional.txt | 3 ---
 2 files changed, 1 insertion(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/868694ec/requirements-common.txt
----------------------------------------------------------------------
diff --git a/requirements-common.txt b/requirements-common.txt
index 81a470b..5e261a0 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -38,6 +38,7 @@ python-magic==0.4.3
 python-openid==2.2.5
 python-oembed==0.2.1
 pytidylib==0.2.1
+requests==1.2.3
 # for taskd proc name switching
 setproctitle==1.1.7
 # dep of pypeline

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/868694ec/requirements-optional.txt
----------------------------------------------------------------------
diff --git a/requirements-optional.txt b/requirements-optional.txt
index da1f205..accdc43 100644
--- a/requirements-optional.txt
+++ b/requirements-optional.txt
@@ -15,6 +15,3 @@ MySQL-python  # GPL
 # One or the other is required to enable spam checking
 akismet==0.2.0
 PyMollom==0.1  # GPL
-
-# For wiki-export script
-requests


[02/23] git commit: [#6139] ticket:399 Fix imports for ScriptTask

Posted by jo...@apache.org.
[#6139] ticket:399 Fix imports for ScriptTask


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/24d39a7d
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/24d39a7d
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/24d39a7d

Branch: refs/heads/cj/6461
Commit: 24d39a7d0703f9590e556edac9f5d083cb9240e7
Parents: fcf24fc
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Jul 26 08:05:27 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Tue Jul 30 19:29:23 2013 +0000

----------------------------------------------------------------------
 ForgeWiki/forgewiki/scripts/wiki_from_trac/wiki_from_trac.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/24d39a7d/ForgeWiki/forgewiki/scripts/wiki_from_trac/wiki_from_trac.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/scripts/wiki_from_trac/wiki_from_trac.py b/ForgeWiki/forgewiki/scripts/wiki_from_trac/wiki_from_trac.py
index b08df4f..65630aa 100644
--- a/ForgeWiki/forgewiki/scripts/wiki_from_trac/wiki_from_trac.py
+++ b/ForgeWiki/forgewiki/scripts/wiki_from_trac/wiki_from_trac.py
@@ -20,8 +20,8 @@ import logging
 from tempfile import NamedTemporaryFile
 from tg.decorators import cached_property
 
-from extractors import WikiExporter
-from loaders import load_data
+from forgewiki.scripts.wiki_from_trac.extractors import WikiExporter
+from forgewiki.scripts.wiki_from_trac.loaders import load_data
 
 from allura.scripts import ScriptTask
 


[19/23] git commit: [#6456] Refactored ProjectImporterDispatcher to Allura to remove dependency on FI

Posted by jo...@apache.org.
[#6456] Refactored ProjectImporterDispatcher to Allura to remove dependency on FI

Signed-off-by: Cory Johns <cj...@slashdotmedia.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/4ed7af81
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/4ed7af81
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/4ed7af81

Branch: refs/heads/cj/6461
Commit: 4ed7af81a3f595f4081f966f7d2547671e73f8b5
Parents: 868694e
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Tue Jul 23 18:26:48 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Jul 31 18:44:34 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/project.py  | 14 ++++++++++++--
 ForgeImporters/forgeimporters/base.py | 15 ++++-----------
 2 files changed, 16 insertions(+), 13 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4ed7af81/Allura/allura/controllers/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/project.py b/Allura/allura/controllers/project.py
index 8af4396..d2b8510 100644
--- a/Allura/allura/controllers/project.py
+++ b/Allura/allura/controllers/project.py
@@ -20,6 +20,7 @@ import logging
 from datetime import datetime, timedelta
 from urllib import unquote
 from itertools import chain, islice
+from pkg_resources import iter_entry_points
 
 from bson import ObjectId
 from tg import expose, flash, redirect, validate, request, response, config
@@ -47,7 +48,6 @@ from allura.lib.widgets import forms as ff
 from allura.lib.widgets import form_fields as ffw
 from allura.lib.widgets import project_list as plw
 from allura.lib import plugin, exceptions
-from forgeimporters.base import ProjectImporterDispatcher
 from .auth import AuthController
 from .search import SearchController, ProjectBrowseController
 from .static import NewForgeController
@@ -74,7 +74,7 @@ class NeighborhoodController(object):
         self.browse = NeighborhoodProjectBrowseController(neighborhood=self.neighborhood)
         self._admin = NeighborhoodAdminController(self.neighborhood)
         self._moderate = NeighborhoodModerateController(self.neighborhood)
-        self.import_project = ProjectImporterDispatcher(self.neighborhood)
+        self.import_project = ProjectImporterController(self.neighborhood)
 
     def _check_security(self):
         require_access(self.neighborhood, 'read')
@@ -919,3 +919,13 @@ class GrantController(object):
         with h.push_context(self.project._id):
             g.post_event('project_updated')
         redirect(request.referer)
+
+class ProjectImporterController(object):
+    def __init__(self, neighborhood, *a, **kw):
+        super(ProjectImporterController, self).__init__(*a, **kw)
+        self.neighborhood = neighborhood
+
+    @expose()
+    def _lookup(self, source, *rest):
+        for ep in iter_entry_points('allura.project_importers', source):
+            return ep.load()(self.neighborhood), rest

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4ed7af81/ForgeImporters/forgeimporters/base.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/base.py b/ForgeImporters/forgeimporters/base.py
index 777eb8a..bf22f8a 100644
--- a/ForgeImporters/forgeimporters/base.py
+++ b/ForgeImporters/forgeimporters/base.py
@@ -25,18 +25,9 @@ from ming.utils import LazyProperty
 from allura.controllers import BaseController
 
 
-class ProjectImporterDispatcher(BaseController):
-    def __init__(self, neighborhood, *a, **kw):
-        super(ProjectImporterDispatcher, self).__init__(*a, **kw)
-        self.neighborhood = neighborhood
-
-    @expose()
-    def _lookup(self, source, *rest):
-        for ep in iter_entry_points('allura.project_importers', source):
-            return ep.load()(self.neighborhood), rest
-
-
 class ProjectImporter(BaseController):
+    """
+    """
     source = None
 
     @LazyProperty
@@ -73,6 +64,8 @@ class ProjectImporter(BaseController):
 
 
 class ToolImporter(object):
+    """
+    """
     target_app = None  # app or list of apps
     source = None  # string description of source, must match project importer
     controller = None


[06/23] git commit: [#6441] ticket:398 fixed link with comments in tickets imported from trac

Posted by jo...@apache.org.
[#6441]  ticket:398 fixed link with comments  in tickets imported from trac


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/7975c7c7
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/7975c7c7
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/7975c7c7

Branch: refs/heads/cj/6461
Commit: 7975c7c778e2ca346a3084513ee2fdb24ae253db
Parents: 70ee25a
Author: Yuriy Arhipov <yu...@yandex.ru>
Authored: Tue Jul 23 07:19:47 2013 +0400
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 12:21:14 2013 +0000

----------------------------------------------------------------------
 ForgeTracker/forgetracker/import_support.py            | 13 ++++++++-----
 .../forgetracker/tests/functional/test_import.py       |  4 ++--
 2 files changed, 10 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/7975c7c7/ForgeTracker/forgetracker/import_support.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/import_support.py b/ForgeTracker/forgetracker/import_support.py
index 8735f40..26e182e 100644
--- a/ForgeTracker/forgetracker/import_support.py
+++ b/ForgeTracker/forgetracker/import_support.py
@@ -217,7 +217,8 @@ class ImportSupport(object):
         return '(%s)' % m.groups()[0]
 
     def ticket_bracket_link(self, m):
-        return '[#%s]' % m.groups()[0]
+        text = m.groups()[0]
+        return '[\[%s\]](%s:#%s)' % (text, c.app.config.options.mount_point, text)
 
     def get_slug_by_id(self, ticket, comment):
         comment = int(comment)
@@ -234,19 +235,21 @@ class ImportSupport(object):
             return comments.all()[comment-1].slug
 
     def comment_link(self, m):
-        ticket, comment = m.groups()
+        text, ticket, comment = m.groups()
+        ticket = ticket.replace('\n', '')
+        text = text.replace('\n', ' ')
         slug = self.get_slug_by_id(ticket, comment)
         if slug:
-            return '(%s#%s)' % (ticket, self.get_slug_by_id(ticket, comment))
+            return '[%s](%s#%s)' % (text, ticket, slug)
         else:
-            return '\(%s#comment:%s\)' % (ticket, comment)
+            return text
 
     def brackets_escaping(self, m):
         return '[\[%s\]]' % m.groups()[0]
 
     def link_processing(self, text):
         short_link_ticket_pattern = re.compile('(?<!\[)#(\d+)(?!\])')
-        comment_pattern = re.compile('\(\S*/(\d+)#comment:(\d+)\)')
+        comment_pattern = re.compile('\[(\S*\s*\S*)\]\(\S*/(\d+\n*\d*)#comment:(\d+)\)')
         ticket_pattern = re.compile('(?<=\])\(\S*ticket/(\d+)\)')
         brackets_pattern = re.compile('\[\[(.*)\]\]')
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/7975c7c7/ForgeTracker/forgetracker/tests/functional/test_import.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tests/functional/test_import.py b/ForgeTracker/forgetracker/tests/functional/test_import.py
index 1fc7331..f876b93 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_import.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_import.py
@@ -177,10 +177,10 @@ class TestImportController(TestRestApiBase):
                                        #200''')
 
         assert "test link [\[2496\]](http://testlink.com)" in result
-        assert '[test comment]\(204#comment:1\)' in result
+        assert 'test comment' in result
         assert 'test link [\[2496\]](http://testlink.com)' in result
         assert 'test ticket ([#201](201))' in result
-        assert '[#200]' in result
+        assert '[\[200\]](bugs:#200)' in result, result
 
     @td.with_tracker
     def test_links(self):


[05/23] git commit: [#6441] ticket:398 fixed hyperlinks in tickets imported from trac

Posted by jo...@apache.org.
[#6441]  ticket:398 fixed hyperlinks in tickets imported from trac


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/b3fea697
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/b3fea697
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/b3fea697

Branch: refs/heads/cj/6461
Commit: b3fea697bf281649e6dc8cd63a878f9cb45859f3
Parents: fff526f
Author: Yuriy Arhipov <yu...@yandex.ru>
Authored: Fri Jul 19 14:39:19 2013 +0400
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 12:21:13 2013 +0000

----------------------------------------------------------------------
 ForgeTracker/forgetracker/import_support.py     | 37 +++++++++++-
 .../forgetracker/tests/functional/data/sf.json  |  6 ++
 .../tests/functional/test_import.py             | 59 ++++++++++++++++++++
 3 files changed, 101 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b3fea697/ForgeTracker/forgetracker/import_support.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/import_support.py b/ForgeTracker/forgetracker/import_support.py
index a94e70c..268f727 100644
--- a/ForgeTracker/forgetracker/import_support.py
+++ b/ForgeTracker/forgetracker/import_support.py
@@ -18,6 +18,7 @@
 #-*- python -*-
 import logging
 import json
+import re
 from datetime import datetime
 from cStringIO import StringIO
 
@@ -211,9 +212,43 @@ class ImportSupport(object):
         ticket.update(remapped)
         return ticket
 
+    def ticket_link(self, m):
+        return '(%s)' % m.groups()[0]
+
+    def get_slug_by_id(self, ticket, comment):
+        comment = int(comment)
+        ticket = TM.Ticket.query.get(app_config_id=c.app.config._id,
+                                     ticket_num=int(ticket))
+        if not ticket:
+            return ''
+        comments = ticket.discussion_thread.post_class().query.find(dict(
+            discussion_id=ticket.discussion_thread.discussion_id,
+            thread_id=ticket.discussion_thread._id,
+            status={'$in': ['ok', 'pending']})).sort('timestamp')
+
+        if comment <= comments.count():
+            return comments.all()[comment-1].slug
+
+    def comment_link(self, m):
+        ticket, comment = m.groups()
+        return '(%s#%s)' % (ticket, self.get_slug_by_id(ticket, comment))
+
+    def brackets_escaping(self, m):
+        return '[%s]' % m.groups()[0]
+
+    def link_processing(self, text):
+        comment_pattern = re.compile('\(\S*/(\d+)#comment:(\d+)\)')
+        ticket_pattern = re.compile('(?<=\])\(\S*ticket/(\d+)\)')
+        brackets_pattern = re.compile('\[\[(.*)\]\]')
+
+        text = comment_pattern.sub(self.comment_link, text.replace('\n', ''))
+        text = ticket_pattern.sub(self.ticket_link, text)
+        text = brackets_pattern.sub(self.brackets_escaping, text)
+        return text
+
     def make_comment(self, thread, comment_dict):
         ts = self.parse_date(comment_dict['date'])
-        comment = thread.post(text=comment_dict['comment'], timestamp=ts)
+        comment = thread.post(text=self.link_processing(comment_dict['comment']), timestamp=ts)
         comment.author_id = self.get_user_id(comment_dict['submitter'])
         comment.import_id = c.api_token.api_key
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b3fea697/ForgeTracker/forgetracker/tests/functional/data/sf.json
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tests/functional/data/sf.json b/ForgeTracker/forgetracker/tests/functional/data/sf.json
index 58e322c..5a11379 100644
--- a/ForgeTracker/forgetracker/tests/functional/data/sf.json
+++ b/ForgeTracker/forgetracker/tests/functional/data/sf.json
@@ -41,6 +41,12 @@
               "comment": "  * **status** changed from _accepted_ to _closed_\n\n  * **resolution** set to _fixed_\n\nHello,\n\nThis issue is should be resolved with the site redesign.\n\nRegards, Chris Tsai, SourceForge.net Support\n\n", 
               "date": "2009-07-20T15:44:32Z", 
               "submitter": "ctsai"
+            },
+            {
+              "class": "COMMENT",
+              "comment": "test link [[2496]](http://testlink.com)  test ticket ([#201](http://sourceforge.net/apps/trac/sourceforge/ticket/201)) \n [test comment](http://sourceforge.net/apps/trac/sourceforge/ticket/204#comment:1)",
+              "date": "2009-07-21T15:44:32Z",
+              "submitter": "ctsai"
             }
           ], 
           "date": "2009-04-13T08:49:13Z", 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/b3fea697/ForgeTracker/forgetracker/tests/functional/test_import.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tests/functional/test_import.py b/ForgeTracker/forgetracker/tests/functional/test_import.py
index 06fde25..202c757 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_import.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_import.py
@@ -24,10 +24,13 @@ from nose.tools import assert_equal
 
 import ming
 from pylons import app_globals as g
+from pylons import tmpl_context as c
 
 from allura import model as M
 from alluratest.controller import TestRestApiBase
 from allura.tests import decorators as td
+from forgetracker import model as TM
+from forgetracker.import_support import ImportSupport
 
 class TestImportController(TestRestApiBase):
 
@@ -164,3 +167,59 @@ class TestImportController(TestRestApiBase):
         assert ticket_json['summary'] in r
         r = self.app.get('/p/test/bugs/')
         assert ticket_json['summary'] in r
+
+    @td.with_tracker
+    def test_link_processing(self):
+        import_support = ImportSupport()
+        result = import_support.link_processing('''test link [[2496]](http://testlink.com)
+                                       test ticket ([#201](http://sourceforge.net/apps/trac/sourceforge/ticket/201))
+                                       [test comment](http://sourceforge.net/apps/trac/sourceforge/ticket/204#comment:1)''')
+
+        assert "test link [2496](http://testlink.com)" in result
+        assert '[test comment](204#)' in result
+        assert 'test link [2496](http://testlink.com)' in result
+        assert 'test ticket ([#201](201))' in result
+
+    @td.with_tracker
+    def test_links(self):
+        api_ticket = M.ApiTicket(user_id=self.user._id, capabilities={'import': ['Projects','test']},
+                                 expires=datetime.utcnow() + timedelta(days=1))
+        ming.orm.session(api_ticket).flush()
+        self.set_api_token(api_ticket)
+
+        doc_text = open(os.path.dirname(__file__) + '/data/sf.json').read()
+        self.api_post('/rest/p/test/bugs/perform_import',
+                      doc=doc_text, options='{"user_map": {"hinojosa4": "test-admin", "ma_boehm": "test-user"}}')
+
+        r = self.app.get('/p/test/bugs/204/')
+        ticket = TM.Ticket.query.get(app_config_id=c.app.config._id,
+                                    ticket_num=204)
+        slug = ticket.discussion_thread.post_class().query.find(dict(
+            discussion_id=ticket.discussion_thread.discussion_id,
+            thread_id=ticket.discussion_thread._id,
+            status={'$in': ['ok', 'pending']})).sort('timestamp').all()[0].slug
+
+        assert '[test comment](204#%s)' % slug in r
+        assert 'test link [2496](http://testlink.com)' in r
+        assert 'test ticket ([#201](201))' in r
+
+    @td.with_tracker
+    def test_slug(self):
+        api_ticket = M.ApiTicket(user_id=self.user._id, capabilities={'import': ['Projects','test']},
+                                 expires=datetime.utcnow() + timedelta(days=1))
+        ming.orm.session(api_ticket).flush()
+        self.set_api_token(api_ticket)
+
+        doc_text = open(os.path.dirname(__file__) + '/data/sf.json').read()
+        self.api_post('/rest/p/test/bugs/perform_import',
+                      doc=doc_text, options='{"user_map": {"hinojosa4": "test-admin", "ma_boehm": "test-user"}}')
+        ticket = TM.Ticket.query.get(app_config_id=c.app.config._id,
+                                    ticket_num=204)
+        comments = ticket.discussion_thread.post_class().query.find(dict(
+            discussion_id=ticket.discussion_thread.discussion_id,
+            thread_id=ticket.discussion_thread._id,
+            status={'$in': ['ok', 'pending']})).sort('timestamp').all()
+
+        import_support = ImportSupport()
+        assert_equal(import_support.get_slug_by_id('204', '1'), comments[0].slug)
+        assert_equal(import_support.get_slug_by_id('204', '2'), comments[1].slug)


[22/23] git commit: [#5543] Require confirmation on post delete

Posted by jo...@apache.org.
[#5543] Require confirmation on post delete

Signed-off-by: Tim Van Steenburgh <tv...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/4343ee03
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/4343ee03
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/4343ee03

Branch: refs/heads/cj/6461
Commit: 4343ee0366e682727db54bb4ac8694ef6f0ca391
Parents: 158cd46
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Tue Jul 30 21:03:18 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Jul 31 18:49:08 2013 +0000

----------------------------------------------------------------------
 Allura/allura/lib/widgets/discuss.py | 3 +++
 1 file changed, 3 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4343ee03/Allura/allura/lib/widgets/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/widgets/discuss.py b/Allura/allura/lib/widgets/discuss.py
index e62a993..eaec991 100644
--- a/Allura/allura/lib/widgets/discuss.py
+++ b/Allura/allura/lib/widgets/discuss.py
@@ -289,6 +289,9 @@ class Post(HierWidget):
                 $('.moderate_post', post).click(function(e){
                     e.preventDefault();
                     var mod = $(this).text();
+                    if (mod === 'Delete' && !confirm('Really delete this post?')) {
+                        return;
+                    }
                     var id_post = $(post).attr('id');
                     $.ajax({
                         type: 'POST',


[10/23] git commit: [#6460] Get root_project of artifact

Posted by jo...@apache.org.
[#6460] Get root_project of artifact

Signed-off-by: Tim Van Steenburgh <tv...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/625bf2ec
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/625bf2ec
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/625bf2ec

Branch: refs/heads/cj/6461
Commit: 625bf2ec9dfe3765d7d8813324b8a61986aba51f
Parents: 4b2d8c1
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Wed Jul 31 14:18:47 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 14:18:47 2013 +0000

----------------------------------------------------------------------
 Allura/allura/lib/security.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/625bf2ec/Allura/allura/lib/security.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/security.py b/Allura/allura/lib/security.py
index a39e68d..a0497ca 100644
--- a/Allura/allura/lib/security.py
+++ b/Allura/allura/lib/security.py
@@ -287,9 +287,8 @@ def has_access(obj, permission, user=None, project=None):
                 elif isinstance(obj, M.Project):
                     project = obj.root_project
                 else:
-                    project = getattr(obj, 'project', None)
-                    if project is None:
-                        project = c.project.root_project
+                    project = getattr(obj, 'project', None) or c.project
+                    project = project.root_project
             roles = cred.user_roles(user_id=user._id, project_id=project._id).reaching_ids
         chainable_roles = []
         for rid in roles:


[23/23] git commit: [#6461] Partial implementation of GC Issues importer based on gdata API

Posted by jo...@apache.org.
[#6461] Partial implementation of GC Issues importer based on gdata API

The gdata API was shut off, so to avoid unnecessary work, most of the
GDataAPI classes are left unimplemented and untested, but the core logic
in GoogleCodeTrackerImporter is implemented and tested.

Signed-off-by: Cory Johns <cj...@slashdotmedia.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/c7fd7876
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/c7fd7876
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/c7fd7876

Branch: refs/heads/cj/6461
Commit: c7fd787689c12f2b95c0530eff66cfe9e509ae0f
Parents: 4343ee0
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Mon Jul 29 00:15:16 2013 +0000
Committer: Cory Johns <cj...@slashdotmedia.com>
Committed: Wed Jul 31 19:13:41 2013 +0000

----------------------------------------------------------------------
 .../forgeimporters/google/__init__.py           |   5 +-
 ForgeImporters/forgeimporters/google/code.py    |  13 +-
 ForgeImporters/forgeimporters/google/project.py |   4 +-
 ForgeImporters/forgeimporters/google/tasks.py   |   8 +-
 .../forgeimporters/google/tests/test_code.py    |  22 +-
 ForgeImporters/forgeimporters/google/tracker.py | 260 +++++++++++++++++++
 .../tests/google/test_extractor.py              |  12 +-
 .../forgeimporters/tests/google/test_tasks.py   |   8 +-
 .../forgeimporters/tests/google/test_tracker.py | 234 +++++++++++++++++
 ForgeImporters/setup.py                         |   1 +
 requirements-common.txt                         |   1 +
 11 files changed, 523 insertions(+), 45 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/ForgeImporters/forgeimporters/google/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/__init__.py b/ForgeImporters/forgeimporters/google/__init__.py
index 17d724f..57e384b 100644
--- a/ForgeImporters/forgeimporters/google/__init__.py
+++ b/ForgeImporters/forgeimporters/google/__init__.py
@@ -56,10 +56,9 @@ class GoogleCodeProjectExtractor(object):
 
     DEFAULT_ICON = 'http://www.gstatic.com/codesite/ph/images/defaultlogo.png'
 
-    def __init__(self, project, page='project_info'):
-        gc_project_name = project.get_tool_data('google-code', 'project_name')
+    def __init__(self, allura_project, gc_project_name, page):
+        self.project = allura_project
         self.url = self.PAGE_MAP[page] % urllib.quote(gc_project_name)
-        self.project = project
         self.page = BeautifulSoup(urllib2.urlopen(self.url))
 
     def get_short_description(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/ForgeImporters/forgeimporters/google/code.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/code.py b/ForgeImporters/forgeimporters/google/code.py
index 8e047fb..ef7f800 100644
--- a/ForgeImporters/forgeimporters/google/code.py
+++ b/ForgeImporters/forgeimporters/google/code.py
@@ -86,8 +86,8 @@ class GoogleRepoImportController(BaseController):
     @require_post()
     @validate(GoogleRepoImportSchema(), error_handler=index)
     def create(self, gc_project_name, mount_point, mount_label, **kw):
-        c.project.set_tool_data('google-code', project_name=gc_project_name)
         app = GoogleRepoImporter.import_tool(c.project,
+                project_name=gc_project_name,
                 mount_point=mount_point,
                 mount_label=mount_label)
         redirect(app.url())
@@ -100,18 +100,13 @@ class GoogleRepoImporter(ToolImporter):
     tool_label = 'Google Code Source Importer'
     tool_description = 'Import your SVN, Git, or Hg repo from Google Code'
 
-    def import_tool(self, project=None, mount_point=None, mount_label=None):
+    def import_tool(self, project, project_name, mount_point=None, mount_label=None):
         """ Import a Google Code repo into a new SVN, Git, or Hg Allura tool.
 
         """
-        if not project:
-            raise Exception("You must supply a project")
-        if not project.get_tool_data('google-code', 'project_name'):
-            raise Exception("Missing Google Code project name")
-        extractor = GoogleCodeProjectExtractor(project, page='source_browse')
+        extractor = GoogleCodeProjectExtractor(project, project_name, 'source_browse')
         repo_type = extractor.get_repo_type()
-        repo_url = get_repo_url(project.get_tool_data('google-code',
-            'project_name'), repo_type)
+        repo_url = get_repo_url(project_name, repo_type)
         app = project.install_app(
                 REPO_ENTRY_POINTS[repo_type],
                 mount_point=mount_point or 'code',

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/ForgeImporters/forgeimporters/google/project.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/project.py b/ForgeImporters/forgeimporters/google/project.py
index d579606..7416258 100644
--- a/ForgeImporters/forgeimporters/google/project.py
+++ b/ForgeImporters/forgeimporters/google/project.py
@@ -90,9 +90,9 @@ class GoogleCodeProjectImporter(base.ProjectImporter):
             redirect('.')
 
         c.project.set_tool_data('google-code', project_name=project_name)
-        tasks.import_project_info.post()
+        tasks.import_project_info.post(project_name)
         for importer_name in tools:
-            tasks.import_tool.post(importer_name)
+            tasks.import_tool.post(importer_name, project_name)
 
         flash('Welcome to the %s Project System! '
               'Your project data will be imported and should show up here shortly.' % config['site_name'])

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/ForgeImporters/forgeimporters/google/tasks.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/tasks.py b/ForgeImporters/forgeimporters/google/tasks.py
index 65dd126..3e6e74d 100644
--- a/ForgeImporters/forgeimporters/google/tasks.py
+++ b/ForgeImporters/forgeimporters/google/tasks.py
@@ -27,8 +27,8 @@ from ..base import ToolImporter
 
 
 @task
-def import_project_info():
-    extractor = GoogleCodeProjectExtractor(c.project, 'project_info')
+def import_project_info(project_name):
+    extractor = GoogleCodeProjectExtractor(c.project, project_name, 'project_info')
     extractor.get_short_description()
     extractor.get_icon()
     extractor.get_license()
@@ -36,6 +36,6 @@ def import_project_info():
     g.post_event('project_updated')
 
 @task
-def import_tool(importer_name, mount_point=None, mount_label=None):
+def import_tool(importer_name, project_name, mount_point=None, mount_label=None):
     importer = ToolImporter.by_name(importer_name)
-    importer.import_tool(c.project, mount_point, mount_label)
+    importer.import_tool(c.project, project_name, mount_point, mount_label)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/ForgeImporters/forgeimporters/google/tests/test_code.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/tests/test_code.py b/ForgeImporters/forgeimporters/google/tests/test_code.py
index cc91178..fe6943b 100644
--- a/ForgeImporters/forgeimporters/google/tests/test_code.py
+++ b/ForgeImporters/forgeimporters/google/tests/test_code.py
@@ -56,30 +56,20 @@ class TestGoogleRepoImporter(TestCase):
         project.get_tool_data.side_effect = lambda *args: gc_proj_name
         return project
 
-    @patch('forgeimporters.google.code.GoogleCodeProjectExtractor.get_repo_type')
+    @patch('forgeimporters.google.code.GoogleCodeProjectExtractor')
     @patch('forgeimporters.google.code.get_repo_url')
-    def test_import_tool_happy_path(self, get_repo_url, get_repo_type):
-        get_repo_type.return_value = 'git'
+    def test_import_tool_happy_path(self, get_repo_url, gcpe):
+        gcpe.return_value.get_repo_type.return_value = 'git'
         get_repo_url.return_value = 'http://remote/clone/url/'
         p = self._make_project(gc_proj_name='myproject')
-        GoogleRepoImporter().import_tool(p)
+        GoogleRepoImporter().import_tool(p, 'project_name')
+        get_repo_url.assert_called_once_with('project_name', 'git')
         p.install_app.assert_called_once_with('Git',
                 mount_point='code',
                 mount_label='Code',
                 init_from_url='http://remote/clone/url/',
                 )
 
-    def test_no_project(self):
-        with self.assertRaises(Exception) as cm:
-            GoogleRepoImporter().import_tool()
-        self.assertEqual(str(cm.exception), "You must supply a project")
-
-    def test_no_google_code_project_name(self):
-        p = self._make_project()
-        with self.assertRaises(Exception) as cm:
-            GoogleRepoImporter().import_tool(p)
-        self.assertEqual(str(cm.exception), "Missing Google Code project name")
-
 
 class TestGoogleRepoImportController(TestController, TestCase):
     def setUp(self):
@@ -110,8 +100,6 @@ class TestGoogleRepoImportController(TestController, TestCase):
                 status=302)
         project = M.Project.query.get(shortname=test_project_with_repo)
         self.assertEqual(r.location, 'http://localhost/p/{}/mymount'.format(test_project_with_repo))
-        self.assertEqual(project.get_tool_data('google-code', 'project_name'),
-                'poop')
         self.assertEqual(project._id, gri.import_tool.call_args[0][0]._id)
         self.assertEqual(u'mymount', gri.import_tool.call_args[1]['mount_point'])
         self.assertEqual(u'mylabel', gri.import_tool.call_args[1]['mount_label'])

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/ForgeImporters/forgeimporters/google/tracker.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/tracker.py b/ForgeImporters/forgeimporters/google/tracker.py
new file mode 100644
index 0000000..602690e
--- /dev/null
+++ b/ForgeImporters/forgeimporters/google/tracker.py
@@ -0,0 +1,260 @@
+#       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 collections import defaultdict
+from datetime import datetime
+
+from pylons import tmpl_context as c
+import gdata
+from ming.orm import session
+
+from allura.lib import helpers as h
+
+from forgetracker.tracker_main import ForgeTrackerApp
+from forgetracker import model as TM
+from ..base import ToolImporter
+
+
+class GoogleCodeTrackerImporter(ToolImporter):
+    source = 'Google Code'
+    target_app = ForgeTrackerApp
+    controller = None
+    tool_label = 'Issues'
+
+    field_types = defaultdict(lambda: 'string',
+            milestone='milestone',
+            priority='select',
+            type='select',
+        )
+
+    def import_tool(self, project, project_name, mount_point=None, mount_label=None):
+        c.app = project.install_app('tracker', mount_point, mount_label)
+        c.app.globals.open_status_names = ['New', 'Accepted', 'Started']
+        c.app.globals.closed_status_names = ['Fixed', 'Verified', 'Invalid', 'Duplicate', 'WontFix', 'Done']
+        self.custom_fields = {}
+        extractor = GDataAPIExtractor(project_name)
+        for issue in extractor.iter_issues():
+            ticket = TM.Ticket.new()
+            self.process_fields(ticket, issue)
+            self.process_labels(ticket, issue)
+            self.process_comments(ticket, extractor.iter_comments(issue))
+            session(ticket).flush(ticket)
+            session(ticket).expunge(ticket)
+        self.postprocess_custom_fields()
+        session(c.app).flush(c.app)
+        session(c.app.globals).flush(c.app.globals)
+
+    def custom_field(self, name):
+        if name not in self.custom_fields:
+            self.custom_fields[name] = {
+                    'type': self.field_types[name.lower()],
+                    'label': name,
+                    'name': u'_%s' % name.lower(),
+                    'options': set(),
+                }
+        return self.custom_fields[name]
+
+    def process_fields(self, ticket, issue):
+        ticket.summary = issue.summary
+        ticket.description = issue.description
+        ticket.status = issue.status
+        ticket.created_date = datetime.strptime(issue.created_date, '')
+        ticket.mod_date = datetime.strptime(issue.mod_date, '')
+
+    def process_labels(self, ticket, issue):
+        labels = set()
+        custom_fields = defaultdict(set)
+        for label in issue.labels:
+            if u'-' in label:
+                name, value = label.split(u'-', 1)
+                cf = self.custom_field(name)
+                cf['options'].add(value)
+                custom_fields[cf['name']].add(value)
+            else:
+                labels.add(label)
+        ticket.labels = list(labels)
+        ticket.custom_fields = {n: u', '.join(sorted(v)) for n,v in custom_fields.iteritems()}
+
+    def process_comments(self, ticket, comments):
+        for comment in comments:
+            p = ticket.thread.add_post(
+                    text = (
+                        u'Originally posted by: [{author.name}]({author.link})\n'
+                        '\n'
+                        '{body}\n'
+                        '\n'
+                        '{updates}').format(
+                            author=comment.author,
+                            body=comment.text,
+                            updates='\n'.join(
+                                '*%s*: %s' % (k,v)
+                                for k,v in comment.updates.items()
+                            ),
+                    )
+                )
+            p.add_multiple_attachments(comment.attachments)
+
+    def postprocess_custom_fields(self):
+        c.app.globals.custom_fields = []
+        for name, field in self.custom_fields.iteritems():
+            if field['name'] == '_milestone':
+                field['milestones'] = [{
+                        'name': milestone,
+                        'due_date': None,
+                        'complete': False,
+                    } for milestone in field['options']]
+                field['options'] = ''
+            elif field['type'] == 'select':
+                field['options'] = ' '.join(field['options'])
+            else:
+                field['options'] = ''
+            c.app.globals.custom_fields.append(field)
+
+
+class GDataAPIExtractor(object):
+    def __init__(self, project_name):
+        self.project_name = project_name
+
+    def iter_issues(self, limit=50):
+        """
+        Iterate over all issues for a project,
+        using paging to keep the responses reasonable.
+        """
+        start = 1
+
+        client = gdata.projecthosting.client.ProjectHostingClient()
+        while True:
+            query = gdata.projecthosting.client.Query(start_index=start, max_results=limit)
+            issues = client.get_issues(self.project_name, query=query).entry
+            if len(issues) <= 0:
+                return
+            for issue in issues:
+                yield GDataAPIIssue(issue)
+            start += limit
+
+    def iter_comments(self, issue, limit=50):
+        """
+        Iterate over all comments for a given issue,
+        using paging to keep the responses reasonable.
+        """
+        start = 1
+
+        client = gdata.projecthosting.client.ProjectHostingClient()
+        while True:
+            query = gdata.projecthosting.client.Query(start_index=start, max_results=limit)
+            issues = client.get_comments(self.project_name, query=query).entry
+            if len(issues) <= 0:
+                return
+            for comment in comments:
+                yield GDataAPIComment(comment)
+            start += limit
+
+
+class GDataAPIUser(object):
+    def __init__(self, user):
+        self.user = user
+
+    @property
+    def name(self):
+        return h.really_unicode(self.user.name.text)
+
+    @property
+    def link(self):
+        return u'http://code.google.com/u/%s' % self.name
+
+
+class GDataAPIIssue(object):
+    def __init__(self, issue):
+        self.issue = issue
+
+    @property
+    def summary(self):
+        return h.really_unicode(self.issue.title.text)
+
+    @property
+    def description(self):
+        return h.really_unicode(self.issue.content.text)
+
+    @property
+    def created_date(self):
+        return self.to_date(self.issue.published.text)
+
+    @property
+    def mod_date(self):
+        return self.to_date(self.issue.updated.text)
+
+    @property
+    def creator(self):
+        return h.really_unicode(self.issue.author[0].name.text)
+
+    @property
+    def status(self):
+        if getattr(self.issue, 'status', None) is not None:
+            return h.really_unicode(self.issue.status.text)
+        return u''
+
+    @property
+    def owner(self):
+        if getattr(self.issue, 'owner', None) is not None:
+            return h.really_unicode(self.issue.owner.username.text)
+        return u''
+
+    @property
+    def labels(self):
+        return [h.really_unicode(l.text) for l in self.issue.labels]
+
+
+class GDataAPIComment(object):
+    def __init__(self, comment):
+        self.comment = comment
+
+    @property
+    def author(self):
+        return GDataAPIUser(self.comment.author[0])
+
+    @property
+    def created_date(self):
+        return h.really_unicode(self.comment.published.text)
+
+    @property
+    def body(self):
+        return h.really_unicode(self.comment.content.text)
+
+    @property
+    def updates(self):
+        return {}
+
+    @property
+    def attachments(self):
+        return []
+
+
+class GDataAPIAttachment(object):
+    def __init__(self, attachment):
+        self.attachment = attachment
+
+    @property
+    def filename(self):
+        pass
+
+    @property
+    def type(self):
+        pass
+
+    @property
+    def file(self):
+        pass

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/ForgeImporters/forgeimporters/tests/google/test_extractor.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/tests/google/test_extractor.py b/ForgeImporters/forgeimporters/tests/google/test_extractor.py
index e346f1e..1a3a87c 100644
--- a/ForgeImporters/forgeimporters/tests/google/test_extractor.py
+++ b/ForgeImporters/forgeimporters/tests/google/test_extractor.py
@@ -36,16 +36,15 @@ class TestGoogleCodeProjectExtractor(TestCase):
         self._p_soup.stop()
 
     def test_init(self):
-        extractor = google.GoogleCodeProjectExtractor(self.project, 'project_info')
+        extractor = google.GoogleCodeProjectExtractor(self.project, 'my-project', 'project_info')
 
-        self.project.get_tool_data.assert_called_once_with('google-code', 'project_name')
         self.urlopen.assert_called_once_with('http://code.google.com/p/my-project/')
         self.assertEqual(extractor.project, self.project)
         self.soup.assert_called_once_with(self.urlopen.return_value)
         self.assertEqual(extractor.page, self.soup.return_value)
 
     def test_get_short_description(self):
-        extractor = google.GoogleCodeProjectExtractor(self.project, 'project_info')
+        extractor = google.GoogleCodeProjectExtractor(self.project, 'my-project', 'project_info')
         extractor.page.find.return_value.string = 'My Super Project'
 
         extractor.get_short_description()
@@ -57,7 +56,7 @@ class TestGoogleCodeProjectExtractor(TestCase):
     @mock.patch.object(google, 'M')
     def test_get_icon(self, M, StringIO):
         self.urlopen.return_value.info.return_value = {'content-type': 'image/png'}
-        extractor = google.GoogleCodeProjectExtractor(self.project, 'project_info')
+        extractor = google.GoogleCodeProjectExtractor(self.project, 'my-project', 'project_info')
         extractor.page.find.return_value.attrMap = {'src': 'http://example.com/foo/bar/my-logo.png'}
         self.urlopen.reset_mock()
 
@@ -75,7 +74,7 @@ class TestGoogleCodeProjectExtractor(TestCase):
     @mock.patch.object(google, 'M')
     def test_get_license(self, M):
         self.project.trove_license = []
-        extractor = google.GoogleCodeProjectExtractor(self.project, 'project_info')
+        extractor = google.GoogleCodeProjectExtractor(self.project, 'my-project', 'project_info')
         extractor.page.find.return_value.findNext.return_value.find.return_value.string = '  New BSD License  '
         trove = M.TroveCategory.query.get.return_value
 
@@ -94,7 +93,8 @@ class TestGoogleCodeProjectExtractor(TestCase):
 
     def _make_extractor(self, html):
         from BeautifulSoup import BeautifulSoup
-        extractor = google.GoogleCodeProjectExtractor(self.project)
+        with mock.patch.object(google, 'urllib2') as urllib2:
+            extractor = google.GoogleCodeProjectExtractor(self.project, 'my-project', 'project_info')
         extractor.page = BeautifulSoup(html)
         extractor.url="http://test/source/browse"
         return extractor

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/ForgeImporters/forgeimporters/tests/google/test_tasks.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/tests/google/test_tasks.py b/ForgeImporters/forgeimporters/tests/google/test_tasks.py
index bb9319d..23da83f 100644
--- a/ForgeImporters/forgeimporters/tests/google/test_tasks.py
+++ b/ForgeImporters/forgeimporters/tests/google/test_tasks.py
@@ -25,8 +25,8 @@ from ...google import tasks
 @mock.patch.object(tasks, 'c')
 def test_import_project_info(c, session, gpe):
     c.project = mock.Mock(name='project')
-    tasks.import_project_info()
-    gpe.assert_called_once_with(c.project, 'project_info')
+    tasks.import_project_info('my-project')
+    gpe.assert_called_once_with(c.project, 'my-project', 'project_info')
     gpe.return_value.get_short_description.assert_called_once_with()
     gpe.return_value.get_icon.assert_called_once_with()
     gpe.return_value.get_license.assert_called_once_with()
@@ -37,6 +37,6 @@ def test_import_project_info(c, session, gpe):
 @mock.patch.object(tasks, 'c')
 def test_import_tool(c, by_name):
     c.project = mock.Mock(name='project')
-    tasks.import_tool('importer_name', 'mount_point', 'mount_label')
+    tasks.import_tool('importer_name', 'project_name', 'mount_point', 'mount_label')
     by_name.assert_called_once_with('importer_name')
-    by_name.return_value.import_tool.assert_called_once_with(c.project, 'mount_point', 'mount_label')
+    by_name.return_value.import_tool.assert_called_once_with(c.project, 'project_name', 'mount_point', 'mount_label')

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/ForgeImporters/forgeimporters/tests/google/test_tracker.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/tests/google/test_tracker.py b/ForgeImporters/forgeimporters/tests/google/test_tracker.py
new file mode 100644
index 0000000..d54ac90
--- /dev/null
+++ b/ForgeImporters/forgeimporters/tests/google/test_tracker.py
@@ -0,0 +1,234 @@
+#       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 operator import itemgetter
+from unittest import TestCase
+import mock
+
+from ...google import tracker
+
+
+class TestTrackerImporter(TestCase):
+    @mock.patch.object(tracker, 'c')
+    @mock.patch.object(tracker, 'session')
+    @mock.patch.object(tracker, 'TM')
+    @mock.patch.object(tracker, 'GDataAPIExtractor')
+    def test_import_tool(self, gdata, TM, session, c):
+        importer = tracker.GoogleCodeTrackerImporter()
+        importer.process_fields = mock.Mock()
+        importer.process_labels = mock.Mock()
+        importer.process_comments = mock.Mock()
+        importer.postprocess_custom_fields = mock.Mock()
+        project = mock.Mock()
+        app = project.install_app.return_value
+        extractor = gdata.return_value
+        issues = extractor.iter_issues.return_value = [mock.Mock(), mock.Mock()]
+        tickets = TM.Ticket.new.side_effect = [mock.Mock(), mock.Mock()]
+        comments = extractor.iter_comments.side_effect = [mock.Mock(), mock.Mock()]
+
+        importer.import_tool(project, 'project_name', 'mount_point', 'mount_label')
+
+        project.install_app.assert_called_once_with('tracker', 'mount_point', 'mount_label')
+        gdata.assert_called_once_with('project_name')
+        self.assertEqual(importer.process_fields.call_args_list, [
+                mock.call(tickets[0], issues[0]),
+                mock.call(tickets[1], issues[1]),
+            ])
+        self.assertEqual(importer.process_labels.call_args_list, [
+                mock.call(tickets[0], issues[0]),
+                mock.call(tickets[1], issues[1]),
+            ])
+        self.assertEqual(importer.process_comments.call_args_list, [
+                mock.call(tickets[0], comments[0]),
+                mock.call(tickets[1], comments[1]),
+            ])
+        self.assertEqual(extractor.iter_comments.call_args_list, [
+                mock.call(issues[0]),
+                mock.call(issues[1]),
+            ])
+        self.assertEqual(session.call_args_list, [
+                mock.call(tickets[0]),
+                mock.call(tickets[0]),
+                mock.call(tickets[1]),
+                mock.call(tickets[1]),
+                mock.call(app),
+                mock.call(app.globals),
+            ])
+        self.assertEqual(session.return_value.flush.call_args_list, [
+                mock.call(tickets[0]),
+                mock.call(tickets[1]),
+                mock.call(app),
+                mock.call(app.globals),
+            ])
+        self.assertEqual(session.return_value.expunge.call_args_list, [
+                mock.call(tickets[0]),
+                mock.call(tickets[1]),
+            ])
+
+    def test_custom_fields(self):
+        importer = tracker.GoogleCodeTrackerImporter()
+        importer.custom_fields = {}
+        importer.custom_field('Foo')
+        importer.custom_field('Milestone')
+        importer.custom_field('Priority')
+        importer.custom_field('Type')
+        self.assertEqual(importer.custom_fields, {
+                'Foo': {
+                        'type': 'string',
+                        'label': 'Foo',
+                        'name': '_foo',
+                        'options': set(),
+                    },
+                'Milestone': {
+                        'type': 'milestone',
+                        'label': 'Milestone',
+                        'name': '_milestone',
+                        'options': set(),
+                    },
+                'Priority': {
+                        'type': 'select',
+                        'label': 'Priority',
+                        'name': '_priority',
+                        'options': set(),
+                    },
+                'Type': {
+                        'type': 'select',
+                        'label': 'Type',
+                        'name': '_type',
+                        'options': set(),
+                    },
+            })
+        importer.custom_fields = {'Foo': {}}
+        importer.custom_field('Foo')
+        self.assertEqual(importer.custom_fields, {'Foo': {}})
+
+    def test_process_fields(self):
+        ticket = mock.Mock()
+        issue = mock.Mock(
+                summary='summary',
+                description='description',
+                status='status',
+                created_date='created_date',
+                mod_date='mod_date',
+            )
+        importer = tracker.GoogleCodeTrackerImporter()
+        with mock.patch.object(tracker, 'datetime') as dt:
+            dt.strptime.side_effect = lambda s,f: s
+            importer.process_fields(ticket, issue)
+            self.assertEqual(ticket.summary, 'summary')
+            self.assertEqual(ticket.description, 'description')
+            self.assertEqual(ticket.status, 'status')
+            self.assertEqual(ticket.created_date, 'created_date')
+            self.assertEqual(ticket.mod_date, 'mod_date')
+            self.assertEqual(dt.strptime.call_args_list, [
+                    mock.call('created_date', ''),
+                    mock.call('mod_date', ''),
+                ])
+
+    def test_process_labels(self):
+        ticket = mock.Mock(custom_fields={}, labels=[])
+        issue = mock.Mock(labels=['Foo-Bar', 'Baz', 'Foo-Qux'])
+        importer = tracker.GoogleCodeTrackerImporter()
+        importer.custom_field = mock.Mock(side_effect=lambda n: {'name': '_%s' % n.lower(), 'options': set()})
+        importer.process_labels(ticket, issue)
+        self.assertEqual(ticket.labels, ['Baz'])
+        self.assertEqual(ticket.custom_fields, {'_foo': 'Bar, Qux'})
+
+    def test_process_comments(self):
+        def _author(n):
+            a = mock.Mock()
+            a.name = 'author%s' % n
+            a.link = 'author%s_link' % n
+            return a
+        ticket = mock.Mock()
+        comments = [
+                mock.Mock(
+                    author=_author(1),
+                    text='text1',
+                    attachments='attachments1',
+                ),
+                mock.Mock(
+                    author=_author(2),
+                    text='text2',
+                    attachments='attachments2',
+                ),
+            ]
+        comments[0].updates.items.return_value = [('Foo', 'Bar'), ('Baz', 'Qux')]
+        comments[1].updates.items.return_value = []
+        importer = tracker.GoogleCodeTrackerImporter()
+        importer.process_comments(ticket, comments)
+        self.assertEqual(ticket.thread.add_post.call_args_list[0], mock.call(
+                text='Originally posted by: [author1](author1_link)\n'
+                '\n'
+                'text1\n'
+                '\n'
+                '*Foo*: Bar\n'
+                '*Baz*: Qux'
+            ))
+        self.assertEqual(ticket.thread.add_post.call_args_list[1], mock.call(
+                text='Originally posted by: [author2](author2_link)\n'
+                '\n'
+                'text2\n'
+                '\n'
+            ))
+        self.assertEqual(ticket.thread.add_post.return_value.add_multiple_attachments.call_args_list, [
+                mock.call('attachments1'),
+                mock.call('attachments2'),
+            ])
+
+    @mock.patch.object(tracker, 'c')
+    def test_postprocess_custom_fields(self, c):
+        importer = tracker.GoogleCodeTrackerImporter()
+        importer.custom_fields = {
+                'Foo': {
+                    'name': '_foo',
+                    'type': 'string',
+                    'options': set(['foo', 'bar']),
+                },
+                'Milestone': {
+                    'name': '_milestone',
+                    'type': 'milestone',
+                    'options': set(['foo', 'bar']),
+                },
+                'Priority': {
+                    'name': '_priority',
+                    'type': 'select',
+                    'options': set(['foo', 'bar']),
+                },
+            }
+        importer.postprocess_custom_fields()
+        self.assertEqual(sorted(c.app.globals.custom_fields, key=itemgetter('name')), [
+                {
+                    'name': '_foo',
+                    'type': 'string',
+                    'options': '',
+                },
+                {
+                    'name': '_milestone',
+                    'type': 'milestone',
+                    'options': '',
+                    'milestones': [
+                        {'name': 'foo', 'due_date': None, 'complete': False},
+                        {'name': 'bar', 'due_date': None, 'complete': False},
+                    ],
+                },
+                {
+                    'name': '_priority',
+                    'type': 'select',
+                    'options': 'foo bar',
+                },
+            ])

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/ForgeImporters/setup.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/setup.py b/ForgeImporters/setup.py
index 8af3c1a..45a08eb 100644
--- a/ForgeImporters/setup.py
+++ b/ForgeImporters/setup.py
@@ -37,5 +37,6 @@ setup(name='ForgeImporters',
       google-code = forgeimporters.google.project:GoogleCodeProjectImporter
 
       [allura.importers]
+      google-code-tracker = forgeimporters.google.tracker:GoogleCodeTrackerImporter
       google-code-repo = forgeimporters.google.code:GoogleRepoImporter
       """,)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/c7fd7876/requirements-common.txt
----------------------------------------------------------------------
diff --git a/requirements-common.txt b/requirements-common.txt
index 5e261a0..e7b7ef1 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -50,6 +50,7 @@ TurboGears2==2.1.5
 WebOb==1.0.8
 # part of the stdlib, but with a version number.  see http://guide.python-distribute.org/pip.html#listing-installed-packages
 wsgiref==0.1.2
+gdata==2.0.18
 
 # tg2 deps (not used directly)
 Babel==0.9.6


[07/23] git commit: [#6441] ticket:398 refactored hyperlinks in tickets imported from trac

Posted by jo...@apache.org.
[#6441]  ticket:398 refactored hyperlinks in tickets imported from trac


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/70ee25a6
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/70ee25a6
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/70ee25a6

Branch: refs/heads/cj/6461
Commit: 70ee25a6fe029c6443d9aca632bcf9f1972a2fb0
Parents: b3fea69
Author: Yuriy Arhipov <yu...@yandex.ru>
Authored: Mon Jul 22 15:09:42 2013 +0400
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 12:21:14 2013 +0000

----------------------------------------------------------------------
 ForgeTracker/forgetracker/import_support.py         | 16 +++++++++++++---
 .../forgetracker/tests/functional/data/sf.json      |  4 ++--
 .../forgetracker/tests/functional/test_import.py    | 12 +++++++-----
 3 files changed, 22 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/70ee25a6/ForgeTracker/forgetracker/import_support.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/import_support.py b/ForgeTracker/forgetracker/import_support.py
index 268f727..8735f40 100644
--- a/ForgeTracker/forgetracker/import_support.py
+++ b/ForgeTracker/forgetracker/import_support.py
@@ -193,6 +193,7 @@ class ImportSupport(object):
                 new_f, conv = transform
                 remapped[new_f] = conv(v)
 
+        remapped['description'] = self.link_processing(remapped['description'])
         ticket_num = ticket_dict['id']
         existing_ticket = TM.Ticket.query.get(app_config_id=c.app.config._id,
                                           ticket_num=ticket_num)
@@ -215,6 +216,9 @@ class ImportSupport(object):
     def ticket_link(self, m):
         return '(%s)' % m.groups()[0]
 
+    def ticket_bracket_link(self, m):
+        return '[#%s]' % m.groups()[0]
+
     def get_slug_by_id(self, ticket, comment):
         comment = int(comment)
         ticket = TM.Ticket.query.get(app_config_id=c.app.config._id,
@@ -231,17 +235,23 @@ class ImportSupport(object):
 
     def comment_link(self, m):
         ticket, comment = m.groups()
-        return '(%s#%s)' % (ticket, self.get_slug_by_id(ticket, comment))
+        slug = self.get_slug_by_id(ticket, comment)
+        if slug:
+            return '(%s#%s)' % (ticket, self.get_slug_by_id(ticket, comment))
+        else:
+            return '\(%s#comment:%s\)' % (ticket, comment)
 
     def brackets_escaping(self, m):
-        return '[%s]' % m.groups()[0]
+        return '[\[%s\]]' % m.groups()[0]
 
     def link_processing(self, text):
+        short_link_ticket_pattern = re.compile('(?<!\[)#(\d+)(?!\])')
         comment_pattern = re.compile('\(\S*/(\d+)#comment:(\d+)\)')
         ticket_pattern = re.compile('(?<=\])\(\S*ticket/(\d+)\)')
         brackets_pattern = re.compile('\[\[(.*)\]\]')
 
-        text = comment_pattern.sub(self.comment_link, text.replace('\n', ''))
+        text = short_link_ticket_pattern.sub(self.ticket_bracket_link, text)
+        text = comment_pattern.sub(self.comment_link, text)
         text = ticket_pattern.sub(self.ticket_link, text)
         text = brackets_pattern.sub(self.brackets_escaping, text)
         return text

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/70ee25a6/ForgeTracker/forgetracker/tests/functional/data/sf.json
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tests/functional/data/sf.json b/ForgeTracker/forgetracker/tests/functional/data/sf.json
index 5a11379..82d7b89 100644
--- a/ForgeTracker/forgetracker/tests/functional/data/sf.json
+++ b/ForgeTracker/forgetracker/tests/functional/data/sf.json
@@ -44,14 +44,14 @@
             },
             {
               "class": "COMMENT",
-              "comment": "test link [[2496]](http://testlink.com)  test ticket ([#201](http://sourceforge.net/apps/trac/sourceforge/ticket/201)) \n [test comment](http://sourceforge.net/apps/trac/sourceforge/ticket/204#comment:1)",
+              "comment": "test link [[2496]](http://testlink.com)  test ticket ([#201](http://sourceforge.net/apps/trac/sourceforge/ticket/201)) \n [test comment](http://sourceforge.net/apps/trac/sourceforge/ticket/204#comment:1) \n [test comment](http://sourceforge.net/apps/trac/sourceforge/ticket/204#comment:45)",
               "date": "2009-07-21T15:44:32Z",
               "submitter": "ctsai"
             }
           ], 
           "date": "2009-04-13T08:49:13Z", 
           "date_updated": "2009-07-20T15:44:32Z", 
-          "description": "This problem occurs with IE 7, Windows Vista:\r\nOn the project's public info page (for example:\r\nhttps://sourceforge.net/project/admin/public_info.php?group_id=258655), the text boxes next to \"Descriptive Name\" and \"Project Description\" are not aligned properly; see the screenshot attached. ", 
+          "description": "This problem occurs with IE 7, Windows Vista:\r\nOn the project's public info page (for example:\r\nhttps://sourceforge.net/project/admin/public_info.php?group_id=258655), the text boxes next to \"Descriptive Name\" and \"Project Description\" are not aligned properly; see the screenshot attached. ",
           "id": 204, 
           "keywords": "ENGR",
           "milestone": "test_milestone",

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/70ee25a6/ForgeTracker/forgetracker/tests/functional/test_import.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tests/functional/test_import.py b/ForgeTracker/forgetracker/tests/functional/test_import.py
index 202c757..1fc7331 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_import.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_import.py
@@ -173,12 +173,14 @@ class TestImportController(TestRestApiBase):
         import_support = ImportSupport()
         result = import_support.link_processing('''test link [[2496]](http://testlink.com)
                                        test ticket ([#201](http://sourceforge.net/apps/trac/sourceforge/ticket/201))
-                                       [test comment](http://sourceforge.net/apps/trac/sourceforge/ticket/204#comment:1)''')
+                                       [test comment](http://sourceforge.net/apps/trac/sourceforge/ticket/204#comment:1)
+                                       #200''')
 
-        assert "test link [2496](http://testlink.com)" in result
-        assert '[test comment](204#)' in result
-        assert 'test link [2496](http://testlink.com)' in result
+        assert "test link [\[2496\]](http://testlink.com)" in result
+        assert '[test comment]\(204#comment:1\)' in result
+        assert 'test link [\[2496\]](http://testlink.com)' in result
         assert 'test ticket ([#201](201))' in result
+        assert '[#200]' in result
 
     @td.with_tracker
     def test_links(self):
@@ -200,7 +202,7 @@ class TestImportController(TestRestApiBase):
             status={'$in': ['ok', 'pending']})).sort('timestamp').all()[0].slug
 
         assert '[test comment](204#%s)' % slug in r
-        assert 'test link [2496](http://testlink.com)' in r
+        assert 'test link [\[2496\]](http://testlink.com)' in r
         assert 'test ticket ([#201](201))' in r
 
     @td.with_tracker


[21/23] git commit: [#6456] Add project_update event to GC project and code importer

Posted by jo...@apache.org.
[#6456] Add project_update event to GC project and code importer

Signed-off-by: Cory Johns <cj...@slashdotmedia.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/158cd46b
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/158cd46b
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/158cd46b

Branch: refs/heads/cj/6461
Commit: 158cd46bcd01c271baa11569361583c0408b56cd
Parents: 227396f
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Thu Jul 25 15:10:42 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Wed Jul 31 18:44:35 2013 +0000

----------------------------------------------------------------------
 ForgeImporters/forgeimporters/google/code.py     |  5 ++++-
 ForgeImporters/forgeimporters/google/tasks.py    |  2 ++
 ForgeImporters/forgeimporters/tests/test_base.py | 11 -----------
 3 files changed, 6 insertions(+), 12 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/158cd46b/ForgeImporters/forgeimporters/google/code.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/code.py b/ForgeImporters/forgeimporters/google/code.py
index 1ce51de..8e047fb 100644
--- a/ForgeImporters/forgeimporters/google/code.py
+++ b/ForgeImporters/forgeimporters/google/code.py
@@ -19,6 +19,7 @@ import formencode as fe
 from formencode import validators as fev
 
 from pylons import tmpl_context as c
+from pylons import app_globals as g
 from tg import (
         expose,
         redirect,
@@ -111,9 +112,11 @@ class GoogleRepoImporter(ToolImporter):
         repo_type = extractor.get_repo_type()
         repo_url = get_repo_url(project.get_tool_data('google-code',
             'project_name'), repo_type)
-        return project.install_app(
+        app = project.install_app(
                 REPO_ENTRY_POINTS[repo_type],
                 mount_point=mount_point or 'code',
                 mount_label=mount_label or 'Code',
                 init_from_url=repo_url,
                 )
+        g.post_event('project_updated')
+        return app

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/158cd46b/ForgeImporters/forgeimporters/google/tasks.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/tasks.py b/ForgeImporters/forgeimporters/google/tasks.py
index 834dc9d..65dd126 100644
--- a/ForgeImporters/forgeimporters/google/tasks.py
+++ b/ForgeImporters/forgeimporters/google/tasks.py
@@ -16,6 +16,7 @@
 #       under the License.
 
 from pylons import tmpl_context as c
+from pylons import app_globals as g
 
 from ming.orm import ThreadLocalORMSession
 
@@ -32,6 +33,7 @@ def import_project_info():
     extractor.get_icon()
     extractor.get_license()
     ThreadLocalORMSession.flush_all()
+    g.post_event('project_updated')
 
 @task
 def import_tool(importer_name, mount_point=None, mount_label=None):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/158cd46b/ForgeImporters/forgeimporters/tests/test_base.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/tests/test_base.py b/ForgeImporters/forgeimporters/tests/test_base.py
index 303570d..1558db4 100644
--- a/ForgeImporters/forgeimporters/tests/test_base.py
+++ b/ForgeImporters/forgeimporters/tests/test_base.py
@@ -35,17 +35,6 @@ def ep(name, source=None, importer=None, **kw):
     return mep
 
 
-class TestProjectImporterDispatcher(TestCase):
-    @mock.patch.object(base, 'iter_entry_points')
-    def test_lookup(self, iep):
-        eps = iep.return_value = [ep('ep1', 'first'), ep('ep2', 'second')]
-        nbhd = mock.Mock(name='neighborhood')
-        result = base.ProjectImporterDispatcher(nbhd)._lookup('source', 'rest1', 'rest2')
-        self.assertEqual(result, (eps[0].lv, ('rest1', 'rest2')))
-        iep.assert_called_once_with('allura.project_importers', 'source')
-        eps[0].load.return_value.assert_called_once_with(nbhd)
-
-
 class TestProjectImporter(TestCase):
     @mock.patch.object(base, 'iter_entry_points')
     def test_tool_importers(self, iep):


[12/23] git commit: [#6446] ticket:400 in forum api return only ok threads

Posted by jo...@apache.org.
[#6446] ticket:400 in forum api return only ok threads


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/58a2727b
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/58a2727b
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/58a2727b

Branch: refs/heads/cj/6461
Commit: 58a2727bd9cf4af9644ecbc2188d7f5b88c52871
Parents: dbb1cb1
Author: Anton Kasyanov <mi...@gmail.com>
Authored: Mon Jul 22 16:17:11 2013 +0300
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 14:55:48 2013 +0000

----------------------------------------------------------------------
 .../forgediscussion/controllers/root.py            |  2 +-
 .../forgediscussion/tests/functional/test_rest.py  | 17 +++++++++++++++++
 2 files changed, 18 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/58a2727b/ForgeDiscussion/forgediscussion/controllers/root.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/controllers/root.py b/ForgeDiscussion/forgediscussion/controllers/root.py
index 7dd18c7..f4354cc 100644
--- a/ForgeDiscussion/forgediscussion/controllers/root.py
+++ b/ForgeDiscussion/forgediscussion/controllers/root.py
@@ -305,7 +305,7 @@ class ForumRestController(BaseController):
                                         num_views=t.num_views,
                                         url=h.absurl('/rest' + t.url()),
                                         last_post=t.last_post)
-                                   for t in topics]
+                                   for t in topics if t.status == 'ok']
         json['count'] = count
         json['page'] = page
         json['limit'] = limit

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/58a2727b/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py b/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
index 7fc3499..3aca6e0 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
@@ -104,6 +104,23 @@ class TestRootRestController(TestDiscussionApiBase):
         url = 'http://localhost:80/rest/p/test/discussion/general/thread/%s/' % t._id
         assert_equal(topics[1]['url'], url)
 
+    def test_forum_show_ok_topics(self):
+        forum = self.api_get('/rest/p/test/discussion/general/')
+        forum = forum.json['forum']
+        assert_equal(forum['name'], 'General Discussion')
+        topics = forum['topics']
+        assert_equal(len(topics), 2)
+        self.create_topic('general', 'Hi again', 'It should not be shown')
+        t = ForumThread.query.find({'subject': 'Hi again'}).first()
+        first_post = t.first_post
+        first_post.status = u'pending'
+        first_post.commit()
+        forum = self.api_get('/rest/p/test/discussion/general/')
+        forum = forum.json['forum']
+        assert_equal(forum['name'], 'General Discussion')
+        topics = forum['topics']
+        assert_equal(len(topics), 2)
+
     def test_topic(self):
         forum = self.api_get('/rest/p/test/discussion/general/')
         forum = forum.json['forum']


[16/23] git commit: [#6446] ticket:400 removed threads from forum json

Posted by jo...@apache.org.
[#6446] ticket:400 removed threads from forum json


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/8b994ee2
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/8b994ee2
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/8b994ee2

Branch: refs/heads/cj/6461
Commit: 8b994ee25a10fe4f87cc898c506591c7b51fc71c
Parents: 6436aa5
Author: Anton Kasyanov <mi...@gmail.com>
Authored: Wed Jul 24 17:47:55 2013 +0300
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 14:55:49 2013 +0000

----------------------------------------------------------------------
 ForgeDiscussion/forgediscussion/controllers/root.py | 2 ++
 1 file changed, 2 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8b994ee2/ForgeDiscussion/forgediscussion/controllers/root.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/controllers/root.py b/ForgeDiscussion/forgediscussion/controllers/root.py
index f4354cc..fa0e97b 100644
--- a/ForgeDiscussion/forgediscussion/controllers/root.py
+++ b/ForgeDiscussion/forgediscussion/controllers/root.py
@@ -299,6 +299,8 @@ class ForumRestController(BaseController):
         count = topics.count()
         json = {}
         json['forum'] = self.forum.__json__()
+        # it appears that topics replace threads here
+        del json['forum']['threads']
         json['forum']['topics'] = [dict(_id=t._id,
                                         subject=t.subject,
                                         num_replies=t.num_replies,


[15/23] git commit: [#6446] ticket:400 removed trailing whitespaces

Posted by jo...@apache.org.
[#6446] ticket:400 removed trailing whitespaces


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/6436aa54
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/6436aa54
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/6436aa54

Branch: refs/heads/cj/6461
Commit: 6436aa54e610ca805edda2ef6e8287f8e5442e43
Parents: 0877d5c
Author: Anton Kasyanov <mi...@gmail.com>
Authored: Wed Jul 24 17:35:59 2013 +0300
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 14:55:49 2013 +0000

----------------------------------------------------------------------
 Allura/allura/model/discuss.py                                | 2 +-
 ForgeDiscussion/forgediscussion/tests/functional/test_rest.py | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/6436aa54/Allura/allura/model/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/discuss.py b/Allura/allura/model/discuss.py
index ee62778..d13b7e9 100644
--- a/Allura/allura/model/discuss.py
+++ b/Allura/allura/model/discuss.py
@@ -336,7 +336,7 @@ class Thread(Artifact, ActivityObject):
             terms = dict(discussion_id=self.discussion_id, thread_id=self._id,
                     status={'$in': ['ok', 'pending']})
         if status:
-            terms['status'] = status       
+            terms['status'] = status
         q = self.post_class().query.find(terms)
         if style == 'threaded':
             q = q.sort('full_slug')

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/6436aa54/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py b/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
index 6c45a2a..afdcdc5 100644
--- a/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
+++ b/ForgeDiscussion/forgediscussion/tests/functional/test_rest.py
@@ -187,7 +187,7 @@ class TestRootRestController(TestDiscussionApiBase):
         assert_equal(resp.json['limit'], 1)
 
     def test_topic_show_ok_only(self):
-        thread = ForumThread.query.find({'subject': 'Hi guys'}).first()        
+        thread = ForumThread.query.find({'subject': 'Hi guys'}).first()
         url = '/rest/p/test/discussion/general/thread/%s/' % thread._id
         resp = self.app.get(url)
         posts = resp.json['topic']['posts']
@@ -198,7 +198,7 @@ class TestRootRestController(TestDiscussionApiBase):
         last_post.commit()
         ThreadLocalORMSession.flush_all()
         resp = self.app.get(url)
-        posts = resp.json['topic']['posts']        
+        posts = resp.json['topic']['posts']
         assert_equal(len(posts), 1)
 
     def test_security(self):


[09/23] git commit: [#6460] Fixed security checks sometimes using incorrect roles

Posted by jo...@apache.org.
[#6460] Fixed security checks sometimes using incorrect roles

When doing a has_access() check for a given user against a given
artifact without explicitly specifying the project, c.project was being
used to get the list of user's roles instead of the artifact's project
attribute.  If c.project was not the project to which the artifact
belonged, the the wrong set of role_ids were being used, resulting in
access being denied.  It's a bit nonsensical to use an unrelated
project's role_ids to check access to an artifact, and this was breaking
notifications, which fire all pending notifications, regardless of the
context under which fire_ready() was called.

Signed-off-by: Cory Johns <cj...@slashdotmedia.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/4b2d8c17
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/4b2d8c17
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/4b2d8c17

Branch: refs/heads/cj/6461
Commit: 4b2d8c171aedf12c525d659470b0d807732afb9d
Parents: 53e35eb
Author: Cory Johns <cj...@slashdotmedia.com>
Authored: Mon Jul 29 20:40:35 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Wed Jul 31 13:50:42 2013 +0000

----------------------------------------------------------------------
 Allura/allura/lib/security.py                  |  4 +-
 Allura/allura/model/notification.py            |  6 +-
 Allura/allura/tests/model/test_notification.py | 86 +++++++++++++++++++++
 Allura/allura/tests/test_security.py           | 21 +++++
 4 files changed, 113 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4b2d8c17/Allura/allura/lib/security.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/security.py b/Allura/allura/lib/security.py
index 21ffb0a..a39e68d 100644
--- a/Allura/allura/lib/security.py
+++ b/Allura/allura/lib/security.py
@@ -287,7 +287,9 @@ def has_access(obj, permission, user=None, project=None):
                 elif isinstance(obj, M.Project):
                     project = obj.root_project
                 else:
-                    project = c.project.root_project
+                    project = getattr(obj, 'project', None)
+                    if project is None:
+                        project = c.project.root_project
             roles = cred.user_roles(user_id=user._id, project_id=project._id).reaching_ids
         chainable_roles = []
         for rid in roles:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4b2d8c17/Allura/allura/model/notification.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/notification.py b/Allura/allura/model/notification.py
index d4b0755..f1449fe 100644
--- a/Allura/allura/model/notification.py
+++ b/Allura/allura/model/notification.py
@@ -250,10 +250,10 @@ class Notification(MappedClass):
                 not security.has_access(artifact, 'read', user)():
             log.debug("Skipping notification - User %s doesn't have read "
                       "access to artifact %s" % (user_id, str(self.ref_id)))
-            log.debug("User roles [%s]; artifact ACL [%s]; project ACL [%s]",
-                    ', '.join([str(r) for r in security.Credentials.get().user_roles(user_id=user_id, project_id=c.project._id).reaching_ids]),
+            log.debug("User roles [%s]; artifact ACL [%s]; PSC ACL [%s]",
+                    ', '.join([str(r) for r in security.Credentials.get().user_roles(user_id=user_id, project_id=artifact.project._id).reaching_ids]),
                     ', '.join([str(a) for a in artifact.acl]),
-                    ', '.join([str(a) for a in c.project.acl]))
+                    ', '.join([str(a) for a in artifact.parent_security_context().acl]))
             return
         allura.tasks.mail_tasks.sendmail.post(
             destinations=[str(user_id)],

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4b2d8c17/Allura/allura/tests/model/test_notification.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/model/test_notification.py b/Allura/allura/tests/model/test_notification.py
index 4e9dd5b..1e1c273 100644
--- a/Allura/allura/tests/model/test_notification.py
+++ b/Allura/allura/tests/model/test_notification.py
@@ -68,6 +68,92 @@ class TestNotification(unittest.TestCase):
         assert len(subscriptions) == 0
         assert M.Mailbox.query.find().count() == 0
 
+    @mock.patch('allura.tasks.mail_tasks.sendmail')
+    def test_send_direct(self, sendmail):
+        c.user = M.User.query.get(username='test-user')
+        wiki = c.project.app_instance('wiki')
+        page = WM.Page.query.get(app_config_id=wiki.config._id)
+        notification = M.Notification(
+                _id='_id',
+                ref=page.ref,
+                from_address='from_address',
+                reply_to_address='reply_to_address',
+                in_reply_to='in_reply_to',
+                subject='subject',
+                text='text',
+            )
+        notification.footer = lambda: ' footer'
+        notification.send_direct(c.user._id)
+        sendmail.post.assert_called_once_with(
+                destinations=[str(c.user._id)],
+                fromaddr='from_address',
+                reply_to='reply_to_address',
+                subject='subject',
+                message_id='_id',
+                in_reply_to='in_reply_to',
+                text='text footer',
+            )
+
+    @mock.patch('allura.tasks.mail_tasks.sendmail')
+    def test_send_direct_no_access(self, sendmail):
+        c.user = M.User.query.get(username='test-user')
+        wiki = c.project.app_instance('wiki')
+        page = WM.Page.query.get(app_config_id=wiki.config._id)
+        page.parent_security_context().acl = []
+        ThreadLocalORMSession.flush_all()
+        ThreadLocalORMSession.close_all()
+        notification = M.Notification(
+                _id='_id',
+                ref=page.ref,
+                from_address='from_address',
+                reply_to_address='reply_to_address',
+                in_reply_to='in_reply_to',
+                subject='subject',
+                text='text',
+            )
+        notification.footer = lambda: ' footer'
+        notification.send_direct(c.user._id)
+        assert_equal(sendmail.post.call_count, 0)
+
+    @mock.patch('allura.tasks.mail_tasks.sendmail')
+    def test_send_direct_wrong_project_context(self, sendmail):
+        """
+        Test that Notification.send_direct() works as expected even
+        if c.project is wrong.
+
+        This can happen when a notify task is triggered on project A (thus
+        setting c.project to A) and then calls Mailbox.fire_ready() which fires
+        pending Notifications on any waiting Mailbox, regardless of project,
+        but doesn't update c.project.
+        """
+        project1 = c.project
+        project2 = M.Project.query.get(shortname='test2')
+        assert_equal(project1.shortname, 'test')
+        c.user = M.User.query.get(username='test-user')
+        wiki = project1.app_instance('wiki')
+        page = WM.Page.query.get(app_config_id=wiki.config._id)
+        notification = M.Notification(
+                _id='_id',
+                ref=page.ref,
+                from_address='from_address',
+                reply_to_address='reply_to_address',
+                in_reply_to='in_reply_to',
+                subject='subject',
+                text='text',
+            )
+        notification.footer = lambda: ' footer'
+        c.project = project2
+        notification.send_direct(c.user._id)
+        sendmail.post.assert_called_once_with(
+                destinations=[str(c.user._id)],
+                fromaddr='from_address',
+                reply_to='reply_to_address',
+                subject='subject',
+                message_id='_id',
+                in_reply_to='in_reply_to',
+                text='text footer',
+            )
+
 class TestPostNotifications(unittest.TestCase):
 
     def setUp(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/4b2d8c17/Allura/allura/tests/test_security.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_security.py b/Allura/allura/tests/test_security.py
index 40f9b8b..10ae614 100644
--- a/Allura/allura/tests/test_security.py
+++ b/Allura/allura/tests/test_security.py
@@ -136,3 +136,24 @@ class TestSecurity(TestController):
         assert has_access(wiki, 'post', test_user)()
         assert has_access(wiki, 'unmoderated_post', test_user)()
         assert_equal(all_allowed(wiki, test_user), set(['read', 'post', 'unmoderated_post']))
+
+    @td.with_wiki
+    def test_implicit_project(self):
+        '''
+        Test that relying on implicit project context does the Right Thing.
+
+        If you call has_access(artifact, perm), it should use the roles from
+        the project to which artifact belongs, even in c.project is something
+        else.  If you really want to use the roles from an unrelated project,
+        you should have to be very explicit about it, not just set c.project.
+        '''
+        project1 = c.project
+        project2 = M.Project.query.get(shortname='test2')
+        wiki = project1.app_instance('wiki')
+        page = WM.Page.query.get(app_config_id=wiki.config._id)
+        test_user = M.User.by_username('test-user')
+
+        assert_equal(project1.shortname, 'test')
+        assert has_access(page, 'read', test_user)()
+        c.project = project2
+        assert has_access(page, 'read', test_user)()