diff --git a/scripts/boobtracker b/scripts/boobtracker
new file mode 100755
index 0000000000000000000000000000000000000000..335f33c7ae86ee0b679a00fcab01b7df466828f2
--- /dev/null
+++ b/scripts/boobtracker
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2011 Romain Bignon
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from weboob.applications.boobtracker import BoobTracker
+
+
+if __name__ == '__main__':
+ BoobTracker.run()
diff --git a/weboob/applications/boobtracker/__init__.py b/weboob/applications/boobtracker/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4b70f730a7ddc0aa9a14e81adac8fef283a932d
--- /dev/null
+++ b/weboob/applications/boobtracker/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2011 Romain Bignon
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from .boobtracker import BoobTracker
+
+__all__ = ['BoobTracker']
diff --git a/weboob/applications/boobtracker/boobtracker.py b/weboob/applications/boobtracker/boobtracker.py
new file mode 100644
index 0000000000000000000000000000000000000000..a9ed15faf92c47923e25c58c379e43b6e6e49405
--- /dev/null
+++ b/weboob/applications/boobtracker/boobtracker.py
@@ -0,0 +1,348 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2011 Romain Bignon
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+import sys
+
+from weboob.capabilities.bugtracker import ICapBugTracker, Query, Update
+from weboob.tools.application.repl import ReplApplication
+from weboob.tools.application.formatters.iformatter import IFormatter
+from weboob.tools.misc import html2text
+
+
+__all__ = ['BoobTracker']
+
+
+class IssueFormatter(IFormatter):
+ MANDATORY_FIELDS = ('id', 'project', 'title', 'body', 'author')
+
+ def flush(self):
+ pass
+
+ def format_dict(self, item):
+ result = u'%s%s - #%s - %s%s\n' % (self.BOLD, item['project'].name, item['id'], item['title'], self.NC)
+ result += '\n%s\n\n' % item['body']
+ result += 'Author: %s (%s)\n' % (item['author'].name, item['creation'])
+ if item['status']:
+ result += 'Status: %s\n' % item['status'].name
+ if item['version']:
+ result += 'Version: %s\n' % item['version'].name
+ if item['category']:
+ result += 'Category: %s\n' % item['category']
+ if item['assignee']:
+ result += 'Assignee: %s\n' % (item['assignee'].name)
+ if item['attachments']:
+ result += '\nAttachments:\n'
+ for a in item['attachments']:
+ result += '* %s%s%s <%s>\n' % (self.BOLD, a.filename, self.NC, a.url)
+ if item['history']:
+ result += '\nHistory:\n'
+ for u in item['history']:
+ result += '* %s%s - %s%s\n' % (self.BOLD, u.date, u.author, self.NC)
+ if u.message:
+ result += html2text(u.message)
+ return result
+
+class IssuesListFormatter(IFormatter):
+ MANDATORY_FIELDS = ('id', 'project', 'status', 'title', 'category')
+
+ count = 0
+
+ def flush(self):
+ self.count = 0
+ pass
+
+ def format_dict(self, item):
+ self.count += 1
+ result = u'%s* (%s) %s - [%s] %s%s\n' % (self.BOLD, item['id'], item['project'].name, item['status'].name, item['title'], self.NC)
+ result += ' %s' % (item['category'])
+ return result
+
+class BoobTracker(ReplApplication):
+ APPNAME = 'boobtracker'
+ VERSION = '0.9'
+ COPYRIGHT = 'Copyright(C) 2011 Romain Bignon'
+ DESCRIPTION = "Console application allowing to send messages on various websites and " \
+ "to display message threads and contents."
+ CAPS = ICapBugTracker
+ EXTRA_FORMATTERS = {'issue_info': IssueFormatter,
+ 'issues_list': IssuesListFormatter,
+ }
+ COMMANDS_FORMATTERS = {'get': 'issue_info',
+ 'post': 'issue_info',
+ 'edit': 'issue_info',
+ 'search': 'issues_list',
+ 'ls': 'issues_list',
+ }
+
+ def add_application_options(self, group):
+ group.add_option('--author')
+ group.add_option('--title')
+ group.add_option('--assignee')
+ group.add_option('--target-version', dest='version')
+ group.add_option('--category')
+ group.add_option('--status')
+
+ def do_search(self, line):
+ """
+ search PROJECT
+
+ List issues for a project.
+
+ You can use these filters from command line:
+ --author AUTHOR
+ --title TITLE_PATTERN
+ --assignee ASSIGNEE
+ --target-version VERSION
+ --category CATEGORY
+ --status STATUS
+ """
+ query = Query()
+
+ path = self.working_path.get()
+ backends = []
+ if line.strip():
+ query.project, backends = self.parse_id(line, unique_backend=True)
+ elif len(path) > 0:
+ query.project = path[0]
+ else:
+ print >>sys.stderr, 'Please enter a project name'
+ return 1
+
+ query.author = self.options.author
+ query.title = self.options.title
+ query.assignee = self.options.assignee
+ query.version = self.options.version
+ query.category = self.options.category
+ query.status = self.options.status
+
+ self.change_path('/%s/search' % query.project)
+ for backend, issue in self.do('iter_issues', query, backends=backends):
+ self.add_object(issue)
+ self.format(issue)
+ self.flush()
+
+ def complete_get(self, text, line, *ignored):
+ args = line.split(' ')
+ if len(args) == 2:
+ return self._complete_object()
+
+ def do_get(self, line):
+ """
+ get ISSUE
+
+ Get an issue and display it.
+ """
+ if not line:
+ print >>sys.stderr, 'This command takes an argument: %s' % self.get_command_help('get', short=True)
+ return 2
+
+ issue = self.get_object(line, 'get_issue')
+ if not issue:
+ print >>sys.stderr, 'Issue not found: %s' % line
+ return 3
+ self.format(issue)
+ self.flush()
+
+ def complete_comment(self, text, line, *ignored):
+ args = line.split(' ')
+ if len(args) == 2:
+ return self._complete_object()
+
+ def do_comment(self, line):
+ """
+ comment ISSUE [TEXT]
+
+ Comment an issue. If no text is given, enter it in standard input.
+ """
+ id, text = self.parse_command_args(line, 2, 1)
+ if text is None:
+ text = self.acquire_input()
+
+ id, backend_name = self.parse_id(id, unique_backend=True)
+ update = Update(0)
+ update.message = text
+
+ self.do('update_issue', id, update, backends=backend_name).wait()
+
+ def complete_remove(self, text, line, *ignored):
+ args = line.split(' ')
+ if len(args) == 2:
+ return self._complete_object()
+
+ def do_remove(self, line):
+ """
+ remove ISSUE
+
+ Remove an issue.
+ """
+ id, backend_name = self.parse_id(line, unique_backend=True)
+ self.do('remove_issue', id, backends=backend_name).wait()
+
+ ISSUE_FIELDS = (('title', (None, False)),
+ ('assignee', ('members', True)),
+ ('version', ('versions', True)),
+ ('category', ('categories', False)),
+ ('status', ('statuses', True)),
+ )
+
+ def get_list_item(self, objects_list, name):
+ if name is None:
+ return None
+
+ for obj in objects_list:
+ if obj.name.lower() == name.lower():
+ return obj
+ print 'Error: "%s" is not found' % name
+ return None
+
+ def prompt_issue(self, issue, requested_key=None, requested_value=None):
+ for key, (list_name, is_list_object) in self.ISSUE_FIELDS:
+ if requested_key and requested_key != key:
+ continue
+
+ if requested_value:
+ value = requested_value
+ elif not self.interactive:
+ value = getattr(self.options, key)
+ else:
+ value = None
+
+ if sys.stdin.isatty():
+ default = getattr(issue, key)
+ if not default:
+ default = None
+ elif 'name' in dir(default):
+ default = default.name
+ if list_name is None:
+ if value is not None:
+ setattr(issue, key, value)
+ print '%s: %s' % (key.capitalize(), value)
+ continue
+ setattr(issue, key, self.ask(key.capitalize(), default=default))
+ else:
+ objects_list = getattr(issue.project, list_name)
+ if len(objects_list) == 0:
+ continue
+
+ print '----------'
+ if value is not None:
+ if is_list_object:
+ value = self.get_list_item(objects_list, value)
+ if value is not None:
+ setattr(issue, key, value)
+ print '%s: %s' % (key.capitalize(), value.name)
+ continue
+
+ while value is None:
+ print 'Availables:', ', '.join([(o if isinstance(o, basestring) else o.name) for o in objects_list])
+ if is_list_object and getattr(issue, key):
+ default = getattr(issue, key).name
+ else:
+ default = getattr(issue, key) or ''
+ text = self.ask(key.capitalize(), default=default)
+ if not text:
+ break
+ if is_list_object:
+ value = self.get_list_item(objects_list, text)
+ else:
+ value = text
+
+ if value is not None:
+ setattr(issue, key, value)
+
+ def do_post(self, line):
+ """
+ post PROJECT
+
+ Post a new issue.
+
+ If you are not in interactive mode, you can use these parameters:
+ --title TITLE
+ --assignee ASSIGNEE
+ --target-version VERSION
+ --category CATEGORY
+ --status STATUS
+ """
+ if not line.strip():
+ print 'Please give the project name'
+ return 1
+
+ project, backend_name = self.parse_id(line, unique_backend=True)
+
+ backend = self.weboob.get_backend(backend_name)
+ issue = backend.create_issue(project)
+
+ self.prompt_issue(issue)
+ if sys.stdin.isatty():
+ print '----------'
+ print 'Please enter the content of this new issue.'
+ issue.body = self.acquire_input()
+
+ for backend, issue in self.weboob.do('post_issue', issue, backends=backend):
+ if issue:
+ print 'Issue %s%s@%s%s created' % (self.BOLD, issue.id, issue.backend, self.NC)
+
+ def complete_remove(self, text, line, *ignored):
+ args = line.split(' ')
+ if len(args) == 2:
+ return self._complete_object()
+ if len(args) == 3:
+ return dict(self.ISSUE_FIELDS).keys()
+
+ def do_edit(self, line):
+ """
+ edit ISSUE [KEY [VALUE]]
+
+ Edit an issue.
+ If you are not in interactive mode, you can use these parameters:
+ --title TITLE
+ --assignee ASSIGNEE
+ --target-version VERSION
+ --category CATEGORY
+ --status STATUS
+ """
+ _id, key, value = self.parse_command_args(line, 3, 1)
+ issue = self.get_object(_id, 'get_issue')
+ if not issue:
+ print >>sys.stderr, 'Issue not found: %s' % _id
+ return 3
+
+ self.prompt_issue(issue, key, value)
+
+ for backend, i in self.weboob.do('post_issue', issue, backends=issue.backend):
+ if i:
+ print 'Issue %s%s@%s%s updated' % (self.BOLD, issue.id, issue.backend, self.NC)
+ self.format(i)
+ self.flush()
+
+ def complete_attach(self, text, line, *ignored):
+ args = line.split(' ')
+ if len(args) == 2:
+ return self._complete_object()
+ elif len(args) >= 3:
+ return self.path_completer(args[2])
+
+ def do_attach(self, line):
+ """
+ attach ISSUE FILENAME
+
+ Attach a file to an issue (Not implemented yet).
+ """
+ print >>sys.stderr, 'Not implemented yet.'