create/edit tickets in a text editor
This commit is contained in:
parent
1a0dc93388
commit
2b0d4e8b91
2 changed files with 163 additions and 68 deletions
|
|
@ -19,10 +19,16 @@
|
||||||
|
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from email import message_from_string, message_from_file
|
||||||
|
from email.Header import decode_header
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from smtplib import SMTP
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from weboob.capabilities.base import empty, CapBaseObject
|
from weboob.capabilities.base import empty, CapBaseObject
|
||||||
from weboob.capabilities.bugtracker import ICapBugTracker, Query, Update, Project, Issue
|
from weboob.capabilities.bugtracker import ICapBugTracker, Query, Update, Project, Issue, IssueError
|
||||||
from weboob.tools.application.repl import ReplApplication, defaultcount
|
from weboob.tools.application.repl import ReplApplication, defaultcount
|
||||||
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
|
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
|
||||||
from weboob.tools.misc import html2text
|
from weboob.tools.misc import html2text
|
||||||
|
|
@ -223,6 +229,11 @@ class BoobTracker(ReplApplication):
|
||||||
|
|
||||||
self.do('update_issue', id, update, backends=backend_name).wait()
|
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):
|
def do_remove(self, line):
|
||||||
"""
|
"""
|
||||||
remove ISSUE
|
remove ISSUE
|
||||||
|
|
@ -246,63 +257,154 @@ class BoobTracker(ReplApplication):
|
||||||
for obj in objects_list:
|
for obj in objects_list:
|
||||||
if obj.name.lower() == name.lower():
|
if obj.name.lower() == name.lower():
|
||||||
return obj
|
return obj
|
||||||
print 'Error: "%s" is not found' % name
|
|
||||||
return None
|
|
||||||
|
|
||||||
def prompt_issue(self, issue, requested_key=None, requested_value=None):
|
if not name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
raise ValueError('"%s" is not found' % name)
|
||||||
|
|
||||||
|
def issue2text(self, issue, backend=None):
|
||||||
|
if backend is not None and 'username' in backend.config:
|
||||||
|
sender = backend.config['username'].get()
|
||||||
|
else:
|
||||||
|
sender = os.environ.get('USERNAME', 'boobtracker')
|
||||||
|
output = u'From: %s\n' % sender
|
||||||
for key, (list_name, is_list_object) in self.ISSUE_FIELDS:
|
for key, (list_name, is_list_object) in self.ISSUE_FIELDS:
|
||||||
if requested_key and requested_key != key:
|
if not self.interactive:
|
||||||
|
value = getattr(self.options, key)
|
||||||
|
if not value:
|
||||||
|
value = getattr(issue, key)
|
||||||
|
if not value:
|
||||||
|
value = ''
|
||||||
|
elif hasattr(value, 'name'):
|
||||||
|
value = value.name
|
||||||
|
|
||||||
|
if list_name is not None:
|
||||||
|
objects_list = getattr(issue.project, list_name)
|
||||||
|
if len(objects_list) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
output += '%s: %s\n' % (key.capitalize(), value)
|
||||||
|
if list_name is not None:
|
||||||
|
availables = ', '.join(['<%s>' % (o if isinstance(o, basestring) else o.name)
|
||||||
|
for o in objects_list])
|
||||||
|
output += 'X-Available-%s: %s\n' % (key.capitalize(), availables)
|
||||||
|
|
||||||
|
for key, value in issue.fields.iteritems():
|
||||||
|
output += '%s: %s\n' % (key, value or '')
|
||||||
|
|
||||||
|
output += '\n%s' % (issue.body or 'Please write your bug report here.')
|
||||||
|
return output
|
||||||
|
|
||||||
|
def text2issue(self, issue, m):
|
||||||
|
# XXX HACK to support real incoming emails
|
||||||
|
if 'Subject' in m:
|
||||||
|
m['Title'] = m['Subject']
|
||||||
|
|
||||||
|
for key, (list_name, is_list_object) in self.ISSUE_FIELDS:
|
||||||
|
value = m.get(key)
|
||||||
|
if value is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if requested_value:
|
new_value = u''
|
||||||
value = requested_value
|
for part in decode_header(value):
|
||||||
elif not self.interactive:
|
if part[1]:
|
||||||
value = getattr(self.options, key)
|
new_value += unicode(part[0], part[1])
|
||||||
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:
|
else:
|
||||||
objects_list = getattr(issue.project, list_name)
|
new_value += unicode(part[0])
|
||||||
if len(objects_list) == 0:
|
value = new_value
|
||||||
|
|
||||||
|
if is_list_object:
|
||||||
|
objects_list = getattr(issue.project, list_name)
|
||||||
|
value = self.get_list_item(objects_list, value)
|
||||||
|
|
||||||
|
setattr(issue, key, value)
|
||||||
|
|
||||||
|
for key in issue.fields.keys():
|
||||||
|
value = m.get(key)
|
||||||
|
if value is not None:
|
||||||
|
issue.fields[key] = value.decode('utf-8')
|
||||||
|
|
||||||
|
content = u''
|
||||||
|
for part in m.walk():
|
||||||
|
if part.get_content_type() == 'text/plain':
|
||||||
|
s = part.get_payload(decode=True)
|
||||||
|
charsets = part.get_charsets() + m.get_charsets()
|
||||||
|
for charset in charsets:
|
||||||
|
try:
|
||||||
|
if charset is not None:
|
||||||
|
content += unicode(s, charset)
|
||||||
|
else:
|
||||||
|
content += unicode(s)
|
||||||
|
except UnicodeError as e:
|
||||||
|
self.logger.warning('Unicode error: %s' % e)
|
||||||
continue
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.exception(e)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
print '----------'
|
issue.body = content
|
||||||
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:
|
emails = re.findall('<(.*@.*)>', m['From'] or '')
|
||||||
print 'Availables:', ', '.join([(o if isinstance(o, basestring) else o.name) for o in objects_list])
|
if len(emails) > 0:
|
||||||
if is_list_object and getattr(issue, key):
|
return emails[0]
|
||||||
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:
|
def edit_issue(self, issue, edit=True):
|
||||||
setattr(issue, key, value)
|
backend = self.weboob.get_backend(issue.backend)
|
||||||
|
content = self.issue2text(issue, backend)
|
||||||
|
while True:
|
||||||
|
if sys.stdin.isatty():
|
||||||
|
content = self.acquire_input(content, {'vim': "-c 'set ft=mail'"})
|
||||||
|
m = message_from_string(content.encode('utf-8'))
|
||||||
|
else:
|
||||||
|
m = message_from_file(sys.stdin)
|
||||||
|
|
||||||
|
try:
|
||||||
|
email_to = self.text2issue(issue, m)
|
||||||
|
except ValueError as e:
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
raise
|
||||||
|
raw_input("%s -- Press Enter to continue..." % e)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
issue = backend.post_issue(issue)
|
||||||
|
print 'Issue %s %s' % (self.formatter.colored(issue.fullid, 'red', 'bold'),
|
||||||
|
'updated' if edit else 'created')
|
||||||
|
if edit:
|
||||||
|
self.format(issue)
|
||||||
|
elif email_to:
|
||||||
|
self.send_notification(email_to, issue)
|
||||||
|
return 0
|
||||||
|
except IssueError as e:
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
raise
|
||||||
|
raw_input("%s -- Press Enter to continue..." % e)
|
||||||
|
|
||||||
|
def send_notification(self, email_to, issue):
|
||||||
|
text = """Hi,
|
||||||
|
|
||||||
|
You have successfuly created this ticket on the Weboob tracker:
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
You can follow your bug report on this page:
|
||||||
|
|
||||||
|
https://symlink.me/issues/%s
|
||||||
|
|
||||||
|
Regards,
|
||||||
|
|
||||||
|
Weboob Team
|
||||||
|
""" % (issue.title, issue.id)
|
||||||
|
msg = MIMEText(text)
|
||||||
|
msg['Subject'] = 'Issue #%s reported' % issue.id
|
||||||
|
msg['From'] = 'Weboob <weboob@weboob.org>'
|
||||||
|
msg['To'] = email_to
|
||||||
|
s = SMTP('localhost')
|
||||||
|
s.sendmail('weboob@weboob.org', [email_to], msg.as_string())
|
||||||
|
s.quit()
|
||||||
|
|
||||||
def do_post(self, line):
|
def do_post(self, line):
|
||||||
"""
|
"""
|
||||||
|
|
@ -324,19 +426,13 @@ class BoobTracker(ReplApplication):
|
||||||
project, backend_name = self.parse_id(line, unique_backend=True)
|
project, backend_name = self.parse_id(line, unique_backend=True)
|
||||||
|
|
||||||
backend = self.weboob.get_backend(backend_name)
|
backend = self.weboob.get_backend(backend_name)
|
||||||
|
|
||||||
issue = backend.create_issue(project)
|
issue = backend.create_issue(project)
|
||||||
|
issue.backend = backend.name
|
||||||
|
|
||||||
self.prompt_issue(issue)
|
return self.edit_issue(issue, edit=False)
|
||||||
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):
|
def complete_edit(self, text, line, *ignored):
|
||||||
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(' ')
|
args = line.split(' ')
|
||||||
if len(args) == 2:
|
if len(args) == 2:
|
||||||
return self._complete_object()
|
return self._complete_object()
|
||||||
|
|
@ -361,12 +457,7 @@ class BoobTracker(ReplApplication):
|
||||||
print >>sys.stderr, 'Issue not found: %s' % _id
|
print >>sys.stderr, 'Issue not found: %s' % _id
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
self.prompt_issue(issue, key, value)
|
return self.edit_issue(issue, edit=True)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def complete_attach(self, text, line, *ignored):
|
def complete_attach(self, text, line, *ignored):
|
||||||
args = line.split(' ')
|
args = line.split(' ')
|
||||||
|
|
|
||||||
|
|
@ -453,15 +453,19 @@ class ConsoleApplication(BaseApplication):
|
||||||
|
|
||||||
return v.get()
|
return v.get()
|
||||||
|
|
||||||
def acquire_input(self, content=None):
|
def acquire_input(self, content=None, editor_params=None):
|
||||||
editor = os.getenv('EDITOR', 'vi')
|
editor = os.getenv('EDITOR', 'vi')
|
||||||
if sys.stdin.isatty() and editor:
|
if sys.stdin.isatty() and editor:
|
||||||
with NamedTemporaryFile() as f:
|
with NamedTemporaryFile() as f:
|
||||||
filename = f.name
|
filename = f.name
|
||||||
if content is not None:
|
if content is not None:
|
||||||
|
if isinstance(content, unicode):
|
||||||
|
content = content.encode(sys.stdin.encoding or locale.getpreferredencoding())
|
||||||
f.write(content)
|
f.write(content)
|
||||||
f.flush()
|
f.flush()
|
||||||
os.system("%s %s" % (editor, filename))
|
if editor_params is not None and editor in editor_params:
|
||||||
|
params = editor_params[editor]
|
||||||
|
os.system("%s %s %s" % (editor, params, filename))
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
text = f.read()
|
text = f.read()
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue