From c33922f71763b09c066fe026c03ace39f10295ec Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 10 Oct 2024 11:26:17 +1100 Subject: [PATCH 1/4] Pivotal importer: Set a dummy person in comment when it is undefined. Fix 'scrum or scrum' helper info on jira and pivotal commands. --- taiga/importers/management/commands/import_from_jira.py | 2 +- taiga/importers/management/commands/import_from_pivotal.py | 2 +- taiga/importers/pivotal/importer.py | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/taiga/importers/management/commands/import_from_jira.py b/taiga/importers/management/commands/import_from_jira.py index 288cc831..8b3ac640 100644 --- a/taiga/importers/management/commands/import_from_jira.py +++ b/taiga/importers/management/commands/import_from_jira.py @@ -28,7 +28,7 @@ def add_arguments(self, parser): parser.add_argument('--project-type', dest="project_type", type=str, help='Project type in jira: project or board') parser.add_argument('--template', dest='template', default="scrum", - help='template to use: scrum or scrum (default scrum)') + help='template to use: scrum or kanban (default scrum)') parser.add_argument('--ask-for-users', dest='ask_for_users', const=True, action="store_const", default=False, help='Import closed data') diff --git a/taiga/importers/management/commands/import_from_pivotal.py b/taiga/importers/management/commands/import_from_pivotal.py index 9cfb518d..11be32e9 100644 --- a/taiga/importers/management/commands/import_from_pivotal.py +++ b/taiga/importers/management/commands/import_from_pivotal.py @@ -19,7 +19,7 @@ def add_arguments(self, parser): parser.add_argument('--project-id', dest="project_id", type=str, help='Project ID or full name (ex: taigaio/taiga-back)') parser.add_argument('--template', dest='template', default="scrum", - help='template to use: scrum or scrum (default scrum)') + help='template to use: scrum or kanban (default scrum)') parser.add_argument('--ask-for-users', dest='ask_for_users', const=True, action="store_const", default=False, help='Import closed data') diff --git a/taiga/importers/pivotal/importer.py b/taiga/importers/pivotal/importer.py index c7257f9f..d698e273 100644 --- a/taiga/importers/pivotal/importer.py +++ b/taiga/importers/pivotal/importer.py @@ -477,6 +477,11 @@ def _import_comments(self, project_data, obj, story, options): users_bindings = options.get('users_bindings', {}) for comment in story['comments']: + if not comment.get('person', False): + print(f"Person element is missing in comment {comment['id']} of story {story['id']}, will treat as unknown") + comment['person'] = {} + comment['person']['id'] = '0' + comment['person']['name'] = 'unknown' if 'text' in comment: snapshot = take_snapshot( obj, From 744d1b4b315b09cc0b453df848490565d4fd0178 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 18 Nov 2024 14:31:54 +1100 Subject: [PATCH 2/4] Convert custom 'Type' field in Pivotal importer to be a dropdown. Add support for Priority custom field. Fix saving multiple custom fields --- taiga/importers/pivotal/importer.py | 62 ++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/taiga/importers/pivotal/importer.py b/taiga/importers/pivotal/importer.py index d698e273..f86b55b2 100644 --- a/taiga/importers/pivotal/importer.py +++ b/taiga/importers/pivotal/importer.py @@ -265,9 +265,28 @@ def _import_project_data(self, project_id, options): UserStoryCustomAttribute.objects.create( name="Type", description="Story type", - type="text", + type="dropdown", order=2, - project=project + project=project, + extra=[ + "feature", + "bug", + "chore", + "release" + ] + ) + UserStoryCustomAttribute.objects.create( + name="Priority", + description="Priority", + type="dropdown", + order=3, + project=project, + extra=[ + "Critical", + "High", + "Medium", + "Low" + ] ) for user in options.get('users_bindings', {}).values(): if user != self._user: @@ -298,6 +317,7 @@ def _import_user_stories_data(self, project_data, project, options): epics = {e['label']['id']: e for e in project_data['epics']} due_date_field = project.userstorycustomattributes.get(name="Due date") story_type_field = project.userstorycustomattributes.get(name="Type") + story_priority_field = project.userstorycustomattributes.get(name="Priority") story_milestone_binding = {} for iteration in project_data['iterations']: for story in iteration['stories']: @@ -318,6 +338,7 @@ def _import_user_stories_data(self, project_data, project, options): "description", "estimate", "story_type", + "story_priority", "current_state", "deadline", "requested_by_id", @@ -374,12 +395,23 @@ def _import_user_stories_data(self, project_data, project, options): if watcher_user: us.add_watcher(watcher_user) + custom_attributes_values = {} if story.get('deadline', None): - us.custom_attributes_values.attributes_values = {due_date_field.id: story['deadline']} - us.custom_attributes_values.save() + custom_attributes_values[due_date_field.id] = story['deadline'] if story.get('story_type', None): - us.custom_attributes_values.attributes_values = {story_type_field.id: story['story_type']} - us.custom_attributes_values.save() + custom_attributes_values[story_type_field.id] = story['story_type'] + if story.get('story_priority', None): + if story['story_priority'] == "p0": + priority = "Critical" + if story['story_priority'] == "p1": + priority = "High" + if story['story_priority'] == "p2": + priority = "Medium" + if story['story_priority'] == "p3": + priority = "Low" + custom_attributes_values[story_priority_field.id] = priority + us.custom_attributes_values.attributes_values = custom_attributes_values + us.custom_attributes_values.save() UserStory.objects.filter(id=us.id).update( ref=story['id'], @@ -571,6 +603,7 @@ def _transform_activity_data(self, obj, activity, options): users_bindings = options.get('users_bindings', {}) due_date_field = obj.project.userstorycustomattributes.get(name="Due date") story_type_field = obj.project.userstorycustomattributes.get(name="Type") + story_priority_field = obj.project.userstorycustomattributes.get(name="Priority") user = {"pk": None, "name": activity.get('performed_by', {}).get('name', None)} taiga_user = users_bindings.get(activity.get('performed_by', {}).get('id', None), None) @@ -666,6 +699,23 @@ def _transform_activity_data(self, obj, activity, options): "id": story_type_field.id }) + if 'story_priority' in change['new_values']: + if "custom_attributes" not in result['change_old']: + result['change_old']["custom_attributes"] = [] + if "custom_attributes" not in result['change_new']: + result['change_new']["custom_attributes"] = [] + + result['change_old']["custom_attributes"].append({ + "name": "Priority", + "value": change['original_values']['story_priority'], + "id": story_priority_field.id + }) + result['change_new']["custom_attributes"].append({ + "name": "Priority", + "value": change['new_values']['story_priority'], + "id": story_priority_field.id + }) + if 'deadline' in change['new_values']: if "custom_attributes" not in result['change_old']: result['change_old']["custom_attributes"] = [] From ef638946d6574241fb096b0037d99f3f182072c6 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 18 Nov 2024 14:32:42 +1100 Subject: [PATCH 3/4] rewrite the Pivotal '--ask-for-users' option to '--map-users' and optimistically try to map to existing users, no prompting --- .../commands/import_from_pivotal.py | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/taiga/importers/management/commands/import_from_pivotal.py b/taiga/importers/management/commands/import_from_pivotal.py index 11be32e9..2393052e 100644 --- a/taiga/importers/management/commands/import_from_pivotal.py +++ b/taiga/importers/management/commands/import_from_pivotal.py @@ -20,9 +20,9 @@ def add_arguments(self, parser): help='Project ID or full name (ex: taigaio/taiga-back)') parser.add_argument('--template', dest='template', default="scrum", help='template to use: scrum or kanban (default scrum)') - parser.add_argument('--ask-for-users', dest='ask_for_users', const=True, + parser.add_argument('--map-users', dest='map_users', const=True, action="store_const", default=False, - help='Import closed data') + help='Map usernames from Pivotal to Taiga. You can create users in Taiga in advance via /admin/users/user') parser.add_argument('--closed-data', dest='closed_data', const=True, action="store_const", default=False, help='Import closed data') @@ -50,25 +50,13 @@ def handle(self, *args, **options): project_id = input("Project id: ") users_bindings = {} - if options.get('ask_for_users', None): - print("Add the username or email for next pivotal users:") + if options.get('map_users', None): for user in importer.list_users(project_id): try: - users_bindings[user['id']] = User.objects.get(Q(email=user['person'].get('email', "not-valid"))) - break + users_bindings[user['person']['id']] = User.objects.get(Q(email=user['person'].get('email', "not-valid"))) except User.DoesNotExist: pass - while True: - username_or_email = input("{}: ".format(user['person']['name'])) - if username_or_email == "": - break - try: - users_bindings[user['id']] = User.objects.get(Q(username=username_or_email) | Q(email=username_or_email)) - break - except User.DoesNotExist: - print("ERROR: Invalid username or email") - options = { "template": options.get('template'), "import_closed_data": options.get("closed_data", False), From 27d90b5da3bf2e84ae5d54ce73034fb3c42507b0 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 21 Nov 2024 08:37:36 +1100 Subject: [PATCH 4/4] Pivotal importer: ensure stories that have multiple owners get assigned as multiple assignees in Taiga --- taiga/importers/pivotal/importer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/taiga/importers/pivotal/importer.py b/taiga/importers/pivotal/importer.py index f86b55b2..41bbbbfb 100644 --- a/taiga/importers/pivotal/importer.py +++ b/taiga/importers/pivotal/importer.py @@ -358,8 +358,13 @@ def _import_user_stories_data(self, project_data, project, options): tags.append(label['name']) assigned_to = None + assigned_users = [] if len(story['owner_ids']) > 0: assigned_to = users_bindings.get(story['owner_ids'][0], None) + for assignee in story['owner_ids']: + bound_user = users_bindings.get(assignee, None) + if bound_user: + assigned_users.append(bound_user.id) owner = users_bindings.get(story['requested_by_id'], self._user) @@ -382,6 +387,9 @@ def _import_user_stories_data(self, project_data, project, options): milestone=story_milestone_binding.get(story['id'], None) ) + if assigned_users: + us.assigned_users.set(assigned_users) + points = Points.objects.get(project=project, value=story.get('estimate', None)) RolePoints.objects.filter(user_story=us, role__slug="main").update(points_id=points.id)