From 53b0dc8fe9fb26686ef6041ca8e8469673e9684a Mon Sep 17 00:00:00 2001 From: Peter Sanchez Date: Thu, 30 Sep 2021 17:42:02 -0700 Subject: [PATCH] Many improvements to include more mapping options, attachments, checklists, assignees, watchers, etc. --- trello2redmine.py | 739 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 576 insertions(+), 163 deletions(-) diff --git a/trello2redmine.py b/trello2redmine.py index 87b4233..da9954b 100755 --- a/trello2redmine.py +++ b/trello2redmine.py @@ -1,248 +1,661 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import trello2redmine_config as cfg - -import sys import json +import os +import re +import sys + import requests + +import trello2redmine_config as cfg + try: from urlparse import urlparse except ImportError: # python 3 from urllib.parse import urlparse -if len(sys.argv) > 3 or (len(sys.argv) == 2 and sys.argv[1] == '-h'): - print('Usage: {0} [])'.format(sys.argv[0])) - print('See %s for configuration.' % cfg.__file__) - print('Options:') - print(' -h Displays this information.') - print(' -c Commits the import. Otherwise does a dry run (prints resulting JSON to screen instead of sending it to Redmine).') - sys.exit(0) +if len(sys.argv) > 3 or (len(sys.argv) == 2 and sys.argv[1] == "-h"): + print("Usage: {0} [])".format(sys.argv[0])) + print("See %s for configuration." % cfg.__file__) + print("Options:") + print(" -h Displays this information.") + print( + " -c Commits the import. Otherwise does a dry run (prints resulting JSON to screen instead of sending it to Redmine)." + ) + sys.exit(0) # If a dry run, JSON is printed instead of submitted to Redmine. -#dry_run = len(sys.argv) < 2 or sys.argv[2] != '-c' -dry_run = False +dry_run = len(sys.argv) == 2 and sys.argv[1] == "-c" +dry_run = True if dry_run: - print('Making a dry run! Re-run with -c to commit the import into Redmine.') + print("Making a dry run! Re-run with -c to commit the import into Redmine.") else: - print('Making a commit!') + print("Making a commit!") url = urlparse(cfg.trello_board_link) -if not url.netloc.endswith('trello.com'): - print('URL does not seem to be a Trello board link. Are you sure this is correct?') - sys.exit(1) +if not url.netloc.endswith("trello.com"): + print("URL does not seem to be a Trello board link. Are you sure this is correct?") + sys.exit(1) # ============================================================================ # Trello JSON processing starts here. # ============================================================================ -print('Downloading board JSON from Trello...') +print("Downloading board JSON from Trello...") sys.stdout.flush() board = json.loads(open(sys.argv[1]).read()) -#board = requests.get(cfg.trello_board_link + '.json').json() +# board = requests.get(cfg.trello_board_link + '.json').json() -print('Processing board JSON...') +print("Processing board JSON...") sys.stdout.flush() +no_attachment_list = [] +list_status_dict = {} orig_lists_dict = {} lists_dict = {} for l in board["lists"]: - name = l["name"] - if name in cfg.list_map: - name = cfg.list_map[name] - lists_dict[l["id"]] = name - orig_lists_dict[l["id"]] = l["name"] - + name = l["name"] + if name in cfg.list_map: + name = cfg.list_map[name] + lists_dict[l["id"]] = name + orig_lists_dict[l["id"]] = l["name"] + list_status_dict[l["id"]] = l["closed"] + + if l["name"] in cfg.no_file_import: + no_attachment_list.append(l["id"]) + checklists_dict = {} for c in board["checklists"]: - checklists_dict[c["id"]] = c["checkItems"] + checklists_dict[c["id"]] = c["checkItems"] members_dict = {} for m in board["members"]: - name = m["fullName"] - if name in cfg.username_map: - name = cfg.username_map[name] - members_dict[m["id"]] = name + name = m["fullName"] + if name in cfg.username_map: + name = cfg.username_map[name] + members_dict[m["id"]] = name labels_dict = {} for l in board["labels"]: - label = l["name"] - if label in cfg.label_map: - label = cfg.label_map[label] - labels_dict[l["id"]] = label + label = l["name"] + if label in cfg.label_map: + label = cfg.label_map[label] + labels_dict[l["id"]] = label comments_dict = {} for a in board["actions"]: - if a["type"] == "commentCard": - card_id = a["data"]["card"]["id"] - author_id = a["idMemberCreator"] - comment_list = comments_dict[card_id] if card_id in comments_dict else [] - comment_list.append({ - "author": members_dict[author_id] if author_id in members_dict else '', - "text": a["data"]["text"], - }) - comments_dict[card_id] = comment_list - -#print('Members:\n' + str(members_dict)) -#print('Lists:\n' + str(lists_dict)) -#print('Checklists:\n' + str(checklists_dict)) -#print('Comments:\n' + str(comments_dict)) + if a["type"] == "commentCard": + card_id = a["data"]["card"]["id"] + author_id = a["idMemberCreator"] + comment_list = comments_dict[card_id] if card_id in comments_dict else [] + comment_list.append( + { + "author": members_dict[author_id] if author_id in members_dict else "", + "text": a["data"]["text"], + } + ) + comments_dict[card_id] = comment_list + +# print('Members:\n' + str(members_dict)) +# print('Lists:\n' + str(lists_dict)) +# print('Checklists:\n' + str(checklists_dict)) +# print('Comments:\n' + str(comments_dict)) # ============================================================================ # Redmine configuration processing starts here. # ============================================================================ -print('Querying Redmine configuration...') +print("Querying Redmine configuration...") sys.stdout.flush() -redmine_projects = requests.get(cfg.redmine_root_url + '/projects.json?limit=200', verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key}).json() +fname = f"json/{cfg.redmine_project_identifier}-projects.json" +if not os.path.exists(fname): + redmine_projects = requests.get( + cfg.redmine_root_url + "/projects.json?limit=200", + verify=cfg.redmine_verify_certificates, + headers={"X-Redmine-API-Key": cfg.redmine_api_key}, + ).json() + with open(fname, "w") as fp: + json.dump(redmine_projects, fp) +else: + redmine_projects = json.load(open(fname)) + redmine_project_id = -1 for rp in redmine_projects["projects"]: - if rp["identifier"] == cfg.redmine_project_identifier: - redmine_project_id = rp["id"] - break + if rp["identifier"] == cfg.redmine_project_identifier: + redmine_project_id = rp["id"] + break if redmine_project_id < 0: - print('Project with identifier {0} does not exist in Redmine!\n\n{1}'.format(cfg.redmine_project_identifier, str(redmine_users_dict))) - sys.exit(1) -#print('Redmine project ID: {0}'.format(redmine_project_id)) + print( + "Project with identifier {0} does not exist in Redmine!\n\n".format( + cfg.redmine_project_identifier + ) + ) + sys.exit(1) +# print('Redmine project ID: {0}'.format(redmine_project_id)) + +fname = f"json/{cfg.redmine_project_identifier}-users.json" +if not os.path.exists(fname): + redmine_users = requests.get( + cfg.redmine_root_url + "/users.json?limit=200", + verify=cfg.redmine_verify_certificates, + headers={"X-Redmine-API-Key": cfg.redmine_api_key}, + ).json() + with open(fname, "w") as fp: + json.dump(redmine_users, fp) +else: + redmine_users = json.load(open(fname)) -# offset 2676 ensures 'Oliver Kurz' shows up -redmine_users = requests.get(cfg.redmine_root_url + '/users.json?offset=2676', verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key}).json() redmine_users_dict = {} for ru in redmine_users["users"]: - fullname = u'{0} {1}'.format(ru["firstname"], ru["lastname"]) - redmine_users_dict[fullname] = ru["id"] - -print('Redmine users:\n' + str(redmine_users_dict)) - -if not cfg.redmine_default_user in redmine_users_dict: - print('Default user does not exist in Redmine!\n\n{0}'.format(str(redmine_users_dict))) - sys.exit(1) + fullname = "{0} {1}".format(ru["firstname"], ru["lastname"]) + redmine_users_dict[fullname] = ru["id"] + +print("Redmine users:\n" + str(redmine_users_dict)) + +if cfg.redmine_default_user not in redmine_users_dict: + print( + "Default user does not exist in Redmine!\n\n{0}".format(str(redmine_users_dict)) + ) + sys.exit(1) + +fname = f"json/{cfg.redmine_project_identifier}-priorities.json" +if not os.path.exists(fname): + redmine_priorities = requests.get( + cfg.redmine_root_url + "/enumerations/issue_priorities.json", + verify=cfg.redmine_verify_certificates, + headers={"X-Redmine-API-Key": cfg.redmine_api_key}, + ).json() + with open(fname, "w") as fp: + json.dump(redmine_priorities, fp) +else: + redmine_priorities = json.load(open(fname)) -redmine_priorities = requests.get(cfg.redmine_root_url + '/enumerations/issue_priorities.json', verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key}).json() redmine_priorities_dict = {} redmine_default_priority = "Normal" for rp in redmine_priorities["issue_priorities"]: - redmine_priorities_dict[rp["name"]] = rp["id"] - if "is_default" in rp and rp["is_default"]: - redmine_default_priority = rp["name"] - -#print(u'Redmine priorities:\n' + str(redmine_priorities_dict)) + redmine_priorities_dict[rp["name"]] = rp["id"] + if "is_default" in rp and rp["is_default"]: + redmine_default_priority = rp["name"] + +# print(u'Redmine priorities:\n' + str(redmine_priorities_dict)) + +fname = f"json/{cfg.redmine_project_identifier}-trackers.json" +if not os.path.exists(fname): + redmine_trackers = requests.get( + cfg.redmine_root_url + "/trackers.json", + verify=cfg.redmine_verify_certificates, + headers={"X-Redmine-API-Key": cfg.redmine_api_key}, + ).json() + with open(fname, "w") as fp: + json.dump(redmine_trackers, fp) +else: + redmine_trackers = json.load(open(fname)) + +redmine_tracker_dict = {} +redmine_default_tracker = "Task" +for ts in redmine_trackers["trackers"]: + redmine_tracker_dict[ts["name"]] = ts["id"] + +fname = f"json/{cfg.redmine_project_identifier}-statuses.json" +if not os.path.exists(fname): + redmine_statuses = requests.get( + cfg.redmine_root_url + "/issue_statuses.json", + verify=cfg.redmine_verify_certificates, + headers={"X-Redmine-API-Key": cfg.redmine_api_key}, + ).json() + with open(fname, "w") as fp: + json.dump(redmine_statuses, fp) +else: + redmine_statuses = json.load(open(fname)) -redmine_statuses = requests.get(cfg.redmine_root_url + '/issue_statuses.json', verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key}).json() redmine_statuses_dict = {} redmine_default_status = "New" for rs in redmine_statuses["issue_statuses"]: - redmine_statuses_dict[rs["name"]] = rs["id"] - if "is_default" in rs and rs["is_default"]: - redmine_default_status = rs["name"] - -#print(u'Redmine statuses:\n' + str(redmine_priorities_dict)) + redmine_statuses_dict[rs["name"]] = rs["id"] + if "is_default" in rs and rs["is_default"]: + redmine_default_status = rs["name"] + + +# GET VERSIONS +def get_redmine_versions(clean=False): + fname = f"json/{cfg.redmine_project_identifier}-versions.json" + if clean: + try: + os.unlink(fname) + except FileNotFoundError: + pass + + if not os.path.exists(fname): + redmine_versions = requests.get( + cfg.redmine_root_url + f"/projects/{redmine_project_id}/versions.json", + verify=cfg.redmine_verify_certificates, + headers={"X-Redmine-API-Key": cfg.redmine_api_key}, + ).json() + with open(fname, "w") as fp: + json.dump(redmine_versions, fp) + else: + redmine_versions = json.load(open(fname)) + + return redmine_versions + + +redmine_versions = get_redmine_versions() + +i_fname = f"json/{cfg.redmine_project_identifier}-import-history.json" +if not os.path.exists(i_fname): + import_history = [] +else: + import_history = json.load(open(i_fname)) + + +def write_import_history(): + if dry_run: + return + + with open(i_fname, "w") as fp: + json.dump(import_history, fp) + + +# print(u'Redmine statuses:\n' + str(redmine_priorities_dict)) # ============================================================================ # Direct Trello-to-Redmine mappings are made here. # ============================================================================ -print('Generating configuration mappings...') +print("Generating configuration mappings...") sys.stdout.flush() user_map = {} -for id, fullname in members_dict.iteritems(): - if not fullname in redmine_users_dict: - print(u'Warning: user {0} not found in Redmine, defaulting to {1}'.format(fullname, cfg.redmine_default_user)) - fullname = cfg.redmine_default_user - redmine_id = redmine_users_dict[fullname] - user_map[id] = redmine_id - print(u'Matched user {0}, Trello ID {1}, Redmine ID {2}'.format(fullname, id, redmine_id).encode('utf-8')) - -print(u'User ID map:\n{0}\nDefault user ID: {1}'.format(str(user_map), redmine_users_dict[cfg.redmine_default_user]).encode('utf-8')) +for id, fullname in members_dict.items(): + if fullname not in redmine_users_dict: + print( + "Warning: user {0} not found in Redmine, defaulting to {1}".format( + fullname, cfg.redmine_default_user + ) + ) + fullname = cfg.redmine_default_user + redmine_id = redmine_users_dict[fullname] + user_map[id] = redmine_id + print( + "Matched user {0}, Trello ID {1}, Redmine ID {2}".format( + fullname, id, redmine_id + ).encode("utf-8") + ) + +print( + "User ID map:\n{0}\nDefault user ID: {1}".format( + str(user_map), redmine_users_dict[cfg.redmine_default_user] + ).encode("utf-8") +) priority_map = {} -for id, name in labels_dict.iteritems(): - if not name in redmine_priorities_dict: - print(u'Warning: Trello label {0} is not mapped to a Redmine priority, defaulting to {1}'.format(name, redmine_default_priority).encode('utf-8')) - name = redmine_default_priority - redmine_id = redmine_priorities_dict[name] - priority_map[id] = redmine_id - #print(u'Matched label {0}, Trello ID {1}, Redmine ID {2}'.format(name, id, redmine_id).encode('utf-8')) - -#print('Redmine priorities:\n' + str(redmine_priorities_dict)) +for id, name in labels_dict.items(): + if name not in redmine_priorities_dict: + name = redmine_default_priority + redmine_id = redmine_priorities_dict[name] + priority_map[id] = redmine_id + # print(u'Matched label {0}, Trello ID {1}, Redmine ID {2}'.format(name, id, redmine_id).encode('utf-8')) + +# print('Redmine priorities:\n' + str(redmine_priorities_dict)) + +tracker_map = {} +for id, name in labels_dict.items(): + if name not in redmine_tracker_dict: + name = redmine_default_tracker + redmine_id = redmine_tracker_dict[name] + tracker_map[id] = redmine_id status_map = {} -for id, name in lists_dict.iteritems(): - if not name in redmine_statuses_dict: - print(u'Warning: Trello list {0} is not mapped to a Redmine status, defaulting to {1}'.format(name, redmine_default_status).encode('utf-8')) - name = redmine_default_status - redmine_id = redmine_statuses_dict[name] - status_map[id] = redmine_id - #print(u'Matched list {0}, Trello ID {1}, Redmine ID {2}'.format(name, id, redmine_id).encode('utf-8')) - -#print('Redmine statuses:\n' + str(redmine_statuses_dict)) +for id, name in lists_dict.items(): + if name not in redmine_statuses_dict: + if not list_status_dict[id]: + print( + "Warning: Trello list {0} is not mapped to a Redmine status, defaulting to {1}".format( + name, redmine_default_status + ).encode( + "utf-8" + ) + ) + name = redmine_default_status + redmine_id = redmine_statuses_dict[name] + status_map[id] = redmine_id + # print(u'Matched list {0}, Trello ID {1}, Redmine ID {2}'.format(name, id, redmine_id).encode('utf-8')) + +if cfg.closed_list_status not in redmine_statuses_dict: + print( + f"Warning: 'closed_list_status' value not found in Redmine statuses. Defaulting to {redmine_default_status}" + ) + id = redmine_statuses_dict[redmine_default_status] + redmine_statuses_dict[cfg.closed_list_status] = id + +# print('Redmine statuses:\n' + str(redmine_statuses_dict)) + +refetch_versions = False +version_map = {} +list_version_map = {} +_versions = [v for _, v in cfg.version_map.items()] + +for l_id, l_name in orig_lists_dict.items(): + v_name = l_name.split()[-1].replace("v.1", "v1") + if re.match(r"v\d\.\d\.\d$", v_name) or re.match(r"v\d\.\d$", v_name): + v_name = v_name[1:] # remove leading "v" + _versions.append(v_name) + list_version_map[l_id] = v_name + +_versions = list(set(_versions)) + +for v in redmine_versions["versions"]: + version_map[v["name"]] = v["id"] + +for v in _versions: + if v not in version_map: + refetch_versions = True + version = { + "version": {"name": v}, + } + + if dry_run: + print(f"Creating FAKE VERSION {v}") + version_map[v] = -1 + else: + result = requests.post( + cfg.redmine_root_url + f"/projects/{redmine_project_id}/versions.json", + data=json.dumps(version), + verify=cfg.redmine_verify_certificates, + headers={ + "X-Redmine-API-Key": cfg.redmine_api_key, + "Content-Type": "application/json", + }, + ) + if result.status_code >= 400: + print( + "Error on post {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}".format( + str(result), + result.request.headers, + result.headers, + result.content, + ) + ) + print( + json.dumps( + version, sort_keys=False, indent=4, separators=(",", ": ") + ) + ) + sys.exit(1) + else: + vjson = result.json() + version_map[v] = vjson["version"]["id"] + print(f"Created version '{v}'") + +if refetch_versions: + redmine_versions = get_redmine_versions(clean=True) # ============================================================================ # Finally, cards processing. # ============================================================================ -print ('Processing cards...') +print("Processing cards...") sys.stdout.flush() + +def _set_tracker_or_status(c): + result = {} + if c["idLabels"]: + label_id = c["idLabels"][0] + result["tracker_id"] = tracker_map[label_id] + label_name = labels_dict[label_id] + if label_name.startswith("Status:"): + lbname = label_name[7:] + if lbname in redmine_statuses_dict: + result["status_id"] = redmine_statuses_dict[lbname] + + # List to status and version mapping + if list_status_dict[c["idList"]]: + # List is archived + result["status_id"] = redmine_statuses_dict[cfg.closed_list_status] + + lname = orig_lists_dict[c["idList"]] + if lname in cfg.version_map: + result["fixed_version_id"] = version_map[cfg.version_map[lname]] + elif c["idList"] in list_version_map: + v_name = list_version_map[c["idList"]] + result["fixed_version_id"] = version_map[v_name] + + # User assignee and watchers + a_id = None + w_ids = [] + for mid in c["idMembers"]: + if a_id is None: + a_id = mid + else: + w_ids.append(mid) + + if a_id is not None: + result["assigned_to_id"] = a_id + if len(w_ids): + result["watcher_user_ids"] = w_ids + + return result + + +def handle_attachments(c): + uploads = [] + if not c["attachments"]: + return uploads + + # Don't upload files in specific lists + if c["idList"] in no_attachment_list: + print("Skipping upload for card in prohibited upload list.") + return uploads + + for a in c["attachments"]: + if not a["isUpload"]: + continue + + filename = a["fileName"].strip().replace(" ", "_") + filename = re.sub(r"(?u)[^-\w.]", "", filename) # from django + file_path = f"attachments/{filename}" + upload = { + "filename": filename, + "content_type": a["mimeType"], + } + try: + r = requests.get( + a["url"], + stream=True, + headers={ + "Authorization": f'OAuth oauth_consumer_key="{cfg.trello_apikey}", oauth_token="{cfg.trello_token}"', + }, + ) + except Exception as e: + print(f"Failuring downloading file {a['url']}, error: {e}") + continue + + with open(file_path, "wb") as fp: + for chunk in r.iter_content(chunk_size=8192): + fp.write(chunk) + + print(f"Downloaded {a['url']}...") + + # Upload file to Redmine + try: + with open(file_path, "rb") as fp: + r = requests.post( + cfg.redmine_root_url + f"/uploads.json?filename={filename}", + data=fp, + verify=cfg.redmine_verify_certificates, + headers={ + "X-Redmine-API-Key": cfg.redmine_api_key, + "Content-Type": "application/octet-stream", + }, + ) + if r.status_code >= 400: + print( + "Error on post {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}".format( + str(r), r.request.headers, r.headers, r.content + ) + ) + raise Exception("Redmine upload failed") + else: + rmfile = r.json() + upload["token"] = rmfile["upload"]["token"] + + except Exception as e: + print(f"Failuring uploading file to Redmine {file_path}, error: {e}") + continue + + # Delete file locally + os.unlink(file_path) + + # Wrap up this attachment + uploads.append(upload) + + return uploads + + dry_run_counter = 0 +import_counter = 0 for c in board["cards"]: - desc = c["desc"] - if c["idChecklists"]: - desc += u'\n' - for id in c["idChecklists"]: - for item in checklists_dict[id]: - desc += u'\n[{0}] {1}'.format(u'x' if item["state"] == u'complete' else u' ', item["name"]) - card = { - "issue": { - "subject": u'[{0}][{1}] {2}'.format(board["name"], orig_lists_dict[c["idList"]], c["name"]), - "description": desc, - "project_id": redmine_project_id, - "assigned_to_id": user_map[c["idMembers"][0]] if c["idMembers"] else redmine_users_dict[cfg.redmine_default_user], - "status_id": status_map[c["idList"]] if c["idList"] else redmine_statuses_dict[redmine_default_status], - "priority_id": priority_map[c["idLabels"][0]] if c["idLabels"] else redmine_priorities_dict[redmine_default_priority], - "is_private": False - } - } - print(u'Importing {0}...'.format(card["issue"]["subject"]).encode('utf-8')) - issue_id = -1 - if dry_run: - dry_run_counter += 1 - issue_id = dry_run_counter - print(json.dumps(card, sort_keys=False, indent=4, separators=(',', ': '))) - else: - result = requests.post(cfg.redmine_root_url + '/issues.json', data=json.dumps(card), verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key, "Content-Type": "application/json"}) - if result.status_code >= 400: - print('Error on post {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}'.format(str(result), result.request.headers, result.headers, result.content)) - print(json.dumps(card, sort_keys=False, indent=4, separators=(',', ': '))) - sys.exit(1) - else: - print(str(result)) - issue = result.json() - issue_id = issue["issue"]["id"] - #print(u'Response JSON:\n{0}\nIssue ID: {1}'.format(str(issue), issue_id).encode('utf-8')) - if issue_id >= 0 and c["id"] in comments_dict: - for index, comment in enumerate(reversed(comments_dict[c["id"]])): - update = { - "issue": { - "notes": u'{0}:\n{1}'.format(comment["author"], comment["text"]).format('utf-8') if len(comment["author"]) > 0 else comment["text"] - } - } - print(u'Importing comment {0} of {1}...'.format(index + 1, len(comments_dict[c["id"]])).encode('utf-8')) - if dry_run: - print(json.dumps(update, sort_keys=False, indent=4, separators=(',', ': '))) - else: - result = requests.put(cfg.redmine_root_url + '/issues/{0}.json'.format(issue_id), data=json.dumps(update), verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key, "Content-Type": "application/json"}) - if result.status_code >= 400: - print('Error for comments {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}'.format(str(result), result.request.headers, result.headers, result.content)) - sys.exit(1) - else: - print(str(result)) - #print("Aborting after one for testing") - #sys.exit(0) - -print('Done!') + if dry_run and dry_run_counter > 0: + print("ENDING DRY RUN") + sys.exit(0) + + if c["id"] in import_history: + # Already imported this card, continue + continue + + desc = c["desc"] + desc += "\n\n" + desc += "Imported from Trello: [{0}][{1}]".format( + board["name"], orig_lists_dict[c["idList"]] + ) + all_checklists = [] + if c["idChecklists"]: + for id in c["idChecklists"]: + for item in checklists_dict[id]: + all_checklists.append( + { + "subject": item["name"], + "is_done": 1 if item["state"] == "complete" else 0, + } + ) + card = { + "issue": { + "subject": "{0}".format(c["name"]), + "description": desc, + "project_id": redmine_project_id, + "assigned_to_id": user_map[c["idMembers"][0]] + if c["idMembers"] + else redmine_users_dict[cfg.redmine_default_user], + "status_id": status_map[c["idList"]] + if c["idList"] + else redmine_statuses_dict[redmine_default_status], + "priority_id": priority_map[c["idLabels"][0]] + if c["idLabels"] + else redmine_priorities_dict[redmine_default_priority], + "is_private": False, + } + } + + trk = _set_tracker_or_status(c) + if trk: + card["issue"].update(trk) + + if len(all_checklists): + card["issue"]["checklists_attributes"] = all_checklists + + uploads = handle_attachments(c) + if len(uploads): + card["issue"]["uploads"] = uploads + + print("Importing {0}...".format(card["issue"]["subject"]).encode("utf-8")) + issue_id = -1 + if dry_run: + dry_run_counter += 1 + issue_id = dry_run_counter + print(json.dumps(card, sort_keys=False, indent=4, separators=(",", ": "))) + else: + result = requests.post( + cfg.redmine_root_url + "/issues.json", + data=json.dumps(card), + verify=cfg.redmine_verify_certificates, + headers={ + "X-Redmine-API-Key": cfg.redmine_api_key, + "Content-Type": "application/json", + }, + ) + if result.status_code >= 400: + print( + "Error on post {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}".format( + str(result), result.request.headers, result.headers, result.content + ) + ) + print(json.dumps(card, sort_keys=False, indent=4, separators=(",", ": "))) + sys.exit(1) + else: + print(str(result)) + issue = result.json() + issue_id = issue["issue"]["id"] + # print(u'Response JSON:\n{0}\nIssue ID: {1}'.format(str(issue), issue_id).encode('utf-8')) + if issue_id >= 0 and c["id"] in comments_dict: + for index, comment in enumerate(reversed(comments_dict[c["id"]])): + try: + update = { + "issue": { + "notes": "{0}:\n{1}".format( + comment["author"], comment["text"] + ).format("utf-8") + if len(comment["author"]) > 0 + else comment["text"] + } + } + except KeyError as err: + print(f"ERR: {err}, data: {comment}") + + print( + "Importing comment {0} of {1}...".format( + index + 1, len(comments_dict[c["id"]]) + ).encode("utf-8") + ) + if dry_run: + print( + json.dumps( + update, sort_keys=False, indent=4, separators=(",", ": ") + ) + ) + else: + result = requests.put( + cfg.redmine_root_url + "/issues/{0}.json".format(issue_id), + data=json.dumps(update), + verify=cfg.redmine_verify_certificates, + headers={ + "X-Redmine-API-Key": cfg.redmine_api_key, + "Content-Type": "application/json", + }, + ) + if result.status_code >= 400: + print( + "Error for comments {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}".format( + str(result), + result.request.headers, + result.headers, + result.content, + ) + ) + sys.exit(1) + else: + print(str(result)) + + import_history.append(c["id"]) + import_counter += 1 + if import_counter >= 5: + write_import_history() + # print("Aborting after one for testing") + # sys.exit(0) + +print("Done!") -- 2.43.0