You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by tv...@apache.org on 2014/01/31 06:19:45 UTC

git commit: [#7111] Reference importer targets by ep name

Updated Branches:
  refs/heads/tv/7111 [created] bb942bac5


[#7111] Reference importer targets by ep name

Allows subclassed apps to be used as import targets without requiring
a new importer and controller subclass be created.

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/bb942bac
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/bb942bac
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/bb942bac

Branch: refs/heads/tv/7111
Commit: bb942bac554fca0d01c00e122181f0b293125917
Parents: 50fb9ef
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Fri Jan 31 05:08:55 2014 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Fri Jan 31 05:08:55 2014 +0000

----------------------------------------------------------------------
 ForgeImporters/forgeimporters/base.py           | 73 ++++++++++++++++++--
 ForgeImporters/forgeimporters/forge/tracker.py  | 17 ++---
 ForgeImporters/forgeimporters/github/code.py    | 18 ++---
 ForgeImporters/forgeimporters/github/tracker.py | 23 +++---
 ForgeImporters/forgeimporters/github/wiki.py    | 25 ++-----
 ForgeImporters/forgeimporters/google/code.py    | 52 +++-----------
 .../forgeimporters/google/tests/test_code.py    |  2 +-
 ForgeImporters/forgeimporters/google/tracker.py | 17 ++---
 .../forgeimporters/tests/forge/test_tracker.py  |  4 +-
 .../forgeimporters/tests/google/test_tracker.py |  5 +-
 .../forgeimporters/tests/test_base.py           |  4 +-
 .../forgeimporters/trac/tests/test_tickets.py   |  4 +-
 ForgeImporters/forgeimporters/trac/tickets.py   | 17 ++---
 13 files changed, 116 insertions(+), 145 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/base.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/base.py b/ForgeImporters/forgeimporters/base.py
index 848cb38..d558258 100644
--- a/ForgeImporters/forgeimporters/base.py
+++ b/ForgeImporters/forgeimporters/base.py
@@ -340,6 +340,58 @@ class ProjectImporter(BaseController):
         pass
 
 
+class ToolImportControllerMeta(type):
+    def __call__(cls, importer, *args, **kw):
+        """ Decorate the `create` post handler with a validator that references
+        the appropriate App for this controller's importer.
+
+        """
+        if hasattr(cls, 'create') and getattr(cls.create.decoration, 'validation', None) is None:
+            cls.create = validate(cls.import_form(aslist(importer.target_app)[0]),
+                    error_handler=cls.index.__func__)(cls.create)
+        return type.__call__(cls, importer, *args, **kw)
+
+
+class ToolImportController(BaseController):
+    """ Base class for ToolImporter controllers.
+
+    """
+    __metaclass__ = ToolImportControllerMeta
+
+    def __init__(self, importer):
+        """
+        :param importer: :class:`ToolImporter` instance to which this
+            controller belongs.
+
+        """
+        self.importer = importer
+
+    @property
+    def target_app(self):
+        return aslist(self.importer.target_app)[0]
+
+
+class ToolImporterMeta(type):
+    def __init__(cls, name, bases, attrs):
+        if not (hasattr(cls, 'target_app_ep_names')
+                or hasattr(cls, 'target_app')):
+            raise AttributeError("{0} must define either "
+                    "`target_app` or `target_app_ep_names`".format(name))
+        return type.__init__(cls, name, bases, attrs)
+
+    def __call__(cls, *args, **kw):
+        """ Right before the first instance of cls is created, get
+        the list of target_app classes from ep names. Can't do this
+        at cls create/init time b/c g.entry_points is not guaranteed
+        to be loaded at that point.
+
+        """
+        if not getattr(cls, 'target_app', None):
+            cls.target_app = [g.entry_points['tool'][ep_name]
+                    for ep_name in aslist(cls.target_app_ep_names)]
+        return type.__call__(cls, *args, **kw)
+
+
 class ToolImporter(object):
 
     """
@@ -348,10 +400,18 @@ class ToolImporter(object):
     Subclasses are required to implement :meth:`import_tool()` described
     below and define the following attributes:
 
+    .. py:attribute:: target_app_ep_names
+
+       A string or list of strings which are entry point names of the
+       tool(s) to which this class imports. E.g.::
+
+            target_app_ep_names = ['git', 'hg']
+
     .. py:attribute:: target_app
 
        A reference or list of references to the tool(s) that this imports
-       to.  E.g.::
+       to.  This attribute is not required if `target_app_ep_names` is
+       defined (which is preferable). E.g.::
 
             target_app = [forgegit.ForgeGitApp, forgehg.ForgeHgApp]
 
@@ -366,7 +426,10 @@ class ToolImporter(object):
     .. py:attribute:: controller
 
        The controller for this importer, to handle single tool imports.
+
     """
+    __metaclass__ = ToolImporterMeta
+
     target_app = None  # app or list of apps
     source = None  # string description of source, must match project importer
     controller = None
@@ -519,7 +582,7 @@ class ProjectToolsImportController(object):
         hidden = set(aslist(config.get('hidden_importers'), sep=','))
         visible = lambda ep: ep.name not in hidden
         for ep in filter(visible, h.iter_entry_points('allura.importers')):
-            importer = ep.load()
+            importer = ep.load()()
             for tool in aslist(importer.target_app):
                 tools_with_importers.add(tool.tool_label)
                 importer_matrix[importer.source][tool.tool_label] = ep.name
@@ -530,9 +593,9 @@ class ProjectToolsImportController(object):
 
     @expose()
     def _lookup(self, name, *remainder):
-        import_tool = ToolImporter.by_name(name)
-        if import_tool:
-            return import_tool.controller(), remainder
+        importer = ToolImporter.by_name(name)
+        if importer:
+            return importer.controller(importer), remainder
         else:
             raise exc.HTTPNotFound
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/forge/tracker.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/forge/tracker.py b/ForgeImporters/forgeimporters/forge/tracker.py
index 5407743..96c352f 100644
--- a/ForgeImporters/forgeimporters/forge/tracker.py
+++ b/ForgeImporters/forgeimporters/forge/tracker.py
@@ -27,25 +27,23 @@ from tg import (
     expose,
     flash,
     redirect,
-    validate,
 )
 from tg.decorators import (
     with_trailing_slash,
     without_trailing_slash,
 )
 
-from allura.controllers import BaseController
 from allura.lib import helpers as h
 from allura.lib.plugin import ImportIdConverter
 from allura.lib.decorators import require_post
 from allura.lib import validators as v
 from allura import model as M
 
-from forgetracker.tracker_main import ForgeTrackerApp
 from forgetracker import model as TM
 from forgeimporters.base import (
     ToolImporter,
     ToolImportForm,
+    ToolImportController,
     File,
     get_importer_upload_path,
     save_importer_upload,
@@ -56,14 +54,8 @@ class ForgeTrackerImportForm(ToolImportForm):
     tickets_json = v.JsonFile(not_empty=True)
 
 
-class ForgeTrackerImportController(BaseController):
-
-    def __init__(self):
-        self.importer = ForgeTrackerImporter()
-
-    @property
-    def target_app(self):
-        return self.importer.target_app
+class ForgeTrackerImportController(ToolImportController):
+    import_form = ForgeTrackerImportForm
 
     @with_trailing_slash
     @expose('jinja:forgeimporters.forge:templates/tracker/index.html')
@@ -74,7 +66,6 @@ class ForgeTrackerImportController(BaseController):
     @without_trailing_slash
     @expose()
     @require_post()
-    @validate(ForgeTrackerImportForm(ForgeTrackerApp), error_handler=index)
     def create(self, tickets_json, mount_point, mount_label, **kw):
         if self.importer.enforce_limit(c.project):
             save_importer_upload(
@@ -94,7 +85,7 @@ class ForgeTrackerImportController(BaseController):
 
 class ForgeTrackerImporter(ToolImporter):
     source = 'Allura'
-    target_app = ForgeTrackerApp
+    target_app_ep_names = 'tickets'
     controller = ForgeTrackerImportController
     tool_label = 'Tickets'
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/github/code.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/github/code.py b/ForgeImporters/forgeimporters/github/code.py
index ede36b4..aa419cc 100644
--- a/ForgeImporters/forgeimporters/github/code.py
+++ b/ForgeImporters/forgeimporters/github/code.py
@@ -22,7 +22,6 @@ from tg import (
     expose,
     flash,
     redirect,
-    validate,
 )
 from tg.decorators import (
     with_trailing_slash,
@@ -30,14 +29,12 @@ from tg.decorators import (
 )
 
 from allura.lib.decorators import require_post
-from allura.controllers import BaseController
 from allura import model as M
 
-from forgegit.git_main import ForgeGitApp
-
 from forgeimporters.base import (
     ToolImporter,
     ToolImportForm,
+    ToolImportController,
 )
 from forgeimporters.github import GitHubProjectExtractor, GitHubOAuthMixin
 
@@ -47,14 +44,8 @@ class GitHubRepoImportForm(ToolImportForm):
     gh_user_name = fev.UnicodeString(not_empty=True)
 
 
-class GitHubRepoImportController(BaseController, GitHubOAuthMixin):
-
-    def __init__(self):
-        self.importer = GitHubRepoImporter()
-
-    @property
-    def target_app(self):
-        return self.importer.target_app
+class GitHubRepoImportController(ToolImportController, GitHubOAuthMixin):
+    import_form = GitHubRepoImportForm
 
     @with_trailing_slash
     @expose('jinja:forgeimporters.github:templates/code/index.html')
@@ -66,7 +57,6 @@ class GitHubRepoImportController(BaseController, GitHubOAuthMixin):
     @without_trailing_slash
     @expose()
     @require_post()
-    @validate(GitHubRepoImportForm(ForgeGitApp), error_handler=index)
     def create(self, gh_project_name, gh_user_name, mount_point, mount_label, **kw):
         if self.importer.enforce_limit(c.project):
             self.importer.post(
@@ -83,7 +73,7 @@ class GitHubRepoImportController(BaseController, GitHubOAuthMixin):
 
 
 class GitHubRepoImporter(ToolImporter):
-    target_app = ForgeGitApp
+    target_app_ep_names = 'git'
     source = 'GitHub'
     controller = GitHubRepoImportController
     tool_label = 'Source Code'

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/github/tracker.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/github/tracker.py b/ForgeImporters/forgeimporters/github/tracker.py
index 3af1517..cf266af 100644
--- a/ForgeImporters/forgeimporters/github/tracker.py
+++ b/ForgeImporters/forgeimporters/github/tracker.py
@@ -28,7 +28,6 @@ except ImportError:
 from formencode import validators as fev
 from tg import (
     expose,
-    validate,
     flash,
     redirect
 )
@@ -38,7 +37,6 @@ from tg.decorators import (
 )
 
 from allura import model as M
-from allura.controllers import BaseController
 from allura.lib import helpers as h
 from allura.lib.plugin import ImportIdConverter
 from allura.lib.decorators import require_post
@@ -47,10 +45,12 @@ from pylons import tmpl_context as c
 from pylons import app_globals as g
 
 from . import GitHubProjectExtractor, GitHubOAuthMixin
-from ..base import ToolImporter
-from forgetracker.tracker_main import ForgeTrackerApp
 from forgetracker import model as TM
-from forgeimporters.base import ToolImportForm
+from forgeimporters.base import (
+        ToolImporter,
+        ToolImportForm,
+        ToolImportController,
+        )
 from forgeimporters.github.utils import GitHubMarkdownConverter
 
 
@@ -62,14 +62,8 @@ class GitHubTrackerImportForm(ToolImportForm):
     gh_user_name = fev.UnicodeString(not_empty=True)
 
 
-class GitHubTrackerImportController(BaseController, GitHubOAuthMixin):
-
-    def __init__(self):
-        self.importer = GitHubTrackerImporter()
-
-    @property
-    def target_app(self):
-        return self.importer.target_app
+class GitHubTrackerImportController(ToolImportController, GitHubOAuthMixin):
+    import_form = GitHubTrackerImportForm
 
     @with_trailing_slash
     @expose('jinja:forgeimporters.github:templates/tracker/index.html')
@@ -81,7 +75,6 @@ class GitHubTrackerImportController(BaseController, GitHubOAuthMixin):
     @without_trailing_slash
     @expose()
     @require_post()
-    @validate(GitHubTrackerImportForm(ForgeTrackerApp), error_handler=index)
     def create(self, gh_project_name, gh_user_name, mount_point, mount_label, **kw):
         if self.importer.enforce_limit(c.project):
             self.importer.post(
@@ -99,7 +92,7 @@ class GitHubTrackerImportController(BaseController, GitHubOAuthMixin):
 
 class GitHubTrackerImporter(ToolImporter):
     source = 'GitHub'
-    target_app = ForgeTrackerApp
+    target_app_ep_names = 'tickets'
     controller = GitHubTrackerImportController
     tool_label = 'Issues'
     max_ticket_num = 0

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/github/wiki.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/github/wiki.py b/ForgeImporters/forgeimporters/github/wiki.py
index 0bd95da..bfcb1f7 100644
--- a/ForgeImporters/forgeimporters/github/wiki.py
+++ b/ForgeImporters/forgeimporters/github/wiki.py
@@ -31,7 +31,6 @@ from ming.orm import ThreadLocalORMSession
 from formencode import validators as fev
 from tg import (
     expose,
-    validate,
     flash,
     redirect,
 )
@@ -40,7 +39,6 @@ from tg.decorators import (
     without_trailing_slash,
 )
 
-from allura.controllers import BaseController
 from allura.lib import helpers as h
 from allura.lib import utils
 from allura.lib.plugin import ImportIdConverter
@@ -51,6 +49,7 @@ from allura import model as M
 from forgeimporters.base import (
     ToolImporter,
     ToolImportForm,
+    ToolImportController,
 )
 from forgeimporters.github import GitHubProjectExtractor, GitHubOAuthMixin
 from forgeimporters.github.utils import GitHubMarkdownConverter
@@ -61,14 +60,6 @@ from forgewiki.converters import mediawiki2markdown
 import logging
 log = logging.getLogger(__name__)
 
-TARGET_APPS = []
-
-try:
-    from forgewiki.wiki_main import ForgeWikiApp
-    TARGET_APPS.append(ForgeWikiApp)
-except ImportError:
-    pass
-
 
 class GitHubWikiImportForm(ToolImportForm):
     gh_project_name = fev.UnicodeString(not_empty=True)
@@ -76,14 +67,8 @@ class GitHubWikiImportForm(ToolImportForm):
     tool_option = fev.UnicodeString(if_missing=u'')
 
 
-class GitHubWikiImportController(BaseController, GitHubOAuthMixin):
-
-    def __init__(self):
-        self.importer = GitHubWikiImporter()
-
-    @property
-    def target_app(self):
-        return aslist(self.importer.target_app)[0]
+class GitHubWikiImportController(ToolImportController, GitHubOAuthMixin):
+    import_form = GitHubWikiImportForm
 
     @with_trailing_slash
     @expose('jinja:forgeimporters.github:templates/wiki/index.html')
@@ -95,7 +80,6 @@ class GitHubWikiImportController(BaseController, GitHubOAuthMixin):
     @without_trailing_slash
     @expose()
     @require_post()
-    @validate(GitHubWikiImportForm(ForgeWikiApp), error_handler=index)
     def create(self, gh_project_name, gh_user_name, mount_point, mount_label, **kw):
         if self.importer.enforce_limit(c.project):
             self.importer.post(
@@ -113,7 +97,8 @@ class GitHubWikiImportController(BaseController, GitHubOAuthMixin):
 
 
 class GitHubWikiImporter(ToolImporter):
-    target_app = TARGET_APPS
+    target_app_ep_names = 'wiki'
+
     controller = GitHubWikiImportController
     source = 'GitHub'
     tool_label = 'Wiki'

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/google/code.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/code.py b/ForgeImporters/forgeimporters/google/code.py
index 8181c87..13d388d 100644
--- a/ForgeImporters/forgeimporters/google/code.py
+++ b/ForgeImporters/forgeimporters/google/code.py
@@ -26,70 +26,43 @@ from tg import (
     expose,
     flash,
     redirect,
-    validate,
 )
 from tg.decorators import (
     with_trailing_slash,
     without_trailing_slash,
 )
 
-from allura.controllers import BaseController
 from allura.lib import validators as v
 from allura.lib.decorators import require_post
 from allura import model as M
 
 from forgeimporters.base import (
     ToolImporter,
+    ToolImportController,
 )
 from forgeimporters.google import GoogleCodeProjectExtractor
 from forgeimporters.google import GoogleCodeProjectNameValidator
 
-REPO_APPS = {}
-TARGET_APPS = []
-try:
-    from forgehg.hg_main import ForgeHgApp
-    TARGET_APPS.append(ForgeHgApp)
-    REPO_APPS['hg'] = ForgeHgApp
-except ImportError:
-    pass
-try:
-    from forgegit.git_main import ForgeGitApp
-    TARGET_APPS.append(ForgeGitApp)
-    REPO_APPS['git'] = ForgeGitApp
-except ImportError:
-    pass
-try:
-    from forgesvn.svn_main import ForgeSVNApp
-    TARGET_APPS.append(ForgeSVNApp)
-    REPO_APPS['svn'] = ForgeSVNApp
-except ImportError:
-    pass
 
 REPO_URLS = {
     'svn': 'http://{0}.googlecode.com/svn/',
     'git': 'https://code.google.com/p/{0}/',
     'hg': 'https://code.google.com/p/{0}/',
 }
-REPO_ENTRY_POINTS = {
-    'svn': 'SVN',
-    'git': 'Git',
-    'hg': 'Hg',
-}
 
 
 def get_repo_url(project_name, type_):
     return REPO_URLS[type_].format(project_name)
 
 
-def get_repo_class(type_):
-    return REPO_APPS[type_]
-
-
 class GoogleRepoImportForm(fe.schema.Schema):
     gc_project_name = GoogleCodeProjectNameValidator()
     mount_point = fev.UnicodeString()
     mount_label = fev.UnicodeString()
 
+    def __init__(self, *args):
+        pass
+
     def _to_python(self, value, state):
         value = super(self.__class__, self)._to_python(value, state)
 
@@ -107,7 +80,7 @@ class GoogleRepoImportForm(fe.schema.Schema):
             raise fe.Invalid(msg, value, state)
         except Exception:
             raise
-        tool_class = REPO_APPS[repo_type]
+        tool_class = g.entry_points['tool'][repo_type]
         try:
             value['mount_point'] = v.MountPointValidator(
                 tool_class).to_python(mount_point)
@@ -116,14 +89,8 @@ class GoogleRepoImportForm(fe.schema.Schema):
         return value
 
 
-class GoogleRepoImportController(BaseController):
-
-    def __init__(self):
-        self.importer = GoogleRepoImporter()
-
-    @property
-    def target_app(self):
-        return self.importer.target_app[0]
+class GoogleRepoImportController(ToolImportController):
+    import_form = GoogleRepoImportForm
 
     @with_trailing_slash
     @expose('jinja:forgeimporters.google:templates/code/index.html')
@@ -134,7 +101,6 @@ class GoogleRepoImportController(BaseController):
     @without_trailing_slash
     @expose()
     @require_post()
-    @validate(GoogleRepoImportForm(), error_handler=index)
     def create(self, gc_project_name, mount_point, mount_label, **kw):
         if self.importer.enforce_limit(c.project):
             self.importer.post(
@@ -150,7 +116,7 @@ class GoogleRepoImportController(BaseController):
 
 
 class GoogleRepoImporter(ToolImporter):
-    target_app = TARGET_APPS
+    target_app_ep_names = ('git', 'hg', 'svn')
     source = 'Google Code'
     controller = GoogleRepoImportController
     tool_label = 'Source Code'
@@ -165,7 +131,7 @@ class GoogleRepoImporter(ToolImporter):
         repo_type = extractor.get_repo_type()
         repo_url = get_repo_url(project_name, repo_type)
         app = project.install_app(
-            REPO_ENTRY_POINTS[repo_type],
+            repo_type,
             mount_point=mount_point or 'code',
             mount_label=mount_label or 'Code',
             init_from_url=repo_url,

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/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 00c5328..b52ec21 100644
--- a/ForgeImporters/forgeimporters/google/tests/test_code.py
+++ b/ForgeImporters/forgeimporters/google/tests/test_code.py
@@ -70,7 +70,7 @@ class TestGoogleRepoImporter(TestCase):
         app.url = 'foo'
         GoogleRepoImporter().import_tool(p, u, project_name='project_name')
         get_repo_url.assert_called_once_with('project_name', 'git')
-        p.install_app.assert_called_once_with('Git',
+        p.install_app.assert_called_once_with('git',
                                               mount_point='code',
                                               mount_label='Code',
                                               init_from_url='http://remote/clone/url/',

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/google/tracker.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/tracker.py b/ForgeImporters/forgeimporters/google/tracker.py
index b3abc10..7873aa6 100644
--- a/ForgeImporters/forgeimporters/google/tracker.py
+++ b/ForgeImporters/forgeimporters/google/tracker.py
@@ -26,26 +26,24 @@ from tg import (
     expose,
     flash,
     redirect,
-    validate,
 )
 from tg.decorators import (
     with_trailing_slash,
     without_trailing_slash,
 )
 
-from allura.controllers import BaseController
 from allura.lib import helpers as h
 from allura.lib.plugin import ImportIdConverter
 from allura.lib.decorators import require_post
 from allura import model as M
 
-from forgetracker.tracker_main import ForgeTrackerApp
 from forgetracker import model as TM
 from forgeimporters.google import GoogleCodeProjectExtractor
 from forgeimporters.google import GoogleCodeProjectNameValidator
 from forgeimporters.base import (
     ToolImporter,
     ToolImportForm,
+    ToolImportController,
 )
 
 
@@ -53,14 +51,8 @@ class GoogleCodeTrackerImportForm(ToolImportForm):
     gc_project_name = GoogleCodeProjectNameValidator()
 
 
-class GoogleCodeTrackerImportController(BaseController):
-
-    def __init__(self):
-        self.importer = GoogleCodeTrackerImporter()
-
-    @property
-    def target_app(self):
-        return self.importer.target_app
+class GoogleCodeTrackerImportController(ToolImportController):
+    import_form = GoogleCodeTrackerImportForm
 
     @with_trailing_slash
     @expose('jinja:forgeimporters.google:templates/tracker/index.html')
@@ -71,7 +63,6 @@ class GoogleCodeTrackerImportController(BaseController):
     @without_trailing_slash
     @expose()
     @require_post()
-    @validate(GoogleCodeTrackerImportForm(ForgeTrackerApp), error_handler=index)
     def create(self, gc_project_name, mount_point, mount_label, **kw):
         if self.importer.enforce_limit(c.project):
             self.importer.post(
@@ -89,7 +80,7 @@ class GoogleCodeTrackerImportController(BaseController):
 
 class GoogleCodeTrackerImporter(ToolImporter):
     source = 'Google Code'
-    target_app = ForgeTrackerApp
+    target_app_ep_names = 'tickets'
     controller = GoogleCodeTrackerImportController
     tool_label = 'Issues'
     tool_description = 'Import your public tickets from Google Code'

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/tests/forge/test_tracker.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/tests/forge/test_tracker.py b/ForgeImporters/forgeimporters/tests/forge/test_tracker.py
index 152e5bc..152f222 100644
--- a/ForgeImporters/forgeimporters/tests/forge/test_tracker.py
+++ b/ForgeImporters/forgeimporters/tests/forge/test_tracker.py
@@ -315,8 +315,8 @@ class TestForgeTrackerImportController(TestController, TestCase):
         """Mount Allura importer on the Tracker admin controller"""
         super(TestForgeTrackerImportController, self).setUp()
         from forgetracker.tracker_main import TrackerAdminController
-        TrackerAdminController._importer = tracker.ForgeTrackerImportController(
-        )
+        TrackerAdminController._importer = \
+                tracker.ForgeTrackerImportController(tracker.ForgeTrackerImporter())
 
     @with_tracker
     def test_index(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/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
index cfb8d97..e8f12bb 100644
--- a/ForgeImporters/forgeimporters/tests/google/test_tracker.py
+++ b/ForgeImporters/forgeimporters/tests/google/test_tracker.py
@@ -292,8 +292,9 @@ class TestGoogleCodeTrackerImportController(TestController, TestCase):
         """Mount Google Code importer on the Tracker admin controller"""
         super(TestGoogleCodeTrackerImportController, self).setUp()
         from forgetracker.tracker_main import TrackerAdminController
-        TrackerAdminController._importer = tracker.GoogleCodeTrackerImportController(
-        )
+        TrackerAdminController._importer = \
+                tracker.GoogleCodeTrackerImportController(
+                        tracker.GoogleCodeTrackerImporter())
 
     @with_tracker
     def test_index(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/tests/test_base.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/tests/test_base.py b/ForgeImporters/forgeimporters/tests/test_base.py
index e93a6b1..d52f417 100644
--- a/ForgeImporters/forgeimporters/tests/test_base.py
+++ b/ForgeImporters/forgeimporters/tests/test_base.py
@@ -132,6 +132,7 @@ class TestProjectImporter(TestCase):
     @mock.patch.object(base, 'M')
     @mock.patch.object(base, 'c')
     def test_process(self, c, M, import_tool, flash, redirect, by_name):
+        base.ToolImporter.target_app_ep_names = []
         by_name.return_value = base.ToolImporter()
 
         pi = base.ProjectImporter(mock.Mock())
@@ -183,8 +184,7 @@ TA2 = mock.Mock(tool_label='qux', tool_description='qux_desc')
 TA3 = mock.Mock(tool_label='baz', tool_description='baz_desc')
 
 
-class TI1Controller(object):
-
+class TI1Controller(base.ToolImportController):
     @expose()
     def index(self, *a, **kw):
         return 'test importer 1 controller webpage'

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/trac/tests/test_tickets.py b/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
index e9366f2..1ccfd98 100644
--- a/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
+++ b/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
@@ -111,8 +111,8 @@ class TestTracTicketImportController(TestController, TestCase):
         """Mount Trac import controller on the Tracker admin controller"""
         super(TestTracTicketImportController, self).setUp()
         from forgetracker.tracker_main import TrackerAdminController
-        self.importer = TrackerAdminController._importer = TracTicketImportController(
-        )
+        self.importer = TrackerAdminController._importer = \
+                TracTicketImportController(TracTicketImporter())
 
     @with_tracker
     def test_index(self):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/bb942bac/ForgeImporters/forgeimporters/trac/tickets.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/trac/tickets.py b/ForgeImporters/forgeimporters/trac/tickets.py
index c2c28c1..133a379 100644
--- a/ForgeImporters/forgeimporters/trac/tickets.py
+++ b/ForgeImporters/forgeimporters/trac/tickets.py
@@ -27,14 +27,12 @@ from tg import (
     expose,
     flash,
     redirect,
-    validate,
 )
 from tg.decorators import (
     with_trailing_slash,
     without_trailing_slash,
 )
 
-from allura.controllers import BaseController
 from allura.lib.decorators import require_post
 from allura.lib import validators as v
 from allura.lib import helpers as h
@@ -47,8 +45,8 @@ from allura.scripts.trac_export import (
 from forgeimporters.base import (
     ToolImporter,
     ToolImportForm,
+    ToolImportController,
 )
-from forgetracker.tracker_main import ForgeTrackerApp
 from forgetracker.import_support import ImportSupport
 from forgetracker import model as TM
 
@@ -58,14 +56,8 @@ class TracTicketImportForm(ToolImportForm):
     user_map = v.UserMapJsonFile(as_string=True)
 
 
-class TracTicketImportController(BaseController):
-
-    def __init__(self):
-        self.importer = TracTicketImporter()
-
-    @property
-    def target_app(self):
-        return self.importer.target_app
+class TracTicketImportController(ToolImportController):
+    import_form = TracTicketImportForm
 
     @with_trailing_slash
     @expose('jinja:forgeimporters.trac:templates/tickets/index.html')
@@ -76,7 +68,6 @@ class TracTicketImportController(BaseController):
     @without_trailing_slash
     @expose()
     @require_post()
-    @validate(TracTicketImportForm(ForgeTrackerApp), error_handler=index)
     def create(self, trac_url, mount_point, mount_label, user_map=None, **kw):
         if self.importer.enforce_limit(c.project):
             self.importer.post(
@@ -94,7 +85,7 @@ class TracTicketImportController(BaseController):
 
 
 class TracTicketImporter(ToolImporter):
-    target_app = ForgeTrackerApp
+    target_app_ep_names = 'tickets'
     source = 'Trac'
     controller = TracTicketImportController
     tool_label = 'Tickets'