-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create review subtasks for requested reviewers #110
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -59,6 +59,31 @@ def create_task(self, project_id: str, due_date_str: str = None) -> str: | |||||
response = self.asana_api_client.tasks.create(create_task_params) | ||||||
return response["gid"] | ||||||
|
||||||
def create_subtask( | ||||||
self, parent_task_id: str, reviewer: str, task_name: str, task_description, | ||||||
due_date_str: str = None | ||||||
) -> str: | ||||||
""" | ||||||
Creates an Asana subtask for the given parent task, returning the task_id | ||||||
""" | ||||||
validate_object_id(parent_task_id, "AsanaClient.create_subtask requires a task_id") | ||||||
|
||||||
create_task_params = { | ||||||
"name": task_name, | ||||||
"html_notes": task_description, | ||||||
"assignee": reviewer, | ||||||
"parent": parent_task_id | ||||||
} | ||||||
if due_date_str: | ||||||
create_task_params["due_on"] = due_date_str | ||||||
response = self.asana_api_client.tasks.create(create_task_params) | ||||||
return response["gid"] | ||||||
|
||||||
def get_task_completed_status(self, task_id: str) -> bool: | ||||||
"""Returns bool value representing the task completed status.""" | ||||||
response = self.asana_api_client.tasks.find_by_id(task_id, opt_fields=["completed"]) | ||||||
return response["completed"] | ||||||
|
||||||
def update_task(self, task_id: str, fields: dict): | ||||||
""" | ||||||
Updates the specified Asana task, setting the provided fields | ||||||
|
@@ -146,13 +171,29 @@ def create_task(project_id: str, due_date_str: str = None) -> str: | |||||
return AsanaClient.singleton().create_task(project_id, due_date_str=due_date_str) | ||||||
|
||||||
|
||||||
def create_subtask( | ||||||
parent_task_id: str, reviewer_id: str, task_name: str, task_description, | ||||||
due_date_str: str = None | ||||||
) -> str: | ||||||
""" | ||||||
Creates an Asana task and makes is a subtask of the given task id | ||||||
""" | ||||||
return AsanaClient.singleton().create_subtask( | ||||||
parent_task_id, reviewer_id, task_name, task_description, due_date_str | ||||||
) | ||||||
|
||||||
|
||||||
def update_task(task_id: str, fields: dict): | ||||||
""" | ||||||
Updates the specified Asana task, setting the provided fields | ||||||
""" | ||||||
return AsanaClient.singleton().update_task(task_id, fields) | ||||||
|
||||||
|
||||||
def is_task_completed(task_id) -> bool: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the pattern established here is to keep the naming consistent
Suggested change
|
||||||
return AsanaClient.singleton().get_task_completed_status(task_id) | ||||||
|
||||||
|
||||||
def complete_task(task_id: str): | ||||||
return update_task(task_id, {"completed": True}) | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -38,6 +38,29 @@ def update_task(pull_request: PullRequest, task_id: str): | |||||
maybe_complete_tasks_on_merge(pull_request) | ||||||
|
||||||
|
||||||
def create_review_subtask(pull_request: PullRequest, parent_task_id: str, reviewer_handle: str): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
asana_id = asana_helpers.asana_user_id_from_github_handle(reviewer_handle) | ||||||
task_name = asana_helpers.subtask_name_from_pull_request(pull_request) | ||||||
task_description = asana_helpers.subtask_description_from_pull_request( | ||||||
pull_request, reviewer_handle | ||||||
) | ||||||
due_date_str = asana_helpers.default_due_date_str() | ||||||
return asana_client.create_subtask( | ||||||
parent_task_id, asana_id, task_name, task_description, due_date_str=due_date_str | ||||||
) | ||||||
|
||||||
|
||||||
def update_subtask(pull_request: PullRequest, subtask_id): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The naming here is a bit confusing, perhaps just |
||||||
is_task_completed = asana_client.is_task_completed(subtask_id) | ||||||
if is_task_completed: | ||||||
# We must re-open the task | ||||||
asana_client.update_task(subtask_id, {"completed": False}) | ||||||
asana_client.add_comment( | ||||||
subtask_id, | ||||||
f"<body>{pull_request.author_handle()} has asked you to re-review the PR.</body>" | ||||||
) | ||||||
|
||||||
|
||||||
def maybe_complete_tasks_on_merge(pull_request: PullRequest): | ||||||
if asana_logic.should_autocomplete_tasks_on_merge(pull_request): | ||||||
task_ids_to_complete_on_merge = asana_helpers.get_linked_task_ids(pull_request) | ||||||
|
@@ -96,6 +119,12 @@ def upsert_github_review_to_task(review: Review, task_id: str): | |||||
) | ||||||
|
||||||
|
||||||
def update_subtask_after_review(pull_request: PullRequest, review: Review, subtask_id: str) -> None: | ||||||
logger.info(f"Updating subtask {subtask_id} for pull request {pull_request.url()}") | ||||||
if asana_logic.should_complete_subtask(pull_request, review): | ||||||
asana_client.complete_task(subtask_id) | ||||||
|
||||||
|
||||||
def delete_comment(github_comment_id: str): | ||||||
asana_comment_id = dynamodb_client.get_asana_id_from_github_node_id( | ||||||
github_comment_id | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -210,6 +210,33 @@ def get_asana_domain_user_id_from_github_handle(github_handle: str) -> Optional[ | |||||
) | ||||||
|
||||||
|
||||||
def get_asana_id_from_two_github_node_ids(gh_node_id_a: str, gh_node_id_b: str) -> Optional[str]: | ||||||
""" | ||||||
Using the singleton instance of DynamoDbClient, creating it if necessary: | ||||||
|
||||||
Retrieves the Asana object-id associated with the specified GitHub node-ids, | ||||||
or None, if no such association exists. Object-table associations are created | ||||||
by SGTM via the insert_github_node_to_asana_id_mapping method, below. | ||||||
""" | ||||||
return DynamoDbClient.singleton().get_asana_id_from_github_node_id( | ||||||
_get_dynamodb_key_from_two_github_nodes(gh_node_id_a, gh_node_id_b) | ||||||
) | ||||||
|
||||||
|
||||||
def insert_two_github_node_to_asana_id_mapping(gh_node_id_a: str, gh_node_id_b: str, asana_id: str): | ||||||
""" | ||||||
Using the singleton instance of DynamoDbClient, creating it if necessary: | ||||||
|
||||||
Creates an association between two GitHub node-ids and an Asana object-id. The dynamoDb | ||||||
key is formed by concatenating the two GitHub node ids using a "-" separator. | ||||||
""" | ||||||
dynamo_db_key = _get_dynamodb_key_from_two_github_nodes(gh_node_id_a, gh_node_id_b) | ||||||
print(f"Inserting key {dynamo_db_key} to DynamoDb.") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
return DynamoDbClient.singleton().insert_github_node_to_asana_id_mapping( | ||||||
_get_dynamodb_key_from_two_github_nodes(gh_node_id_a, gh_node_id_b), asana_id | ||||||
) | ||||||
|
||||||
|
||||||
def get_all_user_items() -> List[dict]: | ||||||
return DynamoDbClient.singleton().get_all_user_items() | ||||||
|
||||||
|
@@ -236,3 +263,6 @@ def bulk_insert_github_handle_to_asana_user_id_mapping( | |||||
DynamoDbClient.singleton().bulk_insert_github_handle_to_asana_user_id_mapping( | ||||||
gh_and_asana_ids | ||||||
) | ||||||
|
||||||
def _get_dynamodb_key_from_two_github_nodes(gh_node_id_a: str, gh_node_id_b: str) -> str: | ||||||
return f"{gh_node_id_a}-{gh_node_id_b}" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps this function should sort the two node ids, so we don't have to remember which order to pass them in and reduce bugs around unexpected orders |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ def upsert_pull_request(pull_request: PullRequest): | |
task_id = asana_controller.create_task(pull_request.repository_id()) | ||
if task_id is None: | ||
# TODO: Handle this case | ||
logger.error(f"No task id returned from create task {pull_request_id}") | ||
return | ||
|
||
logger.info(f"Task created for pull request {pull_request_id}: {task_id}") | ||
|
@@ -25,6 +26,8 @@ def upsert_pull_request(pull_request: PullRequest): | |
f"Task found for pull request {pull_request_id}, updating task {task_id}" | ||
) | ||
asana_controller.update_task(pull_request, task_id) | ||
upsert_subtasks(pull_request, task_id) | ||
update_subtasks(pull_request, task_id) | ||
|
||
|
||
def _add_asana_task_to_pull_request(pull_request: PullRequest, task_id: str): | ||
|
@@ -42,6 +45,63 @@ def _add_asana_task_to_pull_request(pull_request: PullRequest, task_id: str): | |
pull_request.set_body(new_body) | ||
|
||
|
||
def upsert_subtasks(pull_request: PullRequest, task_id: str) -> None: | ||
""" | ||
Create subtasks for the reviewers. | ||
|
||
This is synchronized with dynamodb so we only create a subtask once for each reviewer. | ||
|
||
We go through the list of requested reviewers and check if they have a subtask created already. | ||
If not, we create the subtask. | ||
""" | ||
for reviewer in pull_request.requested_reviewers(): | ||
subtask_id = dynamodb_client.get_asana_id_from_two_github_node_ids( | ||
pull_request.id(), reviewer.id() | ||
) | ||
if subtask_id is None: | ||
created_subtask_id = asana_controller.create_review_subtask( | ||
pull_request, task_id, reviewer.login() | ||
) | ||
if created_subtask_id is None: | ||
# TODO: Handle this case | ||
logger.error( | ||
f"No subtask id returned from create subtask {pull_request.id()}: {task_id}" | ||
) | ||
continue | ||
|
||
logger.info( | ||
f"Subtask created for pull request {pull_request.id()}: {task_id}: {created_subtask_id}" | ||
) | ||
dynamodb_client.insert_two_github_node_to_asana_id_mapping( | ||
pull_request.id(), reviewer.id(), created_subtask_id | ||
) | ||
|
||
|
||
def update_subtasks(pull_request: PullRequest, task_id: str) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it'd be more efficient to do this and |
||
"""Syncs the subtasks based on latest version of the PR. | ||
|
||
Here we want to do three things: | ||
1. Sync updates to the PR name and other content to the subtask. | ||
2. Re-open, with note, subtasks that were completed since a new review has been requested. | ||
3. Close, with note, subtasks where reviewer has been removed. | ||
""" | ||
for reviewer in pull_request.requested_reviewers(): | ||
subtask_id = dynamodb_client.get_asana_id_from_two_github_node_ids( | ||
pull_request.id(), reviewer.id() | ||
) | ||
if subtask_id is None: | ||
# This should not happen as we should already have created subtasks for all | ||
# requested reviewers. | ||
# TODO: Handle this case | ||
logger.error( | ||
f"No subtask id returned for requested reviewer. PR {pull_request.id()} Reviewer: {reviewer.id()}" | ||
) | ||
asana_controller.update_subtask(pull_request, subtask_id) | ||
|
||
# TODO: Find tasks to be closed with comment. | ||
# TODO: Update the subtask title and task description if applicable. | ||
|
||
|
||
def upsert_comment(pull_request: PullRequest, comment: Comment): | ||
pull_request_id = pull_request.id() | ||
task_id = dynamodb_client.get_asana_id_from_github_node_id(pull_request_id) | ||
|
@@ -71,6 +131,24 @@ def upsert_review(pull_request: PullRequest, review: Review): | |
if review.is_approval_or_changes_requested(): | ||
assign_pull_request_to_author(pull_request) | ||
asana_controller.update_task(pull_request, task_id) | ||
update_subtask_after_review(pull_request, review) | ||
|
||
|
||
def update_subtask_after_review(pull_request: PullRequest, review: Review) -> None: | ||
subtask_id = dynamodb_client.get_asana_id_from_two_github_node_ids( | ||
pull_request.id(), review.author().id() | ||
) | ||
if subtask_id is None: | ||
# This happens if the review was made by a person not requested to review | ||
# TODO: Test this scenario and delete the print and logging statement. | ||
print("Review made by a person that was not requested to review") | ||
logger.info( | ||
f"Could not find subtask for author {review.author().id()} on PR {pull_request.id()}" | ||
) | ||
else: | ||
# Here we want to update the subtask. | ||
# TODO do we want to track this with dynamodb so this only gets executed once? | ||
asana_controller.update_subtask_after_review(pull_request, review, subtask_id) | ||
|
||
|
||
def assign_pull_request_to_author(pull_request: PullRequest): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
login | ||
... on User { | ||
name | ||
id | ||
} | ||
} | ||
body | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'd just call this
assignee
since this is a wrapper around the Asana API and doesn't necessarily need to know the context of "reviewer" - just to format and forward the request to the API