incubator-allura-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From john...@apache.org
Subject [26/50] git commit: [#6461] Partial implementation of GC Issues importer based on gdata API
Date Mon, 12 Aug 2013 16:41:11 GMT
[#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 <cjohns@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/9cc5c8ff
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/9cc5c8ff
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/9cc5c8ff

Branch: refs/heads/cj/6422
Commit: 9cc5c8ffb0824e46b4519c7a07fc9914d0568f3b
Parents: 93d12e6
Author: Cory Johns <cjohns@slashdotmedia.com>
Authored: Mon Jul 29 00:15:16 2013 +0000
Committer: Dave Brondsema <dbrondsema@slashdotmedia.com>
Committed: Tue Aug 6 21:49:17 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/9cc5c8ff/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/9cc5c8ff/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/9cc5c8ff/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/9cc5c8ff/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/9cc5c8ff/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/9cc5c8ff/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/9cc5c8ff/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/9cc5c8ff/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/9cc5c8ff/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/9cc5c8ff/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/9cc5c8ff/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


Mime
View raw message