fix compatibility with redmine 2.4, support start/end/tracker/priority (courtesy of François Revol)

This commit is contained in:
Romain Bignon 2014-03-30 12:32:20 +02:00
commit 07a1d04bb6
5 changed files with 234 additions and 22 deletions

View file

@ -120,7 +120,8 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
raise CollectionNotFound(collection.split_path)
############# CapBugTracker ###################################################
def _build_project(self, project_dict):
@classmethod
def _build_project(cls, project_dict):
project = Project(project_dict['name'], project_dict['name'])
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']]
@ -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']]
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):
"""
Iter issues with optionnal patterns.
@ -136,13 +151,13 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
@param query [Query]
@return [iter(Issue)] issues
"""
# TODO link between text and IDs.
params = self.browser.get_project(query.project)
kwargs = {'subject': query.title,
'author_id': query.author,
'assigned_to_id': query.assignee,
'fixed_version_id': query.version,
'category_id': query.category,
'status_id': query.status,
'author_id': self._attr_to_id(params['members'], query.author),
'assigned_to_id': self._attr_to_id(params['members'], query.assignee),
'fixed_version_id': self._attr_to_id(params['versions'], query.version),
'category_id': self._attr_to_id(params['categories'], query.category),
'status_id': self._attr_to_id(params['statuses'], query.status),
}
r = self.browser.query_issues(query.project, **kwargs)
project = self._build_project(r['project'])
@ -152,6 +167,8 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
obj.title = issue['subject']
obj.creation = issue['created_on']
obj.updated = issue['updated_on']
obj.start = issue['start_date']
obj.due = issue['due_date']
if isinstance(issue['author'], tuple):
obj.author = project.find_user(*issue['author'])
@ -162,6 +179,7 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
else:
obj.assignee = issue['assigned_to']
obj.tracker = issue['tracker']
obj.category = issue['category']
if issue['fixed_version'] is not None:
@ -169,6 +187,7 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
else:
obj.version = None
obj.status = project.find_status(issue['status'])
obj.priority = issue['priority']
yield obj
def get_issue(self, issue):
@ -189,6 +208,8 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
issue.body = params['body']
issue.creation = params['created_on']
issue.updated = params['updated_on']
issue.start = params['start_date']
issue.due = params['due_date']
issue.fields = {}
for key, value in params['fields'].iteritems():
issue.fields[key] = value
@ -214,9 +235,11 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
issue.history.append(update)
issue.author = issue.project.find_user(*params['author'])
issue.assignee = issue.project.find_user(*params['assignee'])
issue.tracker = params['tracker'][1]
issue.category = params['category'][1]
issue.version = issue.project.find_version(*params['version'])
issue.status = issue.project.find_status(params['status'][1])
issue.priority = params['priority'][1]
return issue
@ -239,8 +262,12 @@ class RedmineBackend(BaseBackend, ICapContent, ICapBugTracker, ICapCollection):
kwargs = {'title': issue.title,
'version': issue.version.id if issue.version else None,
'assignee': issue.assignee.id if issue.assignee else None,
'tracker': issue.tracker if issue.tracker else None,
'category': issue.category,
'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,
'fields': issue.fields,
}

View file

@ -199,7 +199,11 @@ class RedmineBrowser(BaseBrowser):
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

View file

@ -23,8 +23,10 @@ import datetime
from weboob.capabilities.bugtracker import IssueError
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.mech import ClientForm
from weboob.tools.json import json
class BaseIssuePage(BasePage):
@ -48,6 +50,10 @@ class BaseIssuePage(BasePage):
int(m.group(4)),
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)
return text
@ -109,6 +115,63 @@ class IssuesPage(BaseIssuePage):
'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):
return self.document.xpath('//form[@id="query_form"]')[0].attrib['method'].upper()
@ -167,7 +230,9 @@ class NewIssuePage(BaseIssuePage):
return m.group(1)
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]
yield label.text.strip(), div
@ -192,6 +257,29 @@ class NewIssuePage(BaseIssuePage):
except ClientForm.ItemNotFoundError:
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):
if category:
select = self.parser.select(self.document.getroot(), 'select#issue_category_id', 1)
@ -209,19 +297,61 @@ class NewIssuePage(BaseIssuePage):
else:
self.logger.warning('Category "%s" not found' % category)
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):
assert status is not None
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):
self.browser['notes'] = message.encode('utf-8')
def set_fields(self, fields):
for key, div in self.iter_custom_fields():
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:
continue
@ -273,13 +403,31 @@ class IssuePage(NewIssuePage):
params['updated_on'] = None
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['tracker'] = self._parse_selection('issue_tracker_id')
params['category'] = self._parse_selection('issue_category_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'] = {}
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['attachments'] = []
@ -326,14 +474,14 @@ class IssuePage(NewIssuePage):
pass
else:
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')
new = None
last = None
if len(i) > 0:
if len(i) == 2:
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:
last = li.find('strike').find('i').text.decode('utf-8')
elif li.find('a') is not None: