Merge branch 'db/6640' of https://git-wip-us.apache.org/repos/asf/incubator-allura into db/6640
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index f600363..8e24cb2 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -947,3 +947,35 @@
         :rtype: ``None``
         """
         pass
+
+class ImportIdConverter(object):
+    '''
+    An interface to convert to and from import_id values for indexing,
+    searching, or displaying.
+
+    To provide a new converter, expose an entry point in setup.py:
+
+        [allura.import_id_converter]
+        mysource = foo.bar:SourceIdConverter
+
+    Then in your .ini file, set import_id_converter=mysource
+    '''
+
+    @classmethod
+    def get(cls):
+        converter = config.get('import_id_converter')
+        if converter:
+            return g.entry_points['allura.import_id_converter'][converter]()
+        return cls()
+
+    def simplify(self, import_id):
+        if hasattr(import_id, 'get'):
+            return import_id.get('source_id')
+        return None
+
+    def expand(self, source_id, app_instance):
+        import_id = {
+                'source_id': source_id,
+            }
+        import_id.update(app_instance.config.options.get('import_id', {}))
+        return import_id
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index 82baad6..3910cad 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -83,8 +83,11 @@
     references = FieldProperty(S.Deprecated)
     backreferences = FieldProperty(S.Deprecated)
     app_config = RelationProperty('AppConfig')
-    # Not null if artifact originated from external import, then API ticket id
-    import_id = FieldProperty(str, if_missing=None)
+    # Not null if artifact originated from external import.  The import ID is
+    # implementation specific, but should probably be an object indicating
+    # the source, original ID, and any other info needed to identify where
+    # the artifact came from.  But if you only have one source, a str might do.
+    import_id = FieldProperty(None, if_missing=None)
     deleted=FieldProperty(bool, if_missing=False)
 
     def __json__(self):
diff --git a/ForgeImporters/forgeimporters/base.py b/ForgeImporters/forgeimporters/base.py
index e0e9d6f..ffb1650 100644
--- a/ForgeImporters/forgeimporters/base.py
+++ b/ForgeImporters/forgeimporters/base.py
@@ -37,6 +37,7 @@
 from allura.lib import exceptions
 from allura.lib import validators as v
 from allura.app import SitemapEntry
+from allura import model as M
 
 from paste.deploy.converters import aslist
 
@@ -245,6 +246,7 @@
         self.after_project_create(c.project, **kw)
         for importer_name in kw['tools']:
             import_tool.post(importer_name, **kw)
+        M.AuditLog.log('import project from %s' % self.source)
 
         flash('Welcome to the %s Project System! '
               'Your project data will be imported and should show up here shortly.' % config['site_name'])
diff --git a/ForgeImporters/forgeimporters/google/code.py b/ForgeImporters/forgeimporters/google/code.py
index 4cf752d..134ecb6 100644
--- a/ForgeImporters/forgeimporters/google/code.py
+++ b/ForgeImporters/forgeimporters/google/code.py
@@ -36,6 +36,7 @@
 from allura.controllers import BaseController
 from allura.lib import validators as v
 from allura.lib.decorators import require_post, task
+from allura import model as M
 
 from forgeimporters.base import (
         ToolImporter,
@@ -168,6 +169,15 @@
                 mount_point=mount_point or 'code',
                 mount_label=mount_label or 'Code',
                 init_from_url=repo_url,
-                )
+                import_id={
+                        'source': self.source,
+                        'project_name': project_name,
+                    }
+            )
+        M.AuditLog.log(
+                'import tool %s from %s on %s' % (
+                    app.config.options.mount_point,
+                    project_name, self.source,
+                ), project=project, user=user, url=app.url)
         g.post_event('project_updated')
         return app
diff --git a/ForgeImporters/forgeimporters/google/tests/test_code.py b/ForgeImporters/forgeimporters/google/tests/test_code.py
index c6874ad..806a004 100644
--- a/ForgeImporters/forgeimporters/google/tests/test_code.py
+++ b/ForgeImporters/forgeimporters/google/tests/test_code.py
@@ -57,20 +57,31 @@
         return project
 
     @patch('forgeimporters.google.code.g')
+    @patch('forgeimporters.google.code.M')
     @patch('forgeimporters.google.code.GoogleCodeProjectExtractor')
     @patch('forgeimporters.google.code.get_repo_url')
-    def test_import_tool_happy_path(self, get_repo_url, gcpe, g):
+    def test_import_tool_happy_path(self, get_repo_url, gcpe, M, g):
         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, Mock(name='c.user'),
-                project_name='project_name')
+        u = Mock(name='c.user')
+        app = p.install_app.return_value
+        app.config.options.mount_point = 'code'
+        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',
                 mount_point='code',
                 mount_label='Code',
                 init_from_url='http://remote/clone/url/',
-                )
+                import_id={
+                        'source': 'Google Code',
+                        'project_name': 'project_name',
+                    },
+            )
+        M.AuditLog.log.assert_called_once_with(
+                'import tool code from project_name on Google Code',
+                project=p, user=u, url='foo')
         g.post_event.assert_called_once_with('project_updated')
 
 
diff --git a/ForgeImporters/forgeimporters/google/tracker.py b/ForgeImporters/forgeimporters/google/tracker.py
index 40dd939..9c259dd 100644
--- a/ForgeImporters/forgeimporters/google/tracker.py
+++ b/ForgeImporters/forgeimporters/google/tracker.py
@@ -24,9 +24,6 @@
 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,
@@ -40,11 +37,13 @@
 
 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, task
+from allura import model as M
 
 from forgetracker.tracker_main import ForgeTrackerApp
 from forgetracker import model as TM
-from . import GoogleCodeProjectExtractor
+from forgeimporters.google import GoogleCodeProjectExtractor
 from forgeimporters.base import (
         ToolImporter,
         ToolImportForm,
@@ -112,10 +111,15 @@
 
     def import_tool(self, project, user, project_name, mount_point=None,
             mount_label=None, **kw):
+        import_id_converter = ImportIdConverter.get()
         app = project.install_app('tickets', mount_point, mount_label,
                 EnableVoting=True,
                 open_status_names='New Accepted Started',
                 closed_status_names='Fixed Verified Invalid Duplicate WontFix Done',
+                import_id={
+                        'source': self.source,
+                        'project_name': project_name,
+                    },
             )
         ThreadLocalORMSession.flush_all()
         try:
@@ -126,7 +130,8 @@
                     ticket = TM.Ticket(
                         app_config_id=app.config._id,
                         custom_fields=dict(),
-                        ticket_num=ticket_num)
+                        ticket_num=ticket_num,
+                        import_id=import_id_converter.expand(ticket_num, app))
                     self.process_fields(ticket, issue)
                     self.process_labels(ticket, issue)
                     self.process_comments(ticket, issue)
@@ -135,6 +140,15 @@
                 app.globals.custom_fields = self.postprocess_custom_fields()
                 app.globals.last_ticket_num = self.max_ticket_num
                 ThreadLocalORMSession.flush_all()
+            M.AuditLog.log(
+                    'import tool %s from %s on %s' % (
+                            app.config.options.mount_point,
+                            project_name, self.source,
+                        ),
+                    project=project,
+                    user=user,
+                    url=app.url,
+                )
             g.post_event('project_updated')
             app.globals.invalidate_bin_counts()
             return app
diff --git a/ForgeImporters/forgeimporters/tests/google/functional/test_tracker.py b/ForgeImporters/forgeimporters/tests/google/functional/test_tracker.py
index 24081d9..9f48257 100644
--- a/ForgeImporters/forgeimporters/tests/google/functional/test_tracker.py
+++ b/ForgeImporters/forgeimporters/tests/google/functional/test_tracker.py
@@ -50,6 +50,7 @@
         self.assertIsNone(self.project.app_instance('test-issue'))
         with mock.patch.object(base.h, 'urlopen') as urlopen,\
              mock.patch.object(google.tracker, 'GoogleCodeProjectExtractor') as GPE,\
+             mock.patch.object(google.tracker.M, 'AuditLog') as AL,\
              mock.patch('forgetracker.tasks.update_bin_counts') as ubc:
             urlopen.side_effect = lambda req, **kw: mock.Mock(read=req.get_full_url, info=lambda:{'content-type': 'text/plain'})
             GPE.iter_issues.return_value = [(issue_id, issue)]
@@ -140,6 +141,19 @@
         self.assertEqual(ticket.votes_up, 1)
         self.assertEqual(ticket.votes, 1)
 
+    def test_import_id(self):
+        ticket = self._make_ticket(self.test_issue, issue_id=6)
+        self.assertEqual(ticket.app.config.options.import_id, {
+                'source': 'Google Code',
+                'project_name': 'test-issue-project',
+            })
+        self.assertEqual(ticket.ticket_num, 6)
+        self.assertEqual(ticket.import_id, {
+                'source': 'Google Code',
+                'project_name': 'test-issue-project',
+                'source_id': 6,
+            })
+
     @skipif(module_not_available('html2text'))
     def test_html2text_escaping(self):
         ticket = self._make_ticket(self.test_issue)
diff --git a/ForgeImporters/forgeimporters/tests/google/test_tracker.py b/ForgeImporters/forgeimporters/tests/google/test_tracker.py
index 5bbf836..d795bab 100644
--- a/ForgeImporters/forgeimporters/tests/google/test_tracker.py
+++ b/ForgeImporters/forgeimporters/tests/google/test_tracker.py
@@ -42,6 +42,13 @@
         importer.postprocess_custom_fields = mock.Mock()
         project, user = mock.Mock(), mock.Mock()
         app = project.install_app.return_value
+        app.config.options.mount_point = 'mount_point'
+        app.config.options.import_id = {
+                'source': 'Google Code',
+                'project_name': 'project_name',
+            }
+        app.config.options.get = lambda *a: getattr(app.config.options, *a)
+        app.url = 'foo'
         issues = gpe.iter_issues.return_value = [(50, mock.Mock()), (100, mock.Mock())]
         tickets = TM.Ticket.side_effect = [mock.Mock(), mock.Mock()]
 
@@ -52,6 +59,10 @@
                 EnableVoting=True,
                 open_status_names='New Accepted Started',
                 closed_status_names='Fixed Verified Invalid Duplicate WontFix Done',
+                import_id={
+                        'source': 'Google Code',
+                        'project_name': 'project_name',
+                    }
             )
         gpe.iter_issues.assert_called_once_with('project_name')
         self.assertEqual(importer.process_fields.call_args_list, [
@@ -79,6 +90,9 @@
                 mock.call(tickets[1]),
             ])
         self.assertEqual(app.globals.last_ticket_num, 100)
+        M.AuditLog.log.assert_called_once_with(
+                'import tool mount_point from project_name on Google Code',
+                project=project, user=user, url='foo')
         g.post_event.assert_called_once_with('project_updated')
         app.globals.invalidate_bin_counts.assert_called_once_with()
 
diff --git a/ForgeImporters/forgeimporters/tests/test_base.py b/ForgeImporters/forgeimporters/tests/test_base.py
index 7de0625..4aefc35 100644
--- a/ForgeImporters/forgeimporters/tests/test_base.py
+++ b/ForgeImporters/forgeimporters/tests/test_base.py
@@ -99,6 +99,30 @@
         self.assertEqual(pi.tool_importers, {'ep1': eps[0].lv, 'ep3': eps[2].lv})
         iep.assert_called_once_with('allura.importers')
 
+    @mock.patch.object(base, 'redirect')
+    @mock.patch.object(base, 'flash')
+    @mock.patch.object(base, 'import_tool')
+    @mock.patch.object(base, 'M')
+    @mock.patch.object(base, 'c')
+    def test_process(self, c, M, import_tool, flash, redirect):
+        pi = base.ProjectImporter(mock.Mock())
+        pi.source = 'Source'
+        pi.after_project_create = mock.Mock()
+        pi.neighborhood.register_project.return_value.script_name = 'script_name/'
+        kw = {
+                'project_name': 'project_name',
+                'project_shortname': 'shortname',
+                'tools': ['tool'],
+            }
+        with mock.patch.dict(base.config, {'site_name': 'foo'}):
+            pi.process(**kw)
+        pi.neighborhood.register_project.assert_called_once_with('shortname', project_name='project_name')
+        pi.after_project_create.assert_called_once_with(c.project, **kw)
+        import_tool.post.assert_called_once_with('tool', **kw)
+        M.AuditLog.log.assert_called_once_with('import project from Source')
+        self.assertEqual(flash.call_count, 1)
+        redirect.assert_called_once_with('script_name/admin/overview')
+
 
 
 TA1 = mock.Mock(tool_label='foo', tool_description='foo_desc')
diff --git a/ForgeImporters/forgeimporters/trac/tests/test_tickets.py b/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
index 74e1049..92e7854 100644
--- a/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
+++ b/ForgeImporters/forgeimporters/trac/tests/test_tickets.py
@@ -19,6 +19,8 @@
 from unittest import TestCase
 from mock import Mock, patch
 
+from tg import config
+
 from allura.tests import TestController
 from allura.tests.decorators import with_tracker
 
@@ -31,18 +33,22 @@
 class TestTracTicketImporter(TestCase):
     @patch('forgeimporters.trac.tickets.session')
     @patch('forgeimporters.trac.tickets.g')
+    @patch('forgeimporters.trac.tickets.AuditLog')
     @patch('forgeimporters.trac.tickets.import_tracker')
     @patch('forgeimporters.trac.tickets.AlluraImportApiClient')
     @patch('forgeimporters.trac.tickets.datetime')
     @patch('forgeimporters.trac.tickets.ApiTicket')
     @patch('forgeimporters.trac.tickets.TracExport')
-    def test_import_tool(self, TracExport, ApiTicket, dt, ApiClient, import_tracker, g, session):
+    def test_import_tool(self, TracExport, ApiTicket, dt, ApiClient, import_tracker, AuditLog, g, session):
         from datetime import datetime, timedelta
         now = datetime.utcnow()
         dt.utcnow.return_value = now
         user_map = {"orig_user":"new_user"}
         importer = TracTicketImporter()
         app = Mock(name='ForgeTrackerApp')
+        app.config.options.mount_point = 'bugs'
+        app.config.options.get = lambda *a: getattr(app.config.options, *a)
+        app.url = 'foo'
         project = Mock(name='Project', shortname='myproject')
         project.install_app.return_value = app
         user = Mock(name='User', _id='id')
@@ -55,7 +61,11 @@
                     )
         self.assertEqual(res, app)
         project.install_app.assert_called_once_with(
-                'Tickets', mount_point='bugs', mount_label='Bugs')
+                'Tickets', mount_point='bugs', mount_label='Bugs',
+                import_id={
+                        'source': 'Trac',
+                        'trac_url': 'http://example.com/trac/url/',
+                    })
         TracExport.return_value = []
         TracExport.assert_called_once_with('http://example.com/trac/url/')
         ApiTicket.assert_called_once_with(
@@ -67,6 +77,9 @@
                 api_client, 'myproject', 'bugs',
                 {"user_map": user_map}, '[]',
                 validate=False)
+        AuditLog.log.assert_called_once_with(
+                'import tool bugs from http://example.com/trac/url/',
+                project=project, user=user, url='foo')
         g.post_event.assert_called_once_with('project_updated')
 
     @patch('forgeimporters.trac.tickets.session')
diff --git a/ForgeImporters/forgeimporters/trac/tickets.py b/ForgeImporters/forgeimporters/trac/tickets.py
index 3063d71..9e4f493 100644
--- a/ForgeImporters/forgeimporters/trac/tickets.py
+++ b/ForgeImporters/forgeimporters/trac/tickets.py
@@ -43,7 +43,7 @@
 from allura.lib.import_api import AlluraImportApiClient
 from allura.lib import validators as v
 from allura.lib import helpers as h
-from allura.model import ApiTicket
+from allura.model import ApiTicket, AuditLog
 from allura.scripts.trac_export import (
         TracExport,
         DateJSONEncoder,
@@ -117,7 +117,11 @@
                 'Tickets',
                 mount_point=mount_point,
                 mount_label=mount_label or 'Tickets',
-                )
+                import_id={
+                        'source': self.source,
+                        'trac_url': trac_url,
+                    },
+            )
         session(app.config).flush(app.config)
         session(app.globals).flush(app.globals)
         try:
@@ -132,6 +136,13 @@
             import_tracker(cli, project.shortname, mount_point,
                     {'user_map': json.loads(user_map) if user_map else {}},
                     export_string, validate=False)
+            AuditLog.log(
+                'import tool %s from %s' % (
+                        app.config.options.mount_point, 
+                        trac_url, 
+                    ), 
+                project=project, user=user, url=app.url,
+            ) 
             g.post_event('project_updated')
             return app
         except Exception as e:
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index 9995257..505a991 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -47,9 +47,9 @@
 from allura.lib.search import search_artifact, SearchError
 from allura.lib import utils
 from allura.lib import helpers as h
+from allura.lib.plugin import ImportIdConverter
 from allura.tasks import mail_tasks
 
-from forgetracker.plugins import ImportIdConverter
 
 log = logging.getLogger(__name__)
 
diff --git a/ForgeTracker/forgetracker/plugins.py b/ForgeTracker/forgetracker/plugins.py
index a32dcfc..3afbca2 100644
--- a/ForgeTracker/forgetracker/plugins.py
+++ b/ForgeTracker/forgetracker/plugins.py
@@ -22,28 +22,3 @@
 
 log = logging.getLogger(__name__)
 
-
-class ImportIdConverter(object):
-    '''
-    An interface to provide authentication services for Allura.
-
-    To provide a new converter, expose an entry point in setup.py:
-
-        [allura.tickets.import_id_converter]
-        mylegacy = foo.bar:LegacyConverter
-
-    Then in your .ini file, set tickets.import_id_converter=mylegacy
-    '''
-
-    @classmethod
-    def get(cls):
-        converter = config.get('tickets.import_id_converter')
-        if converter:
-            return g.entry_points['allura.tickets.import_id_converter'][converter]()
-        return cls()
-
-    def simplify(self, import_id):
-        return import_id
-
-    def expand(self, url_part, app_instance):
-        return url_part
diff --git a/ForgeTracker/forgetracker/tests/functional/test_root.py b/ForgeTracker/forgetracker/tests/functional/test_root.py
index 0ba4f0b..86ef522 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_root.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_root.py
@@ -1573,7 +1573,7 @@
     def test_imported_tickets_redirect(self):
         self.new_ticket(summary='Imported ticket')
         ticket = tm.Ticket.query.get(ticket_num=1)
-        ticket.import_id = '42000'
+        ticket.import_id = {'source_id': '42000'}
         ThreadLocalORMSession.flush_all()
 
         # expect permanent redirect to /p/test/bugs/1/
diff --git a/ForgeTracker/forgetracker/tracker_main.py b/ForgeTracker/forgetracker/tracker_main.py
index 8aa29f6..61f7a99 100644
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -58,6 +58,7 @@
 from allura.lib.widgets import form_fields as ffw
 from allura.lib.widgets.subscriptions import SubscribeForm
 from allura.lib.zarkov_helpers import zero_fill_zarkov_result
+from allura.lib.plugin import ImportIdConverter
 from allura.controllers import AppDiscussionController, AppDiscussionRestController
 from allura.controllers import attachments as ac
 from allura.controllers import BaseController
@@ -75,7 +76,6 @@
 from forgetracker.widgets.ticket_search import TicketSearchResults, MassEdit, MassEditForm, MassMoveForm, SearchHelp
 from forgetracker.widgets.admin_custom_fields import TrackerFieldAdmin, TrackerFieldDisplay
 from forgetracker.import_support import ImportSupport
-from forgetracker.plugins import ImportIdConverter
 
 log = logging.getLogger(__name__)
 
@@ -1197,10 +1197,13 @@
                                                     ticket_num=self.ticket_num)
             if self.ticket is None:
                 self.ticket = TM.Ticket.query.get(
-                        app_config_id=c.app.config._id,
-                        import_id=str(ImportIdConverter.get().expand(ticket_num, c.app)))
+                        app_config_id = c.app.config._id,
+                        import_id = ImportIdConverter.get().expand(ticket_num, c.app),
+                    )
                 if self.ticket is not None:
                     utils.permanent_redirect(self.ticket.url())
+                else:
+                    raise exc.HTTPNotFound('Ticket #%s does not exist.' % ticket_num)
             self.attachment = AttachmentsController(self.ticket)
             # self.comments = CommentController(self.ticket)
 
diff --git a/requirements-common.txt b/requirements-common.txt
index 5e261a0..5e4d6cc 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -21,7 +21,7 @@
 iso8601==0.1.4
 Jinja2==2.6
 Markdown==2.2.0
-Ming==0.3.7
+Ming==0.3.9
 oauth2==1.5.170
 # tg2 dep PasteDeploy must specified before TurboGears2, to avoid a version/allow-hosts problem
 Paste==1.7.5.1
diff --git a/requirements-sf.txt b/requirements-sf.txt
index 286c95a..ecad4a2 100644
--- a/requirements-sf.txt
+++ b/requirements-sf.txt
@@ -6,7 +6,7 @@
 coverage==3.5a1-20110413
 ForgeHg==0.1.16
 ForgePastebin==0.2.7
-GoogleCodeWikiImporter==0.3.1
+GoogleCodeWikiImporter==0.3.3
 mechanize==0.2.4
 mercurial==1.4.3
 MySQL-python==1.2.3c1
@@ -20,7 +20,7 @@
 pyzmq==2.1.7
 html2text==3.200.3dev-20121112
 PyMollom==0.1
-TracWikiImporter==0.2.1
+TracWikiImporter==0.2.2
 
 # use version built from https://github.com/johnsca/GitPython/commits/tv/6000
 # for unmerged fixes for [#5411], [#6000], and [#6078]