← Back to team overview

gtg team mailing list archive

[Merge] lp:~arraintxo/gtg/mantis-backend into lp:gtg

 

Alayn Gortazar has proposed merging lp:~arraintxo/gtg/mantis-backend into lp:gtg.

Requested reviews:
  Gtg developers (gtg)

For more details, see:
https://code.launchpad.net/~arraintxo/gtg/mantis-backend/+merge/71788

Added a readonly backend for Mantis Bug Tracker, based on the previously existing Launchpad's backend
-- 
https://code.launchpad.net/~arraintxo/gtg/mantis-backend/+merge/71788
Your team Gtg developers is requested to review the proposed merge of lp:~arraintxo/gtg/mantis-backend into lp:gtg.
=== modified file 'AUTHORS'
--- AUTHORS	2011-08-12 14:11:35 +0000
+++ AUTHORS	2011-08-16 23:06:29 +0000
@@ -76,3 +76,4 @@
 * Ivan Evtukhovich <evtuhovich@xxxxxxxxx>
 * Madhumitha Viswanathan <madhuvishy@xxxxxxxxx>
 * Fabio Prina <fabio@xxxxxxxxx>
+* Alayn Gortazar <zutoin@xxxxxxxxx>

=== modified file 'CHANGELOG'
--- CHANGELOG	2011-08-12 14:11:35 +0000
+++ CHANGELOG	2011-08-16 23:06:29 +0000
@@ -1,4 +1,5 @@
 ????-??-?? Getting Things GNOME! ?.?.?
+    * Added Mantis Bug Tracker backend, by Alayn Gortazar
     * Fixed crash traceback when pressing 'delete' key, by Jeff Oliver
     * Added link to web documentation in Help menu, by Ronan Jouchet
     * Fixed bug with data consistency #579189, by Marko Kevac

=== added file 'GTG/backends/backend_mantis.py'
--- GTG/backends/backend_mantis.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_mantis.py	2011-08-16 23:06:29 +0000
@@ -0,0 +1,246 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+import os
+import uuid
+
+from GTG                                import _
+from GTG.backends.genericbackend        import GenericBackend
+from GTG.backends.backendsignals        import BackendSignals
+from GTG.backends.periodicimportbackend import PeriodicImportBackend
+from GTG.backends.syncengine     import SyncEngine, SyncMeme
+from GTG.tools.logger                   import Log
+
+from GTG.core.task               import Task
+from suds.client import Client
+
+'''
+Backend for importing mantis issues in GTG
+'''
+class Backend(PeriodicImportBackend):
+    _general_description = { \
+        GenericBackend.BACKEND_NAME:       "backend_mantis", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("MantisBT"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Luca Invernizzi", "Alayn Gortazar"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_READONLY, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("This backend lets you import the issues found on mantis" 
+              " using a prestablished filter called 'gtg'."
+              " As the issue state changes in Mantis, the GTG task is "
+              " updated.\n"
+              "Please note that this is a read only backend, which "
+              "means that if you open one of the imported tasks and "
+              " change one of the:\n"
+              "  - title\n"
+              "  - description\n"
+              "  - tags\n"
+              "Your changes <b>will</b> be reverted when the associated"
+              " issue is modified. Apart from those, you are free to set "
+              " any other field (start/due dates, subtasks...): your "
+              " changes will be preserved. This is useful to add "
+              " personal annotations to issue"), \
+        }
+ 
+    
+    _static_parameters = {\
+        "period": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
+            GenericBackend.PARAM_DEFAULT_VALUE: 5, }, \
+        "username": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
+            GenericBackend.PARAM_DEFAULT_VALUE: 'insert your username', }, \
+        "password": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_PASSWORD, \
+            GenericBackend.PARAM_DEFAULT_VALUE: '', }, \
+        "service-url": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
+            GenericBackend.PARAM_DEFAULT_VALUE: 'http://example.com/mantis', }, \
+        "tag-with-project-name": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
+            GenericBackend.PARAM_DEFAULT_VALUE: True}, \
+       }
+    
+    def __init__(self, parameters):
+        '''
+        See GenericBackend for an explanation of this function.
+        Re-loads the saved state of the synchronization
+        '''
+        super(Backend, self).__init__(parameters)
+        #loading the saved state of the synchronization, if any
+        self.data_path = os.path.join('backends/mantis/', \
+                                      "sync_engine-" + self.get_id())
+        self.sync_engine = self._load_pickled_file(self.data_path, \
+                                                   SyncEngine())    
+    def save_state(self):
+        '''Saves the state of the synchronization'''
+        self._store_pickled_file(self.data_path, self.sync_engine)
+
+    def do_periodic_import(self):
+        #Establishing connection 
+        try:
+            self.cancellation_point()
+            client = Client('%s/api/soap/mantisconnect.php?wsdl' % (self._parameters['service-url']))
+        except KeyError:
+            self.quit(disable = True)
+            BackendSignals().backend_failed(self.get_id(), \
+                            BackendSignals.ERRNO_AUTHENTICATION)
+            return
+
+
+        projects = client.service.mc_projects_get_user_accessible(self._parameters['username'], self._parameters['password'])
+        filters = client.service.mc_filter_get(self._parameters['username'], self._parameters['password'], 0)
+
+        #Fetching the issues
+        self.cancellation_point()
+        my_issues = []
+        for filt in filters:
+            if filt['name'] == 'gtg':
+                for project in projects:
+                    my_issues = client.service.mc_filter_get_issues(self._parameters['username'], self._parameters['password'], project['id'], filt['id'], 0, 100)
+                    for issue in my_issues:
+                        self.cancellation_point()
+                        self._process_mantis_issue(issue)
+        last_issue_list = self.sync_engine.get_all_remote()
+        new_issue_list = [str(issue['id']) for issue in my_issues]
+        for issue_link in set(last_issue_list).difference(set(new_issue_list)):
+            self.cancellation_point()
+            #we make sure that the other backends are not modifying the task
+            # set
+            with self.datastore.get_backend_mutex():
+                tid = self.sync_engine.get_local_id(issue_link)
+                self.datastore.request_task_deletion(tid)
+                try:
+                    self.sync_engine.break_relationship(remote_id = issue_link)
+                except KeyError:
+                    pass
+        return
+
+###############################################################################
+### Process tasks #############################################################
+###############################################################################
+
+    def _process_mantis_issue(self, issue):
+        '''
+        Given a issue object, finds out if it must be synced to a GTG note and, 
+        if so, it carries out the synchronization (by creating or
+        updating a GTG task, or deleting itself if the related task has
+        been deleted)
+
+        @param note: a mantis issue
+        '''
+        action, tid = self.sync_engine.analyze_remote_id(str(issue['id']), \
+                 self.datastore.has_task, lambda b: True)
+        Log.debug("processing mantis (%s)" % (action))
+
+        if action == None:
+            return
+
+        issue_dic = self._prefetch_issue_data(issue)
+        #for the rest of the function, no access to issue must be made, so
+        # that the time of blocking inside the with statements is short.
+        #To be sure of that, set issue to None
+        issue = None
+
+        with self.datastore.get_backend_mutex():
+            if action == SyncEngine.ADD:
+                tid = str(uuid.uuid4())
+                task = self.datastore.task_factory(tid)
+                self._populate_task(task, issue_dic)
+                self.sync_engine.record_relationship(local_id = tid,\
+                            remote_id = str(issue_dic['number']), \
+                            meme = SyncMeme(\
+                                        task.get_modified(), \
+                                        issue_dic['modified'], \
+                                        self.get_id()))
+                self.datastore.push_task(task)
+
+            elif action == SyncEngine.UPDATE:
+                task = self.datastore.get_task(tid)
+                self._populate_task(task, issue_dic)
+                meme = self.sync_engine.get_meme_from_remote_id( \
+                                                    issue_dic['number'])
+                meme.set_local_last_modified(task.get_modified())
+                meme.set_remote_last_modified(issue_dic['modified'])
+        self.save_state()
+
+    def _prefetch_issue_data(self, mantis_issue):
+        '''
+        We fetch all the necessary info that we need from the mantis_issue to populate a
+        task beforehand (these will be used in _populate_task).
+
+        @param mantis_issue: a mantis issue
+        @returns dict: a dictionary containing the relevant issue attributes
+        '''
+        issue_dic = {'title': mantis_issue['summary'],
+                   'text': mantis_issue['description'],
+                   'reporter': mantis_issue['reporter'].name,
+                   'modified': mantis_issue['last_updated'],
+                   'project': mantis_issue['project'].name,
+                   'status': mantis_issue['status'].name,
+                   'completed': (mantis_issue['status'].id >= 80),
+                   'number': str(mantis_issue['id'])}
+
+        try:
+            issue_dic['assigned'] = mantis_issue['handler'].name == self._parameters['username']
+        except AttributeError:
+            issue_dic['assigned'] = False
+            
+        return issue_dic
+
+
+    def _populate_task(self, task, issue_dic):
+        '''
+        Fills a GTG task with the data from a mantis issue.
+
+        @param task: a Task
+        @param issue_dic: a mantis issue
+        
+        '''
+        #set task status
+        if issue_dic["completed"]:
+            task.set_status(Task.STA_DONE)
+        else:
+            task.set_status(Task.STA_ACTIVE)
+        if task.get_title() != issue_dic['title']:
+            task.set_title(_("Iss.") + " %s: " % issue_dic["number"]
+                           + issue_dic['title'])
+        text = self._build_issue_text(issue_dic)
+        if task.get_excerpt() != text:
+            task.set_text(text)
+            
+        new_tags = set([])
+        if self._parameters["tag-with-project-name"]:
+            new_tags = set(['@' + issue_dic['project']])        
+        current_tags = set(task.get_tags_name())
+        #add the new ones
+        for tag in new_tags.difference(current_tags):
+            task.add_tag(tag)
+        
+        task.add_remote_id(self.get_id(), issue_dic['number'])
+
+    def _build_issue_text(self, issue_dic):
+        '''
+        Creates the text that describes a issue
+        '''
+        text = _("Reported by: ") + issue_dic["reporter"] + '\n' 
+        text += _("Link to issue: " ) + \
+                self._parameters['service-url'] + '/view.php?id=%s' % \
+                (issue_dic["number"]) + '\n'
+        text += '\n' + issue_dic["text"]
+        return text

=== modified file 'GTG/gtk/backends_dialog/parameters_ui/__init__.py'
--- GTG/gtk/backends_dialog/parameters_ui/__init__.py	2011-01-03 02:57:55 +0000
+++ GTG/gtk/backends_dialog/parameters_ui/__init__.py	2011-08-16 23:06:29 +0000
@@ -78,6 +78,10 @@
                ), \
                ("password"     , self.UI_generator(PasswordUI)), \
                ("period"       , self.UI_generator(PeriodUI)), \
+               ("service-url", self.UI_generator(TextUI, \
+                            {"description": _("Service URL"), \
+                             "parameter_name": "service-url"}) \
+               ),\
                ("import-from-replies", self.UI_generator(CheckBoxUI, \
                             {"text": _("Import tasks from @ replies " + \
                                                          "directed to you"), \
@@ -100,7 +104,7 @@
                                        "targeted by the bug"), \
                              "parameter": "tag-with-project-name"}) \
                ),\
-            ) 
+           ) 
     def UI_generator(self, param_type, special_arguments = {}):
         '''A helper function to build a widget type from a template.
         It passes to the created widget generator a series of common parameters,

=== added file 'data/icons/hicolor/scalable/apps/backend_mantis.png'
Binary files data/icons/hicolor/scalable/apps/backend_mantis.png	1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_mantis.png	2011-08-16 23:06:29 +0000 differ

Follow ups