fix compatibility with redmine 2.4, support start/end/tracker/priority (courtesy of François Revol)
This commit is contained in:
parent
57da45d9b0
commit
07a1d04bb6
5 changed files with 234 additions and 22 deletions
|
|
@ -120,7 +120,8 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
|
||||||
raise CollectionNotFound(collection.split_path)
|
raise CollectionNotFound(collection.split_path)
|
||||||
|
|
||||||
############# CapBugTracker ###################################################
|
############# CapBugTracker ###################################################
|
||||||
def _build_project(self, project_dict):
|
@classmethod
|
||||||
|
def _build_project(cls, project_dict):
|
||||||
project = Project(project_dict['name'], project_dict['name'])
|
project = Project(project_dict['name'], project_dict['name'])
|
||||||
project.members = [User(int(u[0]), u[1]) for u in project_dict['members']]
|
project.members = [User(int(u[0]), u[1]) for u in project_dict['members']]
|
||||||
project.versions = [Version(int(v[0]), v[1]) for v in project_dict['versions']]
|
project.versions = [Version(int(v[0]), v[1]) for v in project_dict['versions']]
|
||||||
|
|
@ -129,6 +130,20 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
|
||||||
project.statuses = [Status(int(s[0]), s[1], 0) for s in project_dict['statuses']]
|
project.statuses = [Status(int(s[0]), s[1], 0) for s in project_dict['statuses']]
|
||||||
return project
|
return project
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _attr_to_id(availables, text):
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(text, basestring) and text.isdigit():
|
||||||
|
return text
|
||||||
|
|
||||||
|
for value, key in availables:
|
||||||
|
if key.lower() == text.lower():
|
||||||
|
return value
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
def iter_issues(self, query):
|
def iter_issues(self, query):
|
||||||
"""
|
"""
|
||||||
Iter issues with optionnal patterns.
|
Iter issues with optionnal patterns.
|
||||||
|
|
@ -136,13 +151,13 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
|
||||||
@param query [Query]
|
@param query [Query]
|
||||||
@return [iter(Issue)] issues
|
@return [iter(Issue)] issues
|
||||||
"""
|
"""
|
||||||
# TODO link between text and IDs.
|
params = self.browser.get_project(query.project)
|
||||||
kwargs = {'subject': query.title,
|
kwargs = {'subject': query.title,
|
||||||
'author_id': query.author,
|
'author_id': self._attr_to_id(params['members'], query.author),
|
||||||
'assigned_to_id': query.assignee,
|
'assigned_to_id': self._attr_to_id(params['members'], query.assignee),
|
||||||
'fixed_version_id': query.version,
|
'fixed_version_id': self._attr_to_id(params['versions'], query.version),
|
||||||
'category_id': query.category,
|
'category_id': self._attr_to_id(params['categories'], query.category),
|
||||||
'status_id': query.status,
|
'status_id': self._attr_to_id(params['statuses'], query.status),
|
||||||
}
|
}
|
||||||
r = self.browser.query_issues(query.project, **kwargs)
|
r = self.browser.query_issues(query.project, **kwargs)
|
||||||
project = self._build_project(r['project'])
|
project = self._build_project(r['project'])
|
||||||
|
|
@ -152,6 +167,8 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
|
||||||
obj.title = issue['subject']
|
obj.title = issue['subject']
|
||||||
obj.creation = issue['created_on']
|
obj.creation = issue['created_on']
|
||||||
obj.updated = issue['updated_on']
|
obj.updated = issue['updated_on']
|
||||||
|
obj.start = issue['start_date']
|
||||||
|
obj.due = issue['due_date']
|
||||||
|
|
||||||
if isinstance(issue['author'], tuple):
|
if isinstance(issue['author'], tuple):
|
||||||
obj.author = project.find_user(*issue['author'])
|
obj.author = project.find_user(*issue['author'])
|
||||||
|
|
@ -162,6 +179,7 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
|
||||||
else:
|
else:
|
||||||
obj.assignee = issue['assigned_to']
|
obj.assignee = issue['assigned_to']
|
||||||
|
|
||||||
|
obj.tracker = issue['tracker']
|
||||||
obj.category = issue['category']
|
obj.category = issue['category']
|
||||||
|
|
||||||
if issue['fixed_version'] is not None:
|
if issue['fixed_version'] is not None:
|
||||||
|
|
@ -169,6 +187,7 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
|
||||||
else:
|
else:
|
||||||
obj.version = None
|
obj.version = None
|
||||||
obj.status = project.find_status(issue['status'])
|
obj.status = project.find_status(issue['status'])
|
||||||
|
obj.priority = issue['priority']
|
||||||
yield obj
|
yield obj
|
||||||
|
|
||||||
def get_issue(self, issue):
|
def get_issue(self, issue):
|
||||||
|
|
@ -189,6 +208,8 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
|
||||||
issue.body = params['body']
|
issue.body = params['body']
|
||||||
issue.creation = params['created_on']
|
issue.creation = params['created_on']
|
||||||
issue.updated = params['updated_on']
|
issue.updated = params['updated_on']
|
||||||
|
issue.start = params['start_date']
|
||||||
|
issue.due = params['due_date']
|
||||||
issue.fields = {}
|
issue.fields = {}
|
||||||
for key, value in params['fields'].iteritems():
|
for key, value in params['fields'].iteritems():
|
||||||
issue.fields[key] = value
|
issue.fields[key] = value
|
||||||
|
|
@ -214,9 +235,11 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
|
||||||
issue.history.append(update)
|
issue.history.append(update)
|
||||||
issue.author = issue.project.find_user(*params['author'])
|
issue.author = issue.project.find_user(*params['author'])
|
||||||
issue.assignee = issue.project.find_user(*params['assignee'])
|
issue.assignee = issue.project.find_user(*params['assignee'])
|
||||||
|
issue.tracker = params['tracker'][1]
|
||||||
issue.category = params['category'][1]
|
issue.category = params['category'][1]
|
||||||
issue.version = issue.project.find_version(*params['version'])
|
issue.version = issue.project.find_version(*params['version'])
|
||||||
issue.status = issue.project.find_status(params['status'][1])
|
issue.status = issue.project.find_status(params['status'][1])
|
||||||
|
issue.priority = params['priority'][1]
|
||||||
|
|
||||||
return issue
|
return issue
|
||||||
|
|
||||||
|
|
@ -239,8 +262,12 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
|
||||||
kwargs = {'title': issue.title,
|
kwargs = {'title': issue.title,
|
||||||
'version': issue.version.id if issue.version else None,
|
'version': issue.version.id if issue.version else None,
|
||||||
'assignee': issue.assignee.id if issue.assignee else None,
|
'assignee': issue.assignee.id if issue.assignee else None,
|
||||||
|
'tracker': issue.tracker if issue.tracker else None,
|
||||||
'category': issue.category,
|
'category': issue.category,
|
||||||
'status': issue.status.id if issue.status else None,
|
'status': issue.status.id if issue.status else None,
|
||||||
|
'priority': issue.priority if issue.priority else None,
|
||||||
|
'start': issue.start if issue.start else None,
|
||||||
|
'due': issue.due if issue.due else None,
|
||||||
'body': issue.body,
|
'body': issue.body,
|
||||||
'fields': issue.fields,
|
'fields': issue.fields,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,11 @@ class RedmineBrowser(BaseBrowser):
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
for key, div in self.page.iter_custom_fields():
|
for key, div in self.page.iter_custom_fields():
|
||||||
fields[key] = div.attrib['value']
|
if 'value' in div.attrib:
|
||||||
|
fields[key] = div.attrib['value']
|
||||||
|
else:
|
||||||
|
olist = div.xpath('.//option[@selected="selected"]')
|
||||||
|
fields[key] = ', '.join({i.attrib['value'] for i in olist})
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,10 @@ import datetime
|
||||||
|
|
||||||
from weboob.capabilities.bugtracker import IssueError
|
from weboob.capabilities.bugtracker import IssueError
|
||||||
from weboob.tools.browser import BasePage, BrokenPageError
|
from weboob.tools.browser import BasePage, BrokenPageError
|
||||||
|
from weboob.tools.date import parse_french_date
|
||||||
from weboob.tools.misc import to_unicode
|
from weboob.tools.misc import to_unicode
|
||||||
from weboob.tools.mech import ClientForm
|
from weboob.tools.mech import ClientForm
|
||||||
|
from weboob.tools.json import json
|
||||||
|
|
||||||
|
|
||||||
class BaseIssuePage(BasePage):
|
class BaseIssuePage(BasePage):
|
||||||
|
|
@ -48,6 +50,10 @@ class BaseIssuePage(BasePage):
|
||||||
int(m.group(4)),
|
int(m.group(4)),
|
||||||
int(m.group(5)))
|
int(m.group(5)))
|
||||||
|
|
||||||
|
m = re.match('(\d+) (\w+) (\d+) (\d+):(\d+)', text)
|
||||||
|
if m:
|
||||||
|
return parse_french_date(text)
|
||||||
|
|
||||||
self.logger.warning('Unable to parse "%s"' % text)
|
self.logger.warning('Unable to parse "%s"' % text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
@ -109,6 +115,63 @@ class IssuesPage(BaseIssuePage):
|
||||||
'statuses': 'values_status_id',
|
'statuses': 'values_status_id',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_from_js(self, pattern, end, is_list=False):
|
||||||
|
"""
|
||||||
|
find a pattern in any javascript text
|
||||||
|
"""
|
||||||
|
value = None
|
||||||
|
for script in self.document.xpath('//script'):
|
||||||
|
txt = script.text
|
||||||
|
if txt is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
start = txt.find(pattern)
|
||||||
|
if start < 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if value is None:
|
||||||
|
value = ''
|
||||||
|
else:
|
||||||
|
value += ','
|
||||||
|
value += txt[start+len(pattern):start+txt[start+len(pattern):].find(end)+len(pattern)]
|
||||||
|
|
||||||
|
if not is_list:
|
||||||
|
break
|
||||||
|
|
||||||
|
txt = txt[start+len(pattern)+txt[start+len(pattern):].find(end):]
|
||||||
|
|
||||||
|
start = txt.find(pattern)
|
||||||
|
if start < 0:
|
||||||
|
break
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def get_project(self, project_name):
|
||||||
|
project = super(IssuesPage, self).get_project(project_name)
|
||||||
|
if len(project['statuses']) > 0:
|
||||||
|
return project
|
||||||
|
|
||||||
|
args = self.get_from_js('var availableFilters = ', ';')
|
||||||
|
if args is None:
|
||||||
|
return project
|
||||||
|
|
||||||
|
args = json.loads(args)
|
||||||
|
def get_values(key):
|
||||||
|
values = []
|
||||||
|
if not key in args:
|
||||||
|
return values
|
||||||
|
for key, value in args[key]['values']:
|
||||||
|
if value.isdigit():
|
||||||
|
values.append((value, key))
|
||||||
|
return values
|
||||||
|
|
||||||
|
project['members'] = get_values('assigned_to_id')
|
||||||
|
project['categories'] = get_values('category_id')
|
||||||
|
project['versions'] = get_values('fixed_version_id')
|
||||||
|
project['statuses'] = get_values('status_id')
|
||||||
|
return project
|
||||||
|
|
||||||
def get_query_method(self):
|
def get_query_method(self):
|
||||||
return self.document.xpath('//form[@id="query_form"]')[0].attrib['method'].upper()
|
return self.document.xpath('//form[@id="query_form"]')[0].attrib['method'].upper()
|
||||||
|
|
||||||
|
|
@ -167,7 +230,9 @@ class NewIssuePage(BaseIssuePage):
|
||||||
return m.group(1)
|
return m.group(1)
|
||||||
|
|
||||||
def iter_custom_fields(self):
|
def iter_custom_fields(self):
|
||||||
for div in self.document.xpath('//form//input[starts-with(@id, "issue_custom_field")]'):
|
for div in self.document.xpath('//form//input[starts-with(@id, "issue_custom_field")]|//form//select[starts-with(@id, "issue_custom_field")]'):
|
||||||
|
if 'type' in div.attrib and div.attrib['type'] == 'hidden':
|
||||||
|
continue
|
||||||
label = self.document.xpath('//label[@for="%s"]' % div.attrib['id'])[0]
|
label = self.document.xpath('//label[@for="%s"]' % div.attrib['id'])[0]
|
||||||
yield label.text.strip(), div
|
yield label.text.strip(), div
|
||||||
|
|
||||||
|
|
@ -192,6 +257,29 @@ class NewIssuePage(BaseIssuePage):
|
||||||
except ClientForm.ItemNotFoundError:
|
except ClientForm.ItemNotFoundError:
|
||||||
self.logger.warning('Version not found: %s' % version)
|
self.logger.warning('Version not found: %s' % version)
|
||||||
|
|
||||||
|
def set_tracker(self, tracker):
|
||||||
|
if tracker:
|
||||||
|
select = self.parser.select(self.document.getroot(), 'select#issue_tracker_id', 1)
|
||||||
|
for option in select.findall('option'):
|
||||||
|
if option.text and option.text.strip() == tracker:
|
||||||
|
self.browser['issue[tracker_id]'] = [option.attrib['value']]
|
||||||
|
return
|
||||||
|
# value = None
|
||||||
|
# if len(self.document.xpath('//a[@title="New tracker"]')) > 0:
|
||||||
|
# value = self.browser.create_tracker(self.get_project_name(), tracker, self.get_authenticity_token())
|
||||||
|
# if value:
|
||||||
|
# control = self.browser.find_control('issue[tracker_id]')
|
||||||
|
# ClientForm.Item(control, {'name': tracker, 'value': value})
|
||||||
|
# self.browser['issue[tracker_id]'] = [value]
|
||||||
|
# else:
|
||||||
|
# self.logger.warning('Tracker "%s" not found' % tracker)
|
||||||
|
self.logger.warning('Tracker "%s" not found' % tracker)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.browser['issue[tracker_id]'] = ['']
|
||||||
|
except ClientForm.ControlNotFoundError:
|
||||||
|
self.logger.warning('Tracker item not found')
|
||||||
|
|
||||||
def set_category(self, category):
|
def set_category(self, category):
|
||||||
if category:
|
if category:
|
||||||
select = self.parser.select(self.document.getroot(), 'select#issue_category_id', 1)
|
select = self.parser.select(self.document.getroot(), 'select#issue_category_id', 1)
|
||||||
|
|
@ -209,19 +297,61 @@ class NewIssuePage(BaseIssuePage):
|
||||||
else:
|
else:
|
||||||
self.logger.warning('Category "%s" not found' % category)
|
self.logger.warning('Category "%s" not found' % category)
|
||||||
else:
|
else:
|
||||||
self.browser['issue[category_id]'] = ['']
|
try:
|
||||||
|
self.browser['issue[category_id]'] = ['']
|
||||||
|
except ClientForm.ControlNotFoundError:
|
||||||
|
self.logger.warning('Category item not found')
|
||||||
|
|
||||||
def set_status(self, status):
|
def set_status(self, status):
|
||||||
assert status is not None
|
assert status is not None
|
||||||
self.browser['issue[status_id]'] = [str(status)]
|
self.browser['issue[status_id]'] = [str(status)]
|
||||||
|
|
||||||
|
def set_priority(self, priority):
|
||||||
|
if priority:
|
||||||
|
select = self.parser.select(self.document.getroot(), 'select#issue_priority_id', 1)
|
||||||
|
for option in select.findall('option'):
|
||||||
|
if option.text and option.text.strip() == priority:
|
||||||
|
self.browser['issue[priority_id]'] = [option.attrib['value']]
|
||||||
|
return
|
||||||
|
# value = None
|
||||||
|
# if len(self.document.xpath('//a[@title="New priority"]')) > 0:
|
||||||
|
# value = self.browser.create_priority(self.get_project_name(), priority, self.get_authenticity_token())
|
||||||
|
# if value:
|
||||||
|
# control = self.browser.find_control('issue[priority_id]')
|
||||||
|
# ClientForm.Item(control, {'name': priority, 'value': value})
|
||||||
|
# self.browser['issue[priority_id]'] = [value]
|
||||||
|
# else:
|
||||||
|
# self.logger.warning('Priority "%s" not found' % priority)
|
||||||
|
self.logger.warning('Priority "%s" not found' % priority)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.browser['issue[priority_id]'] = ['']
|
||||||
|
except ClientForm.ControlNotFoundError:
|
||||||
|
self.logger.warning('Priority item not found')
|
||||||
|
|
||||||
|
def set_start(self, start):
|
||||||
|
if start is not None:
|
||||||
|
self.browser['issue[start_date]'] = start.strftime("%Y-%m-%d")
|
||||||
|
#XXX: else set to "" ?
|
||||||
|
|
||||||
|
def set_due(self, due):
|
||||||
|
if due is not None:
|
||||||
|
self.browser['issue[due_date]'] = due.strftime("%Y-%m-%d")
|
||||||
|
#XXX: else set to "" ?
|
||||||
|
|
||||||
def set_note(self, message):
|
def set_note(self, message):
|
||||||
self.browser['notes'] = message.encode('utf-8')
|
self.browser['notes'] = message.encode('utf-8')
|
||||||
|
|
||||||
def set_fields(self, fields):
|
def set_fields(self, fields):
|
||||||
for key, div in self.iter_custom_fields():
|
for key, div in self.iter_custom_fields():
|
||||||
try:
|
try:
|
||||||
self.browser[div.attrib['name']] = fields[key]
|
control = self.browser.find_control(div.attrib['name'], nr=0)
|
||||||
|
if isinstance(control, ClientForm.TextControl):
|
||||||
|
control.value = fields[key]
|
||||||
|
else:
|
||||||
|
item = control.get(label=fields[key].encode("utf-8"), nr=0)
|
||||||
|
if item and fields[key] != '':
|
||||||
|
item.selected = True
|
||||||
except KeyError:
|
except KeyError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -273,13 +403,31 @@ class IssuePage(NewIssuePage):
|
||||||
params['updated_on'] = None
|
params['updated_on'] = None
|
||||||
|
|
||||||
params['status'] = self._parse_selection('issue_status_id')
|
params['status'] = self._parse_selection('issue_status_id')
|
||||||
|
params['priority'] = self._parse_selection('issue_priority_id')
|
||||||
params['assignee'] = self._parse_selection('issue_assigned_to_id')
|
params['assignee'] = self._parse_selection('issue_assigned_to_id')
|
||||||
|
params['tracker'] = self._parse_selection('issue_tracker_id')
|
||||||
params['category'] = self._parse_selection('issue_category_id')
|
params['category'] = self._parse_selection('issue_category_id')
|
||||||
params['version'] = self._parse_selection('issue_fixed_version_id')
|
params['version'] = self._parse_selection('issue_fixed_version_id')
|
||||||
|
div = self.parser.select(self.document.getroot(), 'input#issue_start_date', 1)
|
||||||
|
if 'value' in div.attrib:
|
||||||
|
params['start_date'] = datetime.datetime.strptime(div.attrib['value'], "%Y-%m-%d")
|
||||||
|
else:
|
||||||
|
params['start_date'] = None
|
||||||
|
div = self.parser.select(self.document.getroot(), 'input#issue_due_date', 1)
|
||||||
|
if 'value' in div.attrib:
|
||||||
|
params['due_date'] = datetime.datetime.strptime(div.attrib['value'], "%Y-%m-%d")
|
||||||
|
else:
|
||||||
|
params['due_date'] = None
|
||||||
|
|
||||||
params['fields'] = {}
|
params['fields'] = {}
|
||||||
for key, div in self.iter_custom_fields():
|
for key, div in self.iter_custom_fields():
|
||||||
value = div.attrib['value']
|
value = ''
|
||||||
|
if 'value' in div.attrib:
|
||||||
|
value = div.attrib['value']
|
||||||
|
else:
|
||||||
|
# XXX: use _parse_selection()?
|
||||||
|
olist = div.xpath('.//option[@selected="selected"]')
|
||||||
|
value = ', '.join({i.attrib['value'] for i in olist})
|
||||||
params['fields'][key] = value
|
params['fields'][key] = value
|
||||||
|
|
||||||
params['attachments'] = []
|
params['attachments'] = []
|
||||||
|
|
@ -326,14 +474,14 @@ class IssuePage(NewIssuePage):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
for li in details.findall('li'):
|
for li in details.findall('li'):
|
||||||
field = li.find('strong').text.decode('utf-8')
|
field = li.find('strong').text#.decode('utf-8')
|
||||||
i = li.findall('i')
|
i = li.findall('i')
|
||||||
new = None
|
new = None
|
||||||
last = None
|
last = None
|
||||||
if len(i) > 0:
|
if len(i) > 0:
|
||||||
if len(i) == 2:
|
if len(i) == 2:
|
||||||
last = i[0].text.decode('utf-8')
|
last = i[0].text.decode('utf-8')
|
||||||
new = i[-1].text.decode('utf-8')
|
new = i[-1].text#.decode('utf-8')
|
||||||
elif li.find('strike') is not None:
|
elif li.find('strike') is not None:
|
||||||
last = li.find('strike').find('i').text.decode('utf-8')
|
last = li.find('strike').find('i').text.decode('utf-8')
|
||||||
elif li.find('a') is not None:
|
elif li.find('a') is not None:
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,14 @@ from smtplib import SMTP
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
from weboob.capabilities.base import empty, CapBaseObject
|
from weboob.capabilities.base import empty, CapBaseObject
|
||||||
from weboob.capabilities.bugtracker import ICapBugTracker, Query, Update, Project, Issue, IssueError
|
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
|
||||||
|
from weboob.tools.date import parse_french_date
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['BoobTracker']
|
__all__ = ['BoobTracker']
|
||||||
|
|
@ -63,7 +65,9 @@ class IssueFormatter(IFormatter):
|
||||||
result += '\n%s\n\n' % obj.body
|
result += '\n%s\n\n' % obj.body
|
||||||
result += self.format_key('Author', '%s (%s)' % (obj.author.name, obj.creation))
|
result += self.format_key('Author', '%s (%s)' % (obj.author.name, obj.creation))
|
||||||
result += self.format_attr(obj, 'status')
|
result += self.format_attr(obj, 'status')
|
||||||
|
result += self.format_attr(obj, 'priority')
|
||||||
result += self.format_attr(obj, 'version')
|
result += self.format_attr(obj, 'version')
|
||||||
|
result += self.format_attr(obj, 'tracker')
|
||||||
result += self.format_attr(obj, 'category')
|
result += self.format_attr(obj, 'category')
|
||||||
result += self.format_attr(obj, 'assignee')
|
result += self.format_attr(obj, 'assignee')
|
||||||
if hasattr(obj, 'fields') and not empty(obj.fields):
|
if hasattr(obj, 'fields') and not empty(obj.fields):
|
||||||
|
|
@ -122,8 +126,12 @@ class BoobTracker(ReplApplication):
|
||||||
group.add_option('--title')
|
group.add_option('--title')
|
||||||
group.add_option('--assignee')
|
group.add_option('--assignee')
|
||||||
group.add_option('--target-version', dest='version')
|
group.add_option('--target-version', dest='version')
|
||||||
|
group.add_option('--tracker')
|
||||||
group.add_option('--category')
|
group.add_option('--category')
|
||||||
group.add_option('--status')
|
group.add_option('--status')
|
||||||
|
group.add_option('--priority')
|
||||||
|
group.add_option('--start')
|
||||||
|
group.add_option('--due')
|
||||||
|
|
||||||
@defaultcount(10)
|
@defaultcount(10)
|
||||||
def do_search(self, line):
|
def do_search(self, line):
|
||||||
|
|
@ -246,8 +254,12 @@ class BoobTracker(ReplApplication):
|
||||||
ISSUE_FIELDS = (('title', (None, False)),
|
ISSUE_FIELDS = (('title', (None, False)),
|
||||||
('assignee', ('members', True)),
|
('assignee', ('members', True)),
|
||||||
('version', ('versions', True)),
|
('version', ('versions', True)),
|
||||||
|
('tracker', (None, False)),#XXX
|
||||||
('category', ('categories', False)),
|
('category', ('categories', False)),
|
||||||
('status', ('statuses', True)),
|
('status', ('statuses', True)),
|
||||||
|
('priority', (None, False)),#XXX
|
||||||
|
('start', (None, False)),
|
||||||
|
('due', (None, False)),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_list_item(self, objects_list, name):
|
def get_list_item(self, objects_list, name):
|
||||||
|
|
@ -263,6 +275,12 @@ class BoobTracker(ReplApplication):
|
||||||
|
|
||||||
raise ValueError('"%s" is not found' % name)
|
raise ValueError('"%s" is not found' % name)
|
||||||
|
|
||||||
|
def sanitize_key(self, key):
|
||||||
|
if isinstance(key, str):
|
||||||
|
key = unicode(key, "utf8")
|
||||||
|
key = unicodedata.normalize('NFKD', key).encode("ascii", "ignore")
|
||||||
|
return key.replace(' ', '-').capitalize()
|
||||||
|
|
||||||
def issue2text(self, issue, backend=None):
|
def issue2text(self, issue, backend=None):
|
||||||
if backend is not None and 'username' in backend.config:
|
if backend is not None and 'username' in backend.config:
|
||||||
sender = backend.config['username'].get()
|
sender = backend.config['username'].get()
|
||||||
|
|
@ -285,14 +303,15 @@ class BoobTracker(ReplApplication):
|
||||||
if len(objects_list) == 0:
|
if len(objects_list) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
output += '%s: %s\n' % (key.capitalize(), value)
|
output += '%s: %s\n' % (self.sanitize_key(key), value)
|
||||||
if list_name is not None:
|
if list_name is not None:
|
||||||
availables = ', '.join(['<%s>' % (o if isinstance(o, basestring) else o.name)
|
availables = ', '.join(['<%s>' % (o if isinstance(o, basestring) else o.name)
|
||||||
for o in objects_list])
|
for o in objects_list])
|
||||||
output += 'X-Available-%s: %s\n' % (key.capitalize(), availables)
|
output += 'X-Available-%s: %s\n' % (self.sanitize_key(key), availables)
|
||||||
|
|
||||||
for key, value in issue.fields.iteritems():
|
for key, value in issue.fields.iteritems():
|
||||||
output += '%s: %s\n' % (key, value or '')
|
output += '%s: %s\n' % (self.sanitize_key(key), value or '')
|
||||||
|
# TODO: Add X-Available-* for lists
|
||||||
|
|
||||||
output += '\n%s' % (issue.body or 'Please write your bug report here.')
|
output += '\n%s' % (issue.body or 'Please write your bug report here.')
|
||||||
return output
|
return output
|
||||||
|
|
@ -312,17 +331,25 @@ class BoobTracker(ReplApplication):
|
||||||
if part[1]:
|
if part[1]:
|
||||||
new_value += unicode(part[0], part[1])
|
new_value += unicode(part[0], part[1])
|
||||||
else:
|
else:
|
||||||
new_value += unicode(part[0])
|
new_value += part[0].decode('utf-8')
|
||||||
value = new_value
|
value = new_value
|
||||||
|
|
||||||
if is_list_object:
|
if is_list_object:
|
||||||
objects_list = getattr(issue.project, list_name)
|
objects_list = getattr(issue.project, list_name)
|
||||||
value = self.get_list_item(objects_list, value)
|
value = self.get_list_item(objects_list, value)
|
||||||
|
|
||||||
|
# FIXME: autodetect
|
||||||
|
if key in ['start', 'due']:
|
||||||
|
if len(value) > 0:
|
||||||
|
#value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
||||||
|
value = parse_french_date(value)
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
|
||||||
setattr(issue, key, value)
|
setattr(issue, key, value)
|
||||||
|
|
||||||
for key in issue.fields.keys():
|
for key in issue.fields.keys():
|
||||||
value = m.get(key)
|
value = m.get(self.sanitize_key(key))
|
||||||
if value is not None:
|
if value is not None:
|
||||||
issue.fields[key] = value.decode('utf-8')
|
issue.fields[key] = value.decode('utf-8')
|
||||||
|
|
||||||
|
|
@ -336,7 +363,7 @@ class BoobTracker(ReplApplication):
|
||||||
if charset is not None:
|
if charset is not None:
|
||||||
content += unicode(s, charset)
|
content += unicode(s, charset)
|
||||||
else:
|
else:
|
||||||
content += unicode(s)
|
content += unicode(s, encoding='utf-8')
|
||||||
except UnicodeError as e:
|
except UnicodeError as e:
|
||||||
self.logger.warning('Unicode error: %s' % e)
|
self.logger.warning('Unicode error: %s' % e)
|
||||||
continue
|
continue
|
||||||
|
|
@ -367,7 +394,7 @@ class BoobTracker(ReplApplication):
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
if not sys.stdin.isatty():
|
if not sys.stdin.isatty():
|
||||||
raise
|
raise
|
||||||
raw_input("%s -- Press Enter to continue..." % e)
|
raw_input("%s -- Press Enter to continue..." % unicode(e).encode("utf-8"))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -382,7 +409,7 @@ class BoobTracker(ReplApplication):
|
||||||
except IssueError as e:
|
except IssueError as e:
|
||||||
if not sys.stdin.isatty():
|
if not sys.stdin.isatty():
|
||||||
raise
|
raise
|
||||||
raw_input("%s -- Press Enter to continue..." % e)
|
raw_input("%s -- Press Enter to continue..." % unicode(e).encode("utf-8"))
|
||||||
|
|
||||||
def send_notification(self, email_to, issue):
|
def send_notification(self, email_to, issue):
|
||||||
text = """Hi,
|
text = """Hi,
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,10 @@ class Project(CapBaseObject):
|
||||||
name = StringField('Name of the project')
|
name = StringField('Name of the project')
|
||||||
members = Field('Members of projects', list)
|
members = Field('Members of projects', list)
|
||||||
versions = Field('List of versions available for this project', list)
|
versions = Field('List of versions available for this project', list)
|
||||||
|
trackers = Field('All trackers', list)
|
||||||
categories = Field('All categories', list)
|
categories = Field('All categories', list)
|
||||||
statuses = Field('Available statuses for issues', list)
|
statuses = Field('Available statuses for issues', list)
|
||||||
|
priorities = Field('Available priorities for issues', list)
|
||||||
|
|
||||||
def __init__(self, id, name):
|
def __init__(self, id, name):
|
||||||
CapBaseObject.__init__(self, id)
|
CapBaseObject.__init__(self, id)
|
||||||
|
|
@ -199,14 +201,18 @@ class Issue(CapBaseObject):
|
||||||
body = StringField('Text of issue')
|
body = StringField('Text of issue')
|
||||||
creation = DateField('Date when this issue has been created')
|
creation = DateField('Date when this issue has been created')
|
||||||
updated = DateField('Date when this issue has been updated for the last time')
|
updated = DateField('Date when this issue has been updated for the last time')
|
||||||
|
start = DateField('Date when this issue starts')
|
||||||
|
due = DateField('Date when this issue is due for')
|
||||||
attachments = Field('List of attached files', list, tuple)
|
attachments = Field('List of attached files', list, tuple)
|
||||||
history = Field('History of updates', list, tuple)
|
history = Field('History of updates', list, tuple)
|
||||||
author = Field('Author of this issue', User)
|
author = Field('Author of this issue', User)
|
||||||
assignee = Field('User assigned to this issue', User)
|
assignee = Field('User assigned to this issue', User)
|
||||||
|
tracker = StringField('Name of the tracker')
|
||||||
category = StringField('Name of the category')
|
category = StringField('Name of the category')
|
||||||
version = Field('Target version of this issue', Version)
|
version = Field('Target version of this issue', Version)
|
||||||
status = Field('Status of this issue', Status)
|
status = Field('Status of this issue', Status)
|
||||||
fields = Field('Custom fields (key,value)', dict)
|
fields = Field('Custom fields (key,value)', dict)
|
||||||
|
priority = StringField('Priority of the issue') #XXX
|
||||||
|
|
||||||
|
|
||||||
class Query(CapBaseObject):
|
class Query(CapBaseObject):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue