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/09/05 20:20:30 UTC

[05/50] git commit: [#6526] Clean up individual tool importers

[#6526] Clean up individual tool importers

- Make forms look (slightly) better
- Add validation
- Kick off import as task

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

Branch: refs/heads/cj/6596
Commit: 8378ac032f3d92ea77d2756336641f45a9c71931
Parents: ec9c7cf
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Wed Aug 21 20:53:33 2013 +0000
Committer: Tim Van Steenburgh <tv...@gmail.com>
Committed: Tue Aug 27 17:24:58 2013 +0000

----------------------------------------------------------------------
 Allura/allura/lib/validators.py                 | 29 +++++++
 Allura/allura/model/project.py                  | 23 ++----
 ForgeImporters/forgeimporters/base.py           | 10 ++-
 .../forgeimporters/google/__init__.py           |  2 +-
 ForgeImporters/forgeimporters/google/code.py    | 62 +++++++++++++--
 .../google/templates/code/index.html            | 25 ++----
 .../google/templates/tracker/index.html         | 27 +++++++
 .../forgeimporters/google/tests/test_code.py    | 17 ++---
 ForgeImporters/forgeimporters/google/tracker.py | 62 ++++++++++++++-
 .../forgeimporters/templates/importer_base.html | 80 ++++++++++++++++++++
 .../forgeimporters/tests/google/test_tracker.py | 34 ++++++++-
 .../trac/templates/tickets/index.html           | 41 ++++------
 .../forgeimporters/trac/tests/test_tickets.py   | 20 ++---
 ForgeImporters/forgeimporters/trac/tickets.py   | 40 +++++++---
 14 files changed, 364 insertions(+), 108 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/Allura/allura/lib/validators.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/validators.py b/Allura/allura/lib/validators.py
index 9b44d53..fa82d32 100644
--- a/Allura/allura/lib/validators.py
+++ b/Allura/allura/lib/validators.py
@@ -72,6 +72,35 @@ class MaxBytesValidator(fev.FancyValidator):
     def from_python(self, value, state):
         return h.really_unicode(value or '')
 
+class MountPointValidator(fev.UnicodeString):
+    def __init__(self, app_class,
+            reserved_mount_points=('feed', 'index', 'icon', '_nav.json')):
+        super(self.__class__, self).__init__()
+        self.app_class = app_class
+        self.reserved_mount_points = reserved_mount_points
+
+    def _to_python(self, value, state):
+        from pylons import tmpl_context as c
+        project = state.project if hasattr(state, 'project') else c.project
+        mount_point, App = value, self.app_class
+        if not mount_point:
+            base_mount_point = mount_point = App.default_mount_point
+            for x in range(10):
+                if project.app_instance(mount_point) is None: break
+                mount_point = base_mount_point + '-%d' % x
+        if not App.relaxed_mount_points:
+            mount_point = mount_point.lower()
+        if not App.validate_mount_point(mount_point):
+            raise fe.Invalid('Mount point "%s" is invalid' % mount_point,
+                    value, state)
+        if mount_point in self.reserved_mount_points:
+            raise fe.Invalid('Mount point "%s" is reserved' % mount_point,
+                    value, state)
+        if project.app_instance(mount_point) is not None:
+            raise fe.Invalid('Mount point "%s" is already in use' % mount_point,
+                    value, state)
+        return mount_point
+
 class TaskValidator(fev.FancyValidator):
     def _to_python(self, value, state):
         try:

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index ebccb3d..983ef66 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -26,6 +26,7 @@ from tg import config
 from pylons import tmpl_context as c, app_globals as g
 from pylons import request
 from paste.deploy.converters import asbool
+import formencode as fe
 
 from ming import schema as S
 from ming.utils import LazyProperty
@@ -38,6 +39,7 @@ from allura.lib import helpers as h
 from allura.lib import plugin
 from allura.lib import exceptions
 from allura.lib import security
+from allura.lib import validators as v
 from allura.lib.security import has_access
 
 from .session import main_orm_session
@@ -598,23 +600,10 @@ class Project(MappedClass, ActivityNode, ActivityObject):
 
     def install_app(self, ep_name, mount_point=None, mount_label=None, ordinal=None, **override_options):
         App = g.entry_points['tool'][ep_name]
-        if not mount_point:
-            base_mount_point = mount_point = App.default_mount_point
-            for x in range(10):
-                if self.app_instance(mount_point) is None: break
-                mount_point = base_mount_point + '-%d' % x
-        if not App.relaxed_mount_points:
-            mount_point = mount_point.lower()
-        if not App.validate_mount_point(mount_point):
-            raise exceptions.ToolError, 'Mount point "%s" is invalid' % mount_point
-        # HACK: reserved url components
-        if mount_point in ('feed', 'index', 'icon', '_nav.json'):
-            raise exceptions.ToolError, (
-                'Mount point "%s" is reserved' % mount_point)
-        if self.app_instance(mount_point) is not None:
-            raise exceptions.ToolError, (
-                'Mount point "%s" is already in use' % mount_point)
-        assert self.app_instance(mount_point) is None
+        try:
+            mount_point = v.MountPointValidator(App).to_python(mount_point)
+        except fe.Invalid as e:
+            raise exceptions.ToolError(str(e))
         if ordinal is None:
             ordinal = int(self.ordered_mounts(include_hidden=True)[-1]['ordinal']) + 1
         options = App.default_options()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/ForgeImporters/forgeimporters/base.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/base.py b/ForgeImporters/forgeimporters/base.py
index e80235e..bdff43b 100644
--- a/ForgeImporters/forgeimporters/base.py
+++ b/ForgeImporters/forgeimporters/base.py
@@ -35,6 +35,7 @@ from allura.lib.security import require_access
 from allura.lib.plugin import ProjectRegistrationProvider, AdminExtension
 from allura.lib import helpers as h
 from allura.lib import exceptions
+from allura.lib import validators as v
 from allura.app import SitemapEntry
 
 from paste.deploy.converters import aslist
@@ -58,6 +59,13 @@ class ProjectImportForm(schema.Schema):
     project_name = fev.UnicodeString(not_empty=True, max=40)
 
 
+class ToolImportForm(schema.Schema):
+    def __init__(self, tool_class):
+        super(ToolImportForm, self).__init__()
+        self.add_field('mount_point', v.MountPointValidator(tool_class))
+    mount_label = fev.UnicodeString()
+
+
 @task(notifications_disabled=True)
 def import_tool(importer_name, project_name=None, mount_point=None, mount_label=None, **kw):
     importer = ToolImporter.by_name(importer_name)
@@ -388,4 +396,4 @@ class ImportAdminExtension(AdminExtension):
     def update_project_sidebar_menu(self, sidebar_links):
         base_url = c.project.url() + 'admin/ext/'
         link = SitemapEntry('Import', base_url+'import/')
-        sidebar_links.append(link)
\ No newline at end of file
+        sidebar_links.append(link)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/ForgeImporters/forgeimporters/google/__init__.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/__init__.py b/ForgeImporters/forgeimporters/google/__init__.py
index 849f924..ad5f8e5 100644
--- a/ForgeImporters/forgeimporters/google/__init__.py
+++ b/ForgeImporters/forgeimporters/google/__init__.py
@@ -1,4 +1,4 @@
-#       Licensed to the Apache Software Foundation (ASF) under one
+#       LNicensed 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

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/ForgeImporters/forgeimporters/google/code.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/code.py b/ForgeImporters/forgeimporters/google/code.py
index 11dd7a6..60a0158 100644
--- a/ForgeImporters/forgeimporters/google/code.py
+++ b/ForgeImporters/forgeimporters/google/code.py
@@ -15,6 +15,8 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
+import urllib2
+
 import formencode as fe
 from formencode import validators as fev
 
@@ -22,6 +24,7 @@ from pylons import tmpl_context as c
 from pylons import app_globals as g
 from tg import (
         expose,
+        flash,
         redirect,
         validate,
         )
@@ -31,25 +34,30 @@ from tg.decorators import (
         )
 
 from allura.controllers import BaseController
-from allura.lib.decorators import require_post
+from allura.lib import validators as v
+from allura.lib.decorators import require_post, task
 
 from forgeimporters.base import ToolImporter
 from forgeimporters.google import GoogleCodeProjectExtractor
 
+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
 
@@ -69,28 +77,70 @@ def get_repo_url(project_name, type_):
     return REPO_URLS[type_].format(project_name)
 
 
-class GoogleRepoImportSchema(fe.Schema):
+def get_repo_class(type_):
+    return REPO_APPS[type_]
+
+
+@task(notifications_disabled=True)
+def import_tool(**kw):
+    GoogleRepoImporter().import_tool(c.project, c.user, **kw)
+
+
+class GoogleRepoImportForm(fe.schema.Schema):
     gc_project_name = fev.UnicodeString(not_empty=True)
     mount_point = fev.UnicodeString()
     mount_label = fev.UnicodeString()
 
+    def _to_python(self, value, state):
+        value = super(self.__class__, self)._to_python(value, state)
+
+        gc_project_name = value['gc_project_name']
+        mount_point = value['mount_point']
+        try:
+            repo_type = GoogleCodeProjectExtractor(gc_project_name).get_repo_type()
+        except urllib2.HTTPError as e:
+            if e.code == 404:
+                msg = 'No such project'
+            else:
+                msg = str(e)
+            msg = 'gc_project_name:' + msg
+            raise fe.Invalid(msg, value, state)
+        except Exception:
+            raise
+        tool_class = REPO_APPS[repo_type]
+        try:
+            v.MountPointValidator(tool_class).to_python(mount_point)
+        except fe.Invalid as e:
+            raise fe.Invalid('mount_point:' + str(e), value, state)
+        return value
+
 
 class GoogleRepoImportController(BaseController):
+    def __init__(self):
+        self.importer = GoogleRepoImporter()
+
+    @property
+    def target_app(self):
+        return self.importer.target_app[0]
+
     @with_trailing_slash
     @expose('jinja:forgeimporters.google:templates/code/index.html')
     def index(self, **kw):
-        return {}
+        return dict(importer=self.importer,
+                target_app=self.target_app)
 
     @without_trailing_slash
     @expose()
     @require_post()
-    @validate(GoogleRepoImportSchema(), error_handler=index)
+    @validate(GoogleRepoImportForm(), error_handler=index)
     def create(self, gc_project_name, mount_point, mount_label, **kw):
-        app = GoogleRepoImporter().import_tool(c.project, c.user,
+        import_tool.post(
                 project_name=gc_project_name,
                 mount_point=mount_point,
                 mount_label=mount_label)
-        redirect(app.url())
+        flash('Repo import has begun. Your new repo will be available '
+                'when the import is complete.')
+        redirect(c.project.url() + 'admin/')
 
 
 class GoogleRepoImporter(ToolImporter):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/ForgeImporters/forgeimporters/google/templates/code/index.html
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/templates/code/index.html b/ForgeImporters/forgeimporters/google/templates/code/index.html
index 7bace3a..e2cef0c 100644
--- a/ForgeImporters/forgeimporters/google/templates/code/index.html
+++ b/ForgeImporters/forgeimporters/google/templates/code/index.html
@@ -16,27 +16,16 @@
        specific language governing permissions and limitations
        under the License.
 -#}
-{% extends g.theme.master %}
+{% extends 'forgeimporters:templates/importer_base.html' %}
 
 {% block title %}
-{{c.project.name}} / Import Google Code Repo
+{{c.project.name}} / Import your repo from Google Code
 {% endblock %}
 
-{% block header %}
-Import a Repo from a Google Code project
-{% endblock %}
-
-{% block content %}
-<form action="create" method="post" class="pad">
+{% block importer_fields %}
+<div>
   <label for="gc_project_name">Google Code project name (as it appears in a URL)</label>
-  <input name="gc_project_name" />
-
-  <label for="mount_label">Label</label>
-  <input name="mount_label" value="Source" />
-
-  <label for="mount_point">Mount Point</label>
-  <input name="mount_point" value="source" />
-
-  <input type="submit" />
-</form>
+  <input name="gc_project_name" value="{{ c.form_values['gc_project_name'] }}" />
+  {{ error('gc_project_name') }}
+</div>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/ForgeImporters/forgeimporters/google/templates/tracker/index.html
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/templates/tracker/index.html b/ForgeImporters/forgeimporters/google/templates/tracker/index.html
new file mode 100644
index 0000000..08d0084
--- /dev/null
+++ b/ForgeImporters/forgeimporters/google/templates/tracker/index.html
@@ -0,0 +1,27 @@
+{#-
+       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.
+-#}
+{% extends 'forgeimporters:templates/importer_base.html' %}
+
+{% block importer_fields %}
+<div>
+  <label for="gc_project_name">Google Code project name (as it appears in a URL)</label>
+  <input name="gc_project_name" value="{{ c.form_values['gc_project_name'] }}" />
+  {{ error('gc_project_name') }}
+</div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/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 950246a..c6874ad 100644
--- a/ForgeImporters/forgeimporters/google/tests/test_code.py
+++ b/ForgeImporters/forgeimporters/google/tests/test_code.py
@@ -89,12 +89,8 @@ class TestGoogleRepoImportController(TestController, TestCase):
         self.assertIsNotNone(r.html.find(attrs=dict(name="mount_point")))
 
     @with_svn
-    @patch('forgeimporters.google.code.GoogleRepoImporter')
-    def test_create(self, gri):
-        from allura import model as M
-        gri = gri.return_value
-        gri.import_tool.return_value = Mock()
-        gri.import_tool.return_value.url.return_value = '/p/{}/mymount'.format(test_project_with_repo)
+    @patch('forgeimporters.google.code.import_tool')
+    def test_create(self, import_tool):
         params = dict(gc_project_name='poop',
                 mount_label='mylabel',
                 mount_point='mymount',
@@ -102,8 +98,7 @@ class TestGoogleRepoImportController(TestController, TestCase):
         r = self.app.post('/p/{}/admin/src/_importer/create'.format(test_project_with_repo),
                 params,
                 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._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'])
+        self.assertEqual(r.location, 'http://localhost/p/{}/admin/'.format(test_project_with_repo))
+        self.assertEqual(u'mymount', import_tool.post.call_args[1]['mount_point'])
+        self.assertEqual(u'mylabel', import_tool.post.call_args[1]['mount_label'])
+        self.assertEqual(u'poop', import_tool.post.call_args[1]['project_name'])

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/ForgeImporters/forgeimporters/google/tracker.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/google/tracker.py b/ForgeImporters/forgeimporters/google/tracker.py
index 8b1747f..b7fccbe 100644
--- a/ForgeImporters/forgeimporters/google/tracker.py
+++ b/ForgeImporters/forgeimporters/google/tracker.py
@@ -18,23 +18,81 @@
 from collections import defaultdict
 from datetime import datetime
 
+from formencode import validators as fev
+
 from pylons import tmpl_context as c
 from pylons import app_globals as g
 from ming.orm import session, ThreadLocalORMSession
 
 from allura import model as M
+#import gdata
+gdata = None
+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.decorators import require_post, task
 
 from forgetracker.tracker_main import ForgeTrackerApp
 from forgetracker import model as TM
-from ..base import ToolImporter
 from . import GoogleCodeProjectExtractor
+from forgeimporters.base import (
+        ToolImporter,
+        ToolImportForm,
+        )
+
+
+@task(notifications_disabled=True)
+def import_tool(**kw):
+    GoogleCodeTrackerImporter().import_tool(c.project, c.user, **kw)
+
+
+class GoogleCodeTrackerImportForm(ToolImportForm):
+    gc_project_name = fev.UnicodeString(not_empty=True)
+
+
+class GoogleCodeTrackerImportController(BaseController):
+    def __init__(self):
+        self.importer = GoogleCodeTrackerImporter()
+
+    @property
+    def target_app(self):
+        return self.importer.target_app
+
+    @with_trailing_slash
+    @expose('jinja:forgeimporters.google:templates/tracker/index.html')
+    def index(self, **kw):
+        return dict(importer=self.importer,
+                target_app=self.target_app)
+
+    @without_trailing_slash
+    @expose()
+    @require_post()
+    @validate(GoogleCodeTrackerImportForm(ForgeTrackerApp), error_handler=index)
+    def create(self, gc_project_name, mount_point, mount_label, **kw):
+        import_tool.post(
+                project_name=gc_project_name,
+                mount_point=mount_point,
+                mount_label=mount_label,
+                )
+        flash('Ticket import has begun. Your new tracker will be available '
+                'when the import is complete.')
+        redirect(c.project.url() + 'admin/')
 
 
 class GoogleCodeTrackerImporter(ToolImporter):
     source = 'Google Code'
     target_app = ForgeTrackerApp
-    controller = None
+    controller = GoogleCodeTrackerImportController
     tool_label = 'Issues'
 
     field_types = defaultdict(lambda: 'string',

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/ForgeImporters/forgeimporters/templates/importer_base.html
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/templates/importer_base.html b/ForgeImporters/forgeimporters/templates/importer_base.html
new file mode 100644
index 0000000..4bdfe0f
--- /dev/null
+++ b/ForgeImporters/forgeimporters/templates/importer_base.html
@@ -0,0 +1,80 @@
+{#-
+       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.
+-#}
+{% extends g.theme.master %}
+
+{% block title %}
+{{c.project.name}} / {{ importer.tool_description }}
+{% endblock %}
+
+{% block header %}
+{{ importer.tool_description }}
+{% endblock %}
+
+{% block extra_css %}
+  <style type="text/css">
+    form {
+      padding: 0 20px 20px 20px;
+    }
+    form label {
+      display: inline-block;
+      width: 30%;
+      vertical-align: top;
+    }
+    form > div {
+      margin-bottom: 10px;
+    }
+    form > div input {
+      width: 30%;
+      vertical-align: top;
+    }
+    form .error {
+      display: inline-block;
+      color: #f00;
+      background: none;
+      border: none;
+      margin: 0;
+      width: 30%;
+    }
+  </style>
+{% endblock %}
+
+{%- macro error(field_name) %}
+  {% if c.form_errors[field_name] %}
+  <div class="error">{{c.form_errors[field_name]}}</div>
+  {% endif %}
+{%- endmacro %}
+
+{% block content %}
+<form action="create" method="post" enctype="multipart/form-data">
+  {% block importer_fields %}
+  {% endblock %}
+  <div>
+    <label for="mount_label">Label</label>
+    <input name="mount_label" value="{{ c.form_values['mount_label'] or target_app.default_mount_label }}" />
+      {{ error('mount_label') }}
+  </div>
+  <div>
+    <label for="mount_point">Mount Point</label>
+    <input name="mount_point" value="{{ c.form_values['mount_point'] or target_app.default_mount_point }}" />
+      {{ error('mount_point') }}
+  </div>
+
+  <input type="submit" />
+</form>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/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 4a7c28f..3c7f0aa 100644
--- a/ForgeImporters/forgeimporters/tests/google/test_tracker.py
+++ b/ForgeImporters/forgeimporters/tests/google/test_tracker.py
@@ -16,9 +16,12 @@
 #       under the License.
 
 from datetime import datetime
-from operator import itemgetter
 from unittest import TestCase
 import mock
+from mock import patch
+
+from allura.tests import TestController
+from allura.tests.decorators import with_tracker
 
 from ...google import tracker
 
@@ -239,3 +242,32 @@ class TestTrackerImporter(TestCase):
                     'options': 'foo bar',
                 },
             ])
+
+
+class TestGoogleCodeTrackerImportController(TestController, TestCase):
+    def setUp(self):
+        """Mount Google Code importer on the Tracker admin controller"""
+        super(TestGoogleCodeTrackerImportController, self).setUp()
+        from forgetracker.tracker_main import TrackerAdminController
+        TrackerAdminController._importer = tracker.GoogleCodeTrackerImportController()
+
+    @with_tracker
+    def test_index(self):
+        r = self.app.get('/p/test/admin/bugs/_importer/')
+        self.assertIsNotNone(r.html.find(attrs=dict(name="gc_project_name")))
+        self.assertIsNotNone(r.html.find(attrs=dict(name="mount_label")))
+        self.assertIsNotNone(r.html.find(attrs=dict(name="mount_point")))
+
+    @with_tracker
+    @patch('forgeimporters.google.tracker.import_tool')
+    def test_create(self, import_tool):
+        params = dict(gc_project_name='test',
+                mount_label='mylabel',
+                mount_point='mymount',
+                )
+        r = self.app.post('/p/test/admin/bugs/_importer/create', params,
+                status=302)
+        self.assertEqual(r.location, 'http://localhost/p/test/admin/')
+        self.assertEqual(u'mymount', import_tool.post.call_args[1]['mount_point'])
+        self.assertEqual(u'mylabel', import_tool.post.call_args[1]['mount_label'])
+        self.assertEqual(u'test', import_tool.post.call_args[1]['project_name'])

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/ForgeImporters/forgeimporters/trac/templates/tickets/index.html
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/trac/templates/tickets/index.html b/ForgeImporters/forgeimporters/trac/templates/tickets/index.html
index 7c278b0..42b8a9f 100644
--- a/ForgeImporters/forgeimporters/trac/templates/tickets/index.html
+++ b/ForgeImporters/forgeimporters/trac/templates/tickets/index.html
@@ -16,30 +16,19 @@
        specific language governing permissions and limitations
        under the License.
 -#}
-{% extends g.theme.master %}
-
-{% block title %}
-{{c.project.name}} / Import Trac Tickets
-{% endblock %}
-
-{% block header %}
-Import tickets from Trac
-{% endblock %}
-
-{% block content %}
-<form action="create" method="post" enctype="multipart/form-data" class="pad">
-  <label for="trac_url">URL of the Trac instance</label>
-  <input name="trac_url" />
-
-  <label for="user_map">JSON file mapping Trac usernames to Allura usernames (optional)</label>
-  <input name="user_map" type="file" />
-
-  <label for="mount_label">Label</label>
-  <input name="mount_label" value="Source" />
-
-  <label for="mount_point">Mount Point</label>
-  <input name="mount_point" value="source" />
-
-  <input type="submit" />
-</form>
+{% extends 'forgeimporters:templates/importer_base.html' %}
+
+{% block importer_fields %}
+  <div>
+    <label for="trac_url">URL of the Trac instance</label>
+    <input name="trac_url" value="{{ c.form_values['trac_url'] }}" />
+    {{ error('trac_url') }}
+  </div>
+  <div>
+    <label for="user_map">User Map (optional)
+      <br><small>JSON file mapping Trac usernames to Allura usernames</small>
+    </label>
+    <input name="user_map" type="file" />
+    {{ error('user_map') }}
+  </div>
 {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/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 2a539df..953c6d7 100644
--- a/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
+++ b/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
@@ -84,12 +84,8 @@ class TestTracTicketImportController(TestController, TestCase):
         self.assertIsNotNone(r.html.find(attrs=dict(name="mount_point")))
 
     @with_tracker
-    @patch('forgeimporters.trac.tickets.TracTicketImporter')
-    def test_create(self, importer):
-        from allura import model as M
-        importer = importer.return_value
-        importer.import_tool.return_value = Mock()
-        importer.import_tool.return_value.url.return_value = '/p/test/mymount'
+    @patch('forgeimporters.trac.tickets.import_tool')
+    def test_create(self, import_tool):
         params = dict(trac_url='http://example.com/trac/url',
                 mount_label='mylabel',
                 mount_point='mymount',
@@ -97,10 +93,8 @@ class TestTracTicketImportController(TestController, TestCase):
         r = self.app.post('/p/test/admin/bugs/_importer/create', params,
                 upload_files=[('user_map', 'myfile', '{"orig_user": "new_user"}')],
                 status=302)
-        project = M.Project.query.get(shortname='test')
-        self.assertEqual(r.location, 'http://localhost/p/test/mymount')
-        self.assertEqual(project._id, importer.import_tool.call_args[0][0]._id)
-        self.assertEqual(u'mymount', importer.import_tool.call_args[1]['mount_point'])
-        self.assertEqual(u'mylabel', importer.import_tool.call_args[1]['mount_label'])
-        self.assertEqual('{"orig_user": "new_user"}', importer.import_tool.call_args[1]['user_map'])
-        self.assertEqual(u'http://example.com/trac/url', importer.import_tool.call_args[1]['trac_url'])
+        self.assertEqual(r.location, 'http://localhost/p/test/admin/')
+        self.assertEqual(u'mymount', import_tool.post.call_args[1]['mount_point'])
+        self.assertEqual(u'mylabel', import_tool.post.call_args[1]['mount_label'])
+        self.assertEqual('{"orig_user": "new_user"}', import_tool.post.call_args[1]['user_map'])
+        self.assertEqual(u'http://example.com/trac/url', import_tool.post.call_args[1]['trac_url'])

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/8378ac03/ForgeImporters/forgeimporters/trac/tickets.py
----------------------------------------------------------------------
diff --git a/ForgeImporters/forgeimporters/trac/tickets.py b/ForgeImporters/forgeimporters/trac/tickets.py
index dfbb2a2..3e0e486 100644
--- a/ForgeImporters/forgeimporters/trac/tickets.py
+++ b/ForgeImporters/forgeimporters/trac/tickets.py
@@ -21,7 +21,6 @@ from datetime import (
         )
 import json
 
-import formencode as fe
 from formencode import validators as fev
 
 from ming.orm import session
@@ -30,6 +29,7 @@ from pylons import app_globals as g
 from tg import (
         config,
         expose,
+        flash,
         redirect,
         validate,
         )
@@ -39,44 +39,60 @@ from tg.decorators import (
         )
 
 from allura.controllers import BaseController
-from allura.lib.decorators import require_post
+from allura.lib.decorators import require_post, task
 from allura.lib.import_api import AlluraImportApiClient
-from allura.lib.validators import UserMapJsonFile
+from allura.lib import validators as v
 from allura.model import ApiTicket
 from allura.scripts.trac_export import (
         TracExport,
         DateJSONEncoder,
         )
 
-from forgeimporters.base import ToolImporter
+from forgeimporters.base import (
+        ToolImporter,
+        ToolImportForm,
+        )
 from forgetracker.tracker_main import ForgeTrackerApp
 from forgetracker.scripts.import_tracker import import_tracker
 
 
-class TracTicketImportSchema(fe.Schema):
+@task(notifications_disabled=True)
+def import_tool(**kw):
+    TracTicketImporter().import_tool(c.project, c.user, **kw)
+
+
+class TracTicketImportForm(ToolImportForm):
     trac_url = fev.URL(not_empty=True)
-    user_map = UserMapJsonFile(as_string=True)
-    mount_point = fev.UnicodeString()
-    mount_label = fev.UnicodeString()
+    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
+
     @with_trailing_slash
     @expose('jinja:forgeimporters.trac:templates/tickets/index.html')
     def index(self, **kw):
-        return {}
+        return dict(importer=self.importer,
+                target_app=self.target_app)
 
     @without_trailing_slash
     @expose()
     @require_post()
-    @validate(TracTicketImportSchema(), error_handler=index)
+    @validate(TracTicketImportForm(ForgeTrackerApp), error_handler=index)
     def create(self, trac_url, mount_point, mount_label, user_map=None, **kw):
-        app = TracTicketImporter().import_tool(c.project, c.user,
+        import_tool.post(
                 mount_point=mount_point,
                 mount_label=mount_label,
                 trac_url=trac_url,
                 user_map=user_map)
-        redirect(app.url())
+        flash('Ticket import has begun. Your new tracker will be available '
+                'when the import is complete.')
+        redirect(c.project.url() + 'admin/')
 
 
 class TracTicketImporter(ToolImporter):