From efec9fcf00916048340a73b94b7d9330fde1e3b4 Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Mon, 26 Sep 2016 14:02:46 -0400 Subject: [PATCH 01/17] Support passing issue_limit in the config --- bugwarrior/docs/services/redmine.rst | 6 +++--- bugwarrior/services/redmine.py | 14 +++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/bugwarrior/docs/services/redmine.rst b/bugwarrior/docs/services/redmine.rst index 8c9aebdfe..f32c20b73 100644 --- a/bugwarrior/docs/services/redmine.rst +++ b/bugwarrior/docs/services/redmine.rst @@ -17,10 +17,10 @@ Here's an example of a Redmine target:: redmine.key = c0c4c014cafebabe redmine.user_id = 7 redmine.project_name = redmine + redmine.issue_limit = 1000 -The above example is the minimum required to import issues from -Redmine. You can also feel free to use any of the -configuration options described in :ref:`common_configuration_options`. +You can also feel free to use any of the configuration options described in +:ref:`common_configuration_options`. There are also `redmine.login`/`redmine.password` settings if your instance is behind basic auth. diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index a09f855e9..e83159c5d 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -9,15 +9,18 @@ class RedMineClient(ServiceClient): - def __init__(self, url, key, auth): + def __init__(self, url, key, auth, issue_limit): self.url = url self.key = key self.auth = auth + self.issue_limit = issue_limit - def find_issues(self, user_id=None): - args = {"limit": 100} + def find_issues(self, user_id=None, issue_limit=25): + args = {} if user_id is not None: args["assigned_to_id"] = user_id + if issue_limit is not None: + args["limit"] = issue_limit return self.call_api("/issues.json", args)["issues"] def call_api(self, uri, params): @@ -106,12 +109,13 @@ def __init__(self, *args, **kw): self.url = self.config_get('url').rstrip("/") self.key = self.config_get('key') self.user_id = self.config_get('user_id') + self.issue_limit = self.config_get('issue_limit') login = self.config_get_default('login') if login: password = self.config_get_password('password', login) auth = (login, password) if (login and password) else None - self.client = RedMineClient(self.url, self.key, auth) + self.client = RedMineClient(self.url, self.key, auth, self.issue_limit) self.project_name = self.config_get_default('project_name') @@ -137,7 +141,7 @@ def validate_config(cls, config, target): IssueService.validate_config(config, target) def issues(self): - issues = self.client.find_issues(self.user_id) + issues = self.client.find_issues(self.user_id, self.issue_limit) log.debug(" Found %i total.", len(issues)) for issue in issues: From fa6fe1240ce8cc038d86183fda6addf721c8fbb2 Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Mon, 26 Sep 2016 14:02:53 -0400 Subject: [PATCH 02/17] Map due_date --- bugwarrior/services/redmine.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index e83159c5d..e5d1ed832 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -65,8 +65,12 @@ class RedMineIssue(Issue): } def to_taskwarrior(self): + duedate = self.record.get('due_date') + if duedate: + duedate = self.parse_date(duedate) return { 'project': self.get_project_name(), + 'due': duedate, 'priority': self.get_priority(), self.URL: self.get_issue_url(), From 2fbc89c13da2137cc2fd6170cb7834c761887a9d Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Mon, 3 Oct 2016 14:03:53 -0400 Subject: [PATCH 03/17] Add description field, convert ID to numeric --- bugwarrior/services/redmine.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index e5d1ed832..9265838c3 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -39,6 +39,7 @@ class RedMineIssue(Issue): URL = 'redmineurl' SUBJECT = 'redminesubject' ID = 'redmineid' + DESCRIPTION = 'redminedescription' UDAS = { URL: { @@ -50,9 +51,13 @@ class RedMineIssue(Issue): 'label': 'Redmine Subject', }, ID: { - 'type': 'string', + 'type': 'numeric', 'label': 'Redmine ID', }, + DESCRIPTION: { + 'type': 'string', + 'label': 'Redmine Description', + }, } UNIQUE_KEY = (URL, ) @@ -76,6 +81,8 @@ def to_taskwarrior(self): self.URL: self.get_issue_url(), self.SUBJECT: self.record['subject'], self.ID: self.record['id'] + self.ID: self.record['id'], + self.DESCRIPTION: self.record['description'], } def get_priority(self): From cd4eae1c8f9239871339d3cac671b27c6b9f7575 Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Mon, 3 Oct 2016 14:52:23 -0400 Subject: [PATCH 04/17] Add more core fields --- bugwarrior/services/redmine.py | 69 +++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index 9265838c3..5d421d85c 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -40,6 +40,15 @@ class RedMineIssue(Issue): SUBJECT = 'redminesubject' ID = 'redmineid' DESCRIPTION = 'redminedescription' + TRACKER = 'redminetracker' + STATUS = 'redminestatus' + AUTHOR = 'redmineauthor' + CATEGORY = 'redminecategory' + START_DATE = 'redminestartdate' + SPENT_HOURS = 'redminespenthours' + ESTIMATED_HOURS = 'redmineestimatedhours' + CREATED_ON = 'redminecreatedon' + UPDATED_ON = 'redmineupdatedon' UDAS = { URL: { @@ -58,8 +67,45 @@ class RedMineIssue(Issue): 'type': 'string', 'label': 'Redmine Description', }, + TRACKER: { + 'type': 'string', + 'label': 'Redmine Tracker', + }, + STATUS: { + 'type': 'string', + 'label': 'Redmine Status', + }, + AUTHOR: { + 'type': 'string', + 'label': 'Redmine Author', + }, + CATEGORY: { + 'type': 'string', + 'label': 'Redmine Category', + }, + START_DATE: { + 'type': 'date', + 'label': 'Redmine Start Date', + }, + SPENT_HOURS: { + 'type': 'duration', + 'label': 'Redmine Spent Hours', + }, + ESTIMATED_HOURS: { + 'type': 'duration', + 'label': 'Redmine Estimated Hours', + }, + CREATED_ON: { + 'type': 'date', + 'label': 'Redmine Created On', + }, + UPDATED_ON: { + 'type': 'date', + 'label': 'Redmine Updated On', + }, + } - UNIQUE_KEY = (URL, ) + UNIQUE_KEY = (ID, ) PRIORITY_MAP = { 'Low': 'L', @@ -73,16 +119,35 @@ def to_taskwarrior(self): duedate = self.record.get('due_date') if duedate: duedate = self.parse_date(duedate) + spent_hours = self.record.get('spent_hours') + if spent_hours: + spent_hours = str(spent_hours) + ' hours' + estimated_hours = self.record.get('estimated_hours') + if estimated_hours: + estimated_hours = str(estimated_hours) + ' hours' + category = self.record.get('category') + if category: + category = category['name'] + return { 'project': self.get_project_name(), 'due': duedate, + 'annotations': self.extra.get('annotations', []), 'priority': self.get_priority(), self.URL: self.get_issue_url(), self.SUBJECT: self.record['subject'], - self.ID: self.record['id'] self.ID: self.record['id'], self.DESCRIPTION: self.record['description'], + self.TRACKER: self.record['tracker']['name'], + self.STATUS: self.record['status']['name'], + self.AUTHOR: self.record['author']['name'], + self.CATEGORY: category, + self.START_DATE: self.record['start_date'], + self.CREATED_ON: self.record['created_on'], + self.UPDATED_ON: self.record['updated_on'], + self.ESTIMATED_HOURS: estimated_hours, + self.SPENT_HOURS: spent_hours, } def get_priority(self): From f033927b70e405798b9b6f564fafb9ef59b03807 Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Tue, 4 Oct 2016 12:02:06 -0400 Subject: [PATCH 05/17] Remove unneeded user_id, add Assigned To field --- bugwarrior/services/redmine.py | 25 ++++++++++++++++--------- tests/test_redmine.py | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index 5d421d85c..b03f89583 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -15,12 +15,12 @@ def __init__(self, url, key, auth, issue_limit): self.auth = auth self.issue_limit = issue_limit - def find_issues(self, user_id=None, issue_limit=25): + def find_issues(self, issue_limit=100): args = {} - if user_id is not None: - args["assigned_to_id"] = user_id if issue_limit is not None: args["limit"] = issue_limit + + args["assigned_to_id"] = 'me' return self.call_api("/issues.json", args)["issues"] def call_api(self, uri, params): @@ -49,6 +49,7 @@ class RedMineIssue(Issue): ESTIMATED_HOURS = 'redmineestimatedhours' CREATED_ON = 'redminecreatedon' UPDATED_ON = 'redmineupdatedon' + ASSIGNED_TO = 'redmineassignedto' UDAS = { URL: { @@ -103,6 +104,10 @@ class RedMineIssue(Issue): 'type': 'date', 'label': 'Redmine Updated On', }, + ASSIGNED_TO: { + 'type': 'string', + 'label': 'Redmine Assigned To', + }, } UNIQUE_KEY = (ID, ) @@ -128,6 +133,9 @@ def to_taskwarrior(self): category = self.record.get('category') if category: category = category['name'] + assigned_to = self.record.get('assigned_to') + if assigned_to: + assigned_to = assigned_to['name'] return { 'project': self.get_project_name(), @@ -142,6 +150,7 @@ def to_taskwarrior(self): self.TRACKER: self.record['tracker']['name'], self.STATUS: self.record['status']['name'], self.AUTHOR: self.record['author']['name'], + self.ASSIGNED_TO: assigned_to, self.CATEGORY: category, self.START_DATE: self.record['start_date'], self.CREATED_ON: self.record['created_on'], @@ -184,7 +193,6 @@ def __init__(self, *args, **kw): self.url = self.config_get('url').rstrip("/") self.key = self.config_get('key') - self.user_id = self.config_get('user_id') self.issue_limit = self.config_get('issue_limit') login = self.config_get_default('login') @@ -205,20 +213,19 @@ def get_service_metadata(self): def get_keyring_service(cls, config, section): url = config.get(section, cls._get_key('url')) login = config.get(section, cls._get_key('login')) - user_id = config.get(section, cls._get_key('user_id')) - return "redmine://%s@%s/%s" % (login, url, user_id) + return "redmine://%s@%s/%s" % (login, url) @classmethod def validate_config(cls, config, target): - for k in ('redmine.url', 'redmine.key', 'redmine.user_id'): + for k in ('redmine.url', 'redmine.key'): if not config.has_option(target, k): die("[%s] has no '%s'" % (target, k)) IssueService.validate_config(config, target) def issues(self): - issues = self.client.find_issues(self.user_id, self.issue_limit) - log.debug(" Found %i total.", len(issues)) + issues = self.client.find_issues(self.issue_limit) + log.debug(" Found %i total.", len(issues)) for issue in issues: yield self.get_issue_for_record(issue) diff --git a/tests/test_redmine.py b/tests/test_redmine.py index 85852b372..5c7388518 100644 --- a/tests/test_redmine.py +++ b/tests/test_redmine.py @@ -11,7 +11,7 @@ class TestRedmineIssue(AbstractServiceTest, ServiceTest): SERVICE_CONFIG = { 'redmine.url': 'https://something', 'redmine.key': 'something_else', - 'redmine.user_id': '10834u0234', + 'redmine.issue_limit': '100', } arbitrary_issue = { "assigned_to": { From ca03f8db45b649ac80b3bc4799f0afb2133d3751 Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Mon, 19 Dec 2016 17:27:02 -0500 Subject: [PATCH 06/17] Better date/time handling for created, updated, due --- bugwarrior/services/redmine.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index b03f89583..0214ad947 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -122,18 +122,25 @@ class RedMineIssue(Issue): def to_taskwarrior(self): duedate = self.record.get('due_date') - if duedate: - duedate = self.parse_date(duedate) + updated_on = self.record_get('updated_on') + created_on = self.record_get('created_on') spent_hours = self.record.get('spent_hours') + estimated_hours = self.record.get('estimated_hours') + category = self.record.get('category') + assigned_to = self.record.get('assigned_to') + + if duedate: + duedate = self.parse_date(duedate).replace(microsecond=0) + if updated_on: + updated_on = self.parse_date(updated_on).replace(microseconds=0) + if created_on: + created_on = self.parse_date(created_on).replace(microseconds=0) if spent_hours: spent_hours = str(spent_hours) + ' hours' - estimated_hours = self.record.get('estimated_hours') if estimated_hours: estimated_hours = str(estimated_hours) + ' hours' - category = self.record.get('category') if category: category = category['name'] - assigned_to = self.record.get('assigned_to') if assigned_to: assigned_to = assigned_to['name'] @@ -142,7 +149,6 @@ def to_taskwarrior(self): 'due': duedate, 'annotations': self.extra.get('annotations', []), 'priority': self.get_priority(), - self.URL: self.get_issue_url(), self.SUBJECT: self.record['subject'], self.ID: self.record['id'], @@ -153,8 +159,8 @@ def to_taskwarrior(self): self.ASSIGNED_TO: assigned_to, self.CATEGORY: category, self.START_DATE: self.record['start_date'], - self.CREATED_ON: self.record['created_on'], - self.UPDATED_ON: self.record['updated_on'], + self.CREATED_ON: created_on, + self.UPDATED_ON: updated_on, self.ESTIMATED_HOURS: estimated_hours, self.SPENT_HOURS: spent_hours, } From 47837358f3ff0af295817efac226307a252c7c55 Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Mon, 19 Dec 2016 17:27:32 -0500 Subject: [PATCH 07/17] Adjust variable name for consistency --- bugwarrior/services/redmine.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index 0214ad947..1b9a17dba 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -121,7 +121,7 @@ class RedMineIssue(Issue): } def to_taskwarrior(self): - duedate = self.record.get('due_date') + due_date = self.record.get('due_date') updated_on = self.record_get('updated_on') created_on = self.record_get('created_on') spent_hours = self.record.get('spent_hours') @@ -129,8 +129,8 @@ def to_taskwarrior(self): category = self.record.get('category') assigned_to = self.record.get('assigned_to') - if duedate: - duedate = self.parse_date(duedate).replace(microsecond=0) + if due_date: + due_date = self.parse_date(due_date).replace(microsecond=0) if updated_on: updated_on = self.parse_date(updated_on).replace(microseconds=0) if created_on: @@ -146,7 +146,7 @@ def to_taskwarrior(self): return { 'project': self.get_project_name(), - 'due': duedate, + 'due': due_date, 'annotations': self.extra.get('annotations', []), 'priority': self.get_priority(), self.URL: self.get_issue_url(), From 7968a93c1c880d4f5f8a0d178bdce15310b78b66 Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Mon, 19 Dec 2016 17:34:21 -0500 Subject: [PATCH 08/17] Add some TODOs --- bugwarrior/services/redmine.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index 1b9a17dba..dba435b2e 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -17,6 +17,8 @@ def __init__(self, url, key, auth, issue_limit): def find_issues(self, issue_limit=100): args = {} + # TODO: if issue_limit is greater than 100, implement pagination to return all issues. + # Leave the implementation of this to the unlucky soul with >100 issues assigned to them. if issue_limit is not None: args["limit"] = issue_limit @@ -146,6 +148,7 @@ def to_taskwarrior(self): return { 'project': self.get_project_name(), + # TODO: Should this be set as a UDA to be consistent with other services? 'due': due_date, 'annotations': self.extra.get('annotations', []), 'priority': self.get_priority(), @@ -179,6 +182,7 @@ def get_issue_url(self): def get_project_name(self): if self.origin['project_name']: return self.origin['project_name'] + # TODO: Implement a fix that will remove whitespace, special chars, etc from the project name. return self.record["project"]["name"] def get_default_description(self): From ab09296447597a2fed263144d5d79194045eaebd Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Mon, 19 Dec 2016 22:56:13 -0500 Subject: [PATCH 09/17] More date handling fixes, use task calc for estimated hours --- bugwarrior/services/redmine.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index dba435b2e..62cbc2a53 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -3,6 +3,7 @@ from bugwarrior.config import die from bugwarrior.services import Issue, IssueService, ServiceClient +from taskw import TaskWarriorShellout import logging log = logging.getLogger(__name__) @@ -124,8 +125,9 @@ class RedMineIssue(Issue): def to_taskwarrior(self): due_date = self.record.get('due_date') - updated_on = self.record_get('updated_on') - created_on = self.record_get('created_on') + start_date = self.record.get('start_date') + updated_on = self.record.get('updated_on') + created_on = self.record.get('created_on') spent_hours = self.record.get('spent_hours') estimated_hours = self.record.get('estimated_hours') category = self.record.get('category') @@ -133,14 +135,17 @@ def to_taskwarrior(self): if due_date: due_date = self.parse_date(due_date).replace(microsecond=0) + if start_date: + start_date = self.parse_date(start_date).replace(microsecond=0) if updated_on: - updated_on = self.parse_date(updated_on).replace(microseconds=0) + updated_on = self.parse_date(updated_on).replace(microsecond=0) if created_on: - created_on = self.parse_date(created_on).replace(microseconds=0) + created_on = self.parse_date(created_on).replace(microsecond=0) if spent_hours: spent_hours = str(spent_hours) + ' hours' if estimated_hours: estimated_hours = str(estimated_hours) + ' hours' + estimated_hours = self.get_converted_hours(estimated_hours) if category: category = category['name'] if assigned_to: @@ -161,7 +166,7 @@ def to_taskwarrior(self): self.AUTHOR: self.record['author']['name'], self.ASSIGNED_TO: assigned_to, self.CATEGORY: category, - self.START_DATE: self.record['start_date'], + self.START_DATE: start_date, self.CREATED_ON: created_on, self.UPDATED_ON: updated_on, self.ESTIMATED_HOURS: estimated_hours, @@ -179,6 +184,13 @@ def get_issue_url(self): self.origin['url'] + "/issues/" + six.text_type(self.record["id"]) ) + def get_converted_hours(self, estimated_hours): + tw = TaskWarriorShellout() + calc = tw._execute('calc', estimated_hours) + return ( + calc[0].rstrip() + ) + def get_project_name(self): if self.origin['project_name']: return self.origin['project_name'] From 959f998a4dc85b7e4e1ae3c59503f975cd6f73d0 Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Mon, 19 Dec 2016 23:03:19 -0500 Subject: [PATCH 10/17] Specify a more realistic limit in the docs --- bugwarrior/docs/services/redmine.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugwarrior/docs/services/redmine.rst b/bugwarrior/docs/services/redmine.rst index f32c20b73..e67576a43 100644 --- a/bugwarrior/docs/services/redmine.rst +++ b/bugwarrior/docs/services/redmine.rst @@ -17,7 +17,7 @@ Here's an example of a Redmine target:: redmine.key = c0c4c014cafebabe redmine.user_id = 7 redmine.project_name = redmine - redmine.issue_limit = 1000 + redmine.issue_limit = 100 You can also feel free to use any of the configuration options described in :ref:`common_configuration_options`. From b899e8349cca8df3a5113713d643f0c91edbfb96 Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Mon, 19 Dec 2016 23:05:51 -0500 Subject: [PATCH 11/17] Also use task calc on spent_hours --- bugwarrior/services/redmine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index 62cbc2a53..6a0a2b6c1 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -143,6 +143,7 @@ def to_taskwarrior(self): created_on = self.parse_date(created_on).replace(microsecond=0) if spent_hours: spent_hours = str(spent_hours) + ' hours' + spent_hours = self.get_converted_hours(spent_hours) if estimated_hours: estimated_hours = str(estimated_hours) + ' hours' estimated_hours = self.get_converted_hours(estimated_hours) From 1581ed709244ac0a37f2aa6e945a31a179fd52bf Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Tue, 20 Dec 2016 09:58:15 -0500 Subject: [PATCH 12/17] Make project name alphanumeric and lowercase --- bugwarrior/services/redmine.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index 6a0a2b6c1..de27eb065 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -1,5 +1,6 @@ import six import requests +import re from bugwarrior.config import die from bugwarrior.services import Issue, IssueService, ServiceClient @@ -195,8 +196,11 @@ def get_converted_hours(self, estimated_hours): def get_project_name(self): if self.origin['project_name']: return self.origin['project_name'] - # TODO: Implement a fix that will remove whitespace, special chars, etc from the project name. - return self.record["project"]["name"] + # TODO: It would be nice to use the project slug (if the Redmine + # instance supports it), but this would require (1) an API call + # to get the list of projects, and then a look up between the + # project ID contained in self.record and the list of projects. + return re.sub(r'\W+', '', self.record["project"]["name"]).lower() def get_default_description(self): return self.build_default_description( From fc3693ee32e1361108c6d48bead1ca4b6275a5fb Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Tue, 3 Jan 2017 08:49:53 -0500 Subject: [PATCH 13/17] Work on tests --- tests/test_redmine.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/test_redmine.py b/tests/test_redmine.py index 5c7388518..10e76d55f 100644 --- a/tests/test_redmine.py +++ b/tests/test_redmine.py @@ -8,6 +8,7 @@ class TestRedmineIssue(AbstractServiceTest, ServiceTest): + maxDiff = None SERVICE_CONFIG = { 'redmine.url': 'https://something', 'redmine.key': 'something_else', @@ -23,6 +24,7 @@ class TestRedmineIssue(AbstractServiceTest, ServiceTest): "name": "Adam Coddington" }, "created_on": "2014-11-19T16:40:29Z", + "due_on": "2016-12-30T16:40:29Z", "description": "This is a test issue.", "done_ratio": 0, "id": 363901, @@ -32,7 +34,7 @@ class TestRedmineIssue(AbstractServiceTest, ServiceTest): }, "project": { "id": 27375, - "name": "Bugwarrior" + "name": "Boiled Cabbage - Yum" }, "status": { "id": 1, @@ -56,9 +58,11 @@ def test_to_taskwarrior(self): issue = self.service.get_issue_for_record(self.arbitrary_issue) expected_output = { - 'project': self.arbitrary_issue['project']['name'], + 'project': issue.get_project_name(), 'priority': self.service.default_priority, + issue.ASSIGNED_TO: self.arbitrary_issue['assigned_to']['name'], + issue.AUTHOR: self.arbitrary_issue['author']['name'], issue.URL: arbitrary_url, issue.SUBJECT: self.arbitrary_issue['subject'], issue.ID: self.arbitrary_issue['id'], @@ -84,9 +88,12 @@ def test_issues(self): 'description': u'(bw)Is#363901 - Biscuits .. https://something/issues/363901', 'priority': 'M', - 'project': u'Bugwarrior', + 'project': u'boiledcabbageyum', 'redmineid': 363901, + 'redmineassignedto': 'Adam Coddington', + 'redmineauthor': 'Adam Coddington', 'redminesubject': u'Biscuits', + 'redminetracker': u'Task', 'redmineurl': u'https://something/issues/363901', 'tags': []} From 516961b05394b958072e6a37e91d21ce44a05a91 Mon Sep 17 00:00:00 2001 From: Kosta Harlan Date: Tue, 3 Jan 2017 08:50:02 -0500 Subject: [PATCH 14/17] Improved formatting of project names --- bugwarrior/services/redmine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index de27eb065..5e044a56d 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -200,7 +200,7 @@ def get_project_name(self): # instance supports it), but this would require (1) an API call # to get the list of projects, and then a look up between the # project ID contained in self.record and the list of projects. - return re.sub(r'\W+', '', self.record["project"]["name"]).lower() + return re.sub(r'[^a-zA-Z0-9]', '', self.record["project"]["name"]).lower() def get_default_description(self): return self.build_default_description( From 69e63154ef453fec9163454faec92da51c33897e Mon Sep 17 00:00:00 2001 From: ryneeverett Date: Fri, 6 Jan 2017 22:06:07 -0500 Subject: [PATCH 15/17] Update redmine tests. --- tests/test_redmine.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/test_redmine.py b/tests/test_redmine.py index 10e76d55f..7ed89417a 100644 --- a/tests/test_redmine.py +++ b/tests/test_redmine.py @@ -1,5 +1,8 @@ from builtins import next +import datetime + import mock +import dateutil import responses from bugwarrior.services.redmine import RedMineService @@ -14,6 +17,10 @@ class TestRedmineIssue(AbstractServiceTest, ServiceTest): 'redmine.key': 'something_else', 'redmine.issue_limit': '100', } + arbitrary_created = datetime.datetime.utcnow().replace( + tzinfo=dateutil.tz.tz.tzutc(), microsecond=0) - datetime.timedelta(1) + arbitrary_updated = datetime.datetime.utcnow().replace( + tzinfo=dateutil.tz.tz.tzutc(), microsecond=0) arbitrary_issue = { "assigned_to": { "id": 35546, @@ -23,7 +30,7 @@ class TestRedmineIssue(AbstractServiceTest, ServiceTest): "id": 35546, "name": "Adam Coddington" }, - "created_on": "2014-11-19T16:40:29Z", + "created_on": arbitrary_created.isoformat(), "due_on": "2016-12-30T16:40:29Z", "description": "This is a test issue.", "done_ratio": 0, @@ -45,7 +52,7 @@ class TestRedmineIssue(AbstractServiceTest, ServiceTest): "id": 4, "name": "Task" }, - "updated_on": "2014-11-19T16:40:29Z" + "updated_on": arbitrary_updated.isoformat(), } def setUp(self): @@ -58,14 +65,25 @@ def test_to_taskwarrior(self): issue = self.service.get_issue_for_record(self.arbitrary_issue) expected_output = { + 'annotations': [], + 'due': None, 'project': issue.get_project_name(), 'priority': self.service.default_priority, issue.ASSIGNED_TO: self.arbitrary_issue['assigned_to']['name'], issue.AUTHOR: self.arbitrary_issue['author']['name'], + issue.CATEGORY: None, + issue.DESCRIPTION: self.arbitrary_issue['description'], + issue.ESTIMATED_HOURS: None, + issue.STATUS: 'New', issue.URL: arbitrary_url, issue.SUBJECT: self.arbitrary_issue['subject'], + issue.TRACKER: u'Task', + issue.CREATED_ON: self.arbitrary_created, + issue.UPDATED_ON: self.arbitrary_updated, issue.ID: self.arbitrary_issue['id'], + issue.SPENT_HOURS: None, + issue.START_DATE: None, } def get_url(*args): @@ -79,21 +97,31 @@ def get_url(*args): @responses.activate def test_issues(self): self.add_response( - 'https://something/issues.json?assigned_to_id=10834u0234&limit=100', + 'https://something/issues.json?assigned_to_id=me&limit=100', json={'issues': [self.arbitrary_issue]}) issue = next(self.service.issues()) expected = { + 'annotations': [], + 'due': None, 'description': u'(bw)Is#363901 - Biscuits .. https://something/issues/363901', 'priority': 'M', 'project': u'boiledcabbageyum', 'redmineid': 363901, + issue.SPENT_HOURS: None, + issue.START_DATE: None, 'redmineassignedto': 'Adam Coddington', 'redmineauthor': 'Adam Coddington', + issue.CATEGORY: None, + issue.DESCRIPTION: self.arbitrary_issue['description'], + issue.ESTIMATED_HOURS: None, + issue.STATUS: 'New', 'redminesubject': u'Biscuits', 'redminetracker': u'Task', + issue.CREATED_ON: self.arbitrary_created, + issue.UPDATED_ON: self.arbitrary_updated, 'redmineurl': u'https://something/issues/363901', 'tags': []} From c9e07229f8d841feba048f4f4b88013e4da707ee Mon Sep 17 00:00:00 2001 From: ryneeverett Date: Wed, 11 Jan 2017 09:13:06 -0500 Subject: [PATCH 16/17] redmine: Respect only_if_assigned configuration. self.issue_limit = issue_limit - def find_issues(self, issue_limit=100): + def find_issues(self, issue_limit=100, only_if_assigned=False): args = {} # TODO: if issue_limit is greater than 100, implement pagination to return all issues. # Leave the implementation of this to the unlucky soul with >100 issues assigned to them. if issue_limit is not None: args["limit"] = issue_limit - args["assigned_to_id"] = 'me' + if only_if_assigned: + args["assigned_to_id"] = 'me' return self.call_api("/issues.json", args)["issues"] def call_api(self, uri, params): @@ -251,8 +252,8 @@ class RedMineService(IssueService): IssueService.validate_config(config, target) def issues(self): - - issues = self.client.find_issues(self.issue_limit) + only_if_assigned = self.config_get_default('only_if_assigned', False) + issues = self.client.find_issues(self.issue_limit, only_if_assigned=only_if_assigned) log.debug(" Found %i total.", len(issues)) for issue in issues: yield self.get_issue_for_record(issue) diff --git a/tests/test_redmine.py b/tests/test_redmine.py index 7ed8941..8b1fe64 100644 --- a/tests/test_redmine.py +++ b/tests/test_redmine.py @@ -97,7 +97,7 @@ class TestRedmineIssue(AbstractServiceTest, ServiceTest): @responses.activate def test_issues(self): self.add_response( - 'https://something/issues.json?assigned_to_id=me&limit=100', + 'https://something/issues.json?limit=100', json={'issues': [self.arbitrary_issue]}) issue = next(self.service.issues()) --- bugwarrior/services/redmine.py | 9 +++++---- tests/test_redmine.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index 5e044a56d..42b05a8ee 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -17,14 +17,15 @@ def __init__(self, url, key, auth, issue_limit): self.auth = auth self.issue_limit = issue_limit - def find_issues(self, issue_limit=100): + def find_issues(self, issue_limit=100, only_if_assigned=False): args = {} # TODO: if issue_limit is greater than 100, implement pagination to return all issues. # Leave the implementation of this to the unlucky soul with >100 issues assigned to them. if issue_limit is not None: args["limit"] = issue_limit - args["assigned_to_id"] = 'me' + if only_if_assigned: + args["assigned_to_id"] = 'me' return self.call_api("/issues.json", args)["issues"] def call_api(self, uri, params): @@ -251,8 +252,8 @@ def validate_config(cls, config, target): IssueService.validate_config(config, target) def issues(self): - - issues = self.client.find_issues(self.issue_limit) + only_if_assigned = self.config_get_default('only_if_assigned', False) + issues = self.client.find_issues(self.issue_limit, only_if_assigned) log.debug(" Found %i total.", len(issues)) for issue in issues: yield self.get_issue_for_record(issue) diff --git a/tests/test_redmine.py b/tests/test_redmine.py index 7ed89417a..8b1fe6446 100644 --- a/tests/test_redmine.py +++ b/tests/test_redmine.py @@ -97,7 +97,7 @@ def get_url(*args): @responses.activate def test_issues(self): self.add_response( - 'https://something/issues.json?assigned_to_id=me&limit=100', + 'https://something/issues.json?limit=100', json={'issues': [self.arbitrary_issue]}) issue = next(self.service.issues()) From 7ba17253651e903e60416f5dfa092c9be9075490 Mon Sep 17 00:00:00 2001 From: ryneeverett Date: Wed, 11 Jan 2017 09:38:24 -0500 Subject: [PATCH 17/17] redmine: Create redmineduedate UDA. --- bugwarrior/services/redmine.py | 8 ++++++-- tests/test_redmine.py | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/bugwarrior/services/redmine.py b/bugwarrior/services/redmine.py index 42b05a8ee..54e06201c 100644 --- a/bugwarrior/services/redmine.py +++ b/bugwarrior/services/redmine.py @@ -54,6 +54,7 @@ class RedMineIssue(Issue): ESTIMATED_HOURS = 'redmineestimatedhours' CREATED_ON = 'redminecreatedon' UPDATED_ON = 'redmineupdatedon' + DUEDATE = 'redmineduedate' ASSIGNED_TO = 'redmineassignedto' UDAS = { @@ -109,6 +110,10 @@ class RedMineIssue(Issue): 'type': 'date', 'label': 'Redmine Updated On', }, + DUEDATE: { + 'type': 'date', + 'label': 'Redmine Due Date' + }, ASSIGNED_TO: { 'type': 'string', 'label': 'Redmine Assigned To', @@ -156,8 +161,6 @@ def to_taskwarrior(self): return { 'project': self.get_project_name(), - # TODO: Should this be set as a UDA to be consistent with other services? - 'due': due_date, 'annotations': self.extra.get('annotations', []), 'priority': self.get_priority(), self.URL: self.get_issue_url(), @@ -172,6 +175,7 @@ def to_taskwarrior(self): self.START_DATE: start_date, self.CREATED_ON: created_on, self.UPDATED_ON: updated_on, + self.DUEDATE: due_date, self.ESTIMATED_HOURS: estimated_hours, self.SPENT_HOURS: spent_hours, } diff --git a/tests/test_redmine.py b/tests/test_redmine.py index 8b1fe6446..9fb9babe1 100644 --- a/tests/test_redmine.py +++ b/tests/test_redmine.py @@ -66,10 +66,9 @@ def test_to_taskwarrior(self): expected_output = { 'annotations': [], - 'due': None, 'project': issue.get_project_name(), 'priority': self.service.default_priority, - + issue.DUEDATE: None, issue.ASSIGNED_TO: self.arbitrary_issue['assigned_to']['name'], issue.AUTHOR: self.arbitrary_issue['author']['name'], issue.CATEGORY: None, @@ -104,7 +103,7 @@ def test_issues(self): expected = { 'annotations': [], - 'due': None, + issue.DUEDATE: None, 'description': u'(bw)Is#363901 - Biscuits .. https://something/issues/363901', 'priority': 'M',