Skip to content

Commit b8c8142

Browse files
committed
As a user, I want to be notified of the task I created so that I have a link to it
Fixes #14
1 parent 9f71980 commit b8c8142

14 files changed

+262
-63
lines changed

sources/factories/GithubGQLCallFactory.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import gql
1010
from gql.transport.requests import RequestsHTTPTransport
1111
import sources.utils.Constants as cst
12+
from sources.models.CreatedGithubTaskParam import CreatedGithubTaskParam
1213

1314

1415
class GithubGQLCallFactory():
@@ -209,6 +210,10 @@ def create_github_task(self, app_context, mutation_param):
209210
addProjectV2DraftIssue(input: $task) {
210211
projectItem {
211212
id
213+
databaseId
214+
project {
215+
number
216+
}
212217
}
213218
}
214219
}
@@ -223,9 +228,15 @@ def create_github_task(self, app_context, mutation_param):
223228

224229
response = self.__send_gql_request(app_context, query, query_params)
225230

231+
project_item = {}
226232
try:
227-
# pylint: disable-next=unsubscriptable-object
228-
project_item_id = response['addProjectV2DraftIssue']['projectItem']['id']
233+
# pylint: disable=unsubscriptable-object
234+
project_item = CreatedGithubTaskParam(
235+
response['addProjectV2DraftIssue']['projectItem']['id'],
236+
response['addProjectV2DraftIssue']['projectItem']['databaseId'],
237+
response['addProjectV2DraftIssue']['projectItem']['project']['number']
238+
)
239+
# pylint: enable=unsubscriptable-object
229240
except KeyError:
230-
project_item_id = None
231-
return project_item_id
241+
project_item = None
242+
return project_item
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
This file contains an abstract class to provide basic functions for Slack factories, like handling tokens.
3+
"""
4+
5+
from abc import ABCMeta
6+
from flask import current_app
7+
import sources.utils.Constants as cst
8+
9+
10+
class SlackFactoryAbstract(metaclass=ABCMeta):
11+
"""
12+
Class managing the business logic related to Github ProjectV2 items
13+
14+
"""
15+
def __init__(self):
16+
"""
17+
The handler instanciates the objects it needed to complete the processing of the request.
18+
"""
19+
self.__slack_bot_user_token = None
20+
21+
def _get_slack_bot_user_token(self, app_context):
22+
"""
23+
Returns the Slack Bot User token of the app.
24+
If not retrieved yet, it is retrieved from the Flask app configuration.
25+
"""
26+
if self.__slack_bot_user_token is None:
27+
app_context.push() # The factory usually runs in a dedicated thread, so Flask app context must be applied.
28+
self.__slack_bot_user_token = current_app.config[cst.APP_CONFIG_TOKEN_SLACK_BOT_USER_TOKEN]
29+
return self.__slack_bot_user_token
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
This module defines the factory for Slack messages (DM, public, etc.)
3+
"""
4+
import requests
5+
from sources.factories.SlackFactoryAbstract import SlackFactoryAbstract
6+
7+
8+
class SlackMessageFactory(SlackFactoryAbstract):
9+
"""
10+
Class managing the business logic related to Github ProjectV2 items
11+
12+
"""
13+
def __init__(self):
14+
"""
15+
The handler instanciates the objects it needed to complete the processing of the request.
16+
"""
17+
SlackFactoryAbstract.__init__(self)
18+
self.post_message_url = 'https://slack.com/api/chat.postMessage'
19+
20+
def post_message(self, app_context, channel, text):
21+
"""
22+
Sends a message 'text' to the 'channel' as the app.
23+
"""
24+
request_open_view_header = {"Content-type": "application/json",
25+
"Authorization": "Bearer " + self._get_slack_bot_user_token(app_context)}
26+
request_open_view_payload = {}
27+
request_open_view_payload['channel'] = channel
28+
request_open_view_payload['text'] = text
29+
requests.post(url=self.post_message_url, headers=request_open_view_header,
30+
json=request_open_view_payload, timeout=3000)

sources/factories/SlackModalFactory.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,19 @@
44
import json
55
from pathlib import Path
66
import requests
7-
from flask import current_app
8-
import sources.utils.Constants as cst
7+
from sources.factories.SlackFactoryAbstract import SlackFactoryAbstract
98

109

11-
class SlackModalFactory():
10+
class SlackModalFactory(SlackFactoryAbstract):
1211
"""
1312
Class capable of creating and opening modal views for Slack users.
1413
"""
1514

1615
def __init__(self):
16+
SlackFactoryAbstract.__init__(self)
1717
self.open_view_url = 'https://slack.com/api/views.open'
18-
self.__slack_bot_user_token = None
1918
self.__assignee_list = None
2019

21-
def __get_slack_bot_user_token(self, app_context):
22-
"""
23-
Returns the Slack Bot User token of the app.
24-
If not retrieved yet, it is retrieved from the Flask app configuration.
25-
"""
26-
if self.__slack_bot_user_token is None:
27-
app_context.push() # The factory usually runs in a dedicated thread, so Flask app context must be applied.
28-
self.__slack_bot_user_token = current_app.config[cst.APP_CONFIG_TOKEN_SLACK_BOT_USER_TOKEN]
29-
return self.__slack_bot_user_token
30-
3120
def __get_assignee_list(self):
3221
"""
3322
Generate the list of options for the drop-down select of assignee, from thegithub.json config file.
@@ -152,7 +141,7 @@ def create_github_task_modal(self, app_context, trigger_id):
152141
"private_metadata": "",
153142
"callback_id": "ttl_create_github_task_modal_submit"
154143
}'''
155-
request_open_view_header = {"Authorization": "Bearer " + self.__get_slack_bot_user_token(app_context)}
144+
request_open_view_header = {"Authorization": "Bearer " + self._get_slack_bot_user_token(app_context)}
156145
request_open_view_payload = {}
157146
request_open_view_payload['view'] = view
158147
request_open_view_payload['trigger_id'] = trigger_id

sources/handlers/GithubTaskHandler.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"""
44

55
from sources.factories.GithubGQLCallFactory import GithubGQLCallFactory
6+
from sources.factories.SlackMessageFactory import SlackMessageFactory
7+
from sources.models.InitGithubTaskParam import InitGithubTaskParam
68

79

810
class GithubTaskHandler():
@@ -15,45 +17,56 @@ def __init__(self):
1517
The handler instanciates the objects it needed to complete the processing of the request.
1618
"""
1719
self.github_gql_call_factory = GithubGQLCallFactory()
20+
self.slack_message_factory = SlackMessageFactory()
1821

19-
def init_github_task(self, app_context, task_params):
22+
def get_task_link(self, project_number, item_number):
23+
"""
24+
Generates the URL to display a Github task of WP Media from the project number and the project item number
25+
"""
26+
# return f"https://github.com/orgs/wp-media/projects/{project_number}?pane=issue&itemId={item_number}"
27+
return f"https://github.com/users/MathieuLamiot/projects/{project_number}?pane=issue&itemId={item_number}"
28+
29+
def init_github_task(self, app_context, task_params: InitGithubTaskParam):
2030
"""
2131
Create a GitHub task in the configured project according to the task parameters.
2232
To do so, a GQL Mutation is requested to the GitHub API.
2333
24-
task_params:
34+
task_params
2535
- title (Mandatory): Title of the task
2636
- body (Mandatory): Description of the task
2737
"""
2838
mutation_param = {}
2939
# Check mandatory parameters
30-
if 'title' not in task_params:
40+
if task_params.title is None:
3141
raise TypeError('Missing title in task_params')
32-
mutation_param['title'] = task_params['title']
42+
mutation_param['title'] = task_params.title
3343

34-
if 'body' not in task_params:
44+
if task_params.body is None:
3545
raise TypeError('Missing body in task_params')
36-
mutation_param['body'] = task_params['body']
46+
mutation_param['body'] = task_params.body
3747

3848
# Check optional parameters
39-
handle_immediately = False
40-
if 'handle_immediately' in task_params:
41-
handle_immediately = task_params['handle_immediately']
4249

4350
assignee_id = None
44-
if 'assignee' in task_params and 'no-assignee' != task_params['assignee']:
45-
assignee = task_params['assignee']
46-
assignee_id = self.github_gql_call_factory.get_user_id_from_login(app_context, assignee)
51+
if 'no-assignee' != task_params.assignee:
52+
assignee_id = self.github_gql_call_factory.get_user_id_from_login(app_context, task_params.assignee)
4753
if assignee_id is not None:
4854
mutation_param['assigneeIds'] = [assignee_id]
4955

5056
# Create the task and retrieve its ID
51-
project_item_id = self.github_gql_call_factory.create_github_task(app_context, mutation_param)
57+
project_item = self.github_gql_call_factory.create_github_task(app_context, mutation_param)
58+
59+
if project_item is not None:
60+
61+
# Send notifications to Slack
62+
if task_params.initiator is not None:
63+
text = "You created a Github task: " + self.get_task_link(
64+
project_item.project_number, project_item.item_database_id)
65+
self.slack_message_factory.post_message(app_context, task_params.initiator, text)
5266

53-
if project_item_id is not None:
5467
# Set the task to Todo
55-
self.github_gql_call_factory.set_task_to_initial_status(app_context, project_item_id)
68+
self.github_gql_call_factory.set_task_to_initial_status(app_context, project_item.item_id)
5669

57-
if handle_immediately:
70+
if task_params.handle_immediately:
5871
# Set the task to the current sprint
59-
self.github_gql_call_factory.set_task_to_current_sprint(app_context, project_item_id)
72+
self.github_gql_call_factory.set_task_to_current_sprint(app_context, project_item.item_id)

sources/handlers/SlackViewSubmissionHandler.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from threading import Thread
66
from flask import current_app
77
from sources.handlers.GithubTaskHandler import GithubTaskHandler
8+
from sources.models.InitGithubTaskParam import InitGithubTaskParam
89

910

1011
class SlackViewSubmissionHandler():
@@ -49,28 +50,34 @@ def create_github_task_modal_retrieve_params(self, payload_json):
4950
This method extract the github task parameters from a submitted Slack modal "Create GitHub Task".
5051
Only the parameters found in the payload are set.
5152
"""
52-
task_params = {}
5353
modal_values = payload_json["view"]["state"]["values"]
5454

5555
# Title component
56-
task_params['title'] = modal_values['title_block']['task_title']['value']
56+
title = modal_values['title_block']['task_title']['value']
5757

5858
# Description component
5959
body_input = modal_values['description_block']['task_description']['value']
6060
user_name = payload_json["user"]["name"]
61-
task_params['body'] = f"Task submitted by {user_name} through TBTT.\n\n{body_input}"
61+
body = f"Task submitted by {user_name} through TBTT.\n\n{body_input}"
6262

6363
# Immediate component
64+
handle_immediately = False
6465
keys = list(dict.keys(modal_values['immediately_block']))
6566
selected_options = modal_values['immediately_block'][keys[0]]['selected_options']
6667
for selected_option in selected_options:
6768
if 'handle_immediately' == selected_option['value']:
68-
task_params['handle_immediately'] = True
69+
handle_immediately = True
6970

7071
# Assignee component
72+
assignee = 'no-assignee'
7173
keys = list(dict.keys(modal_values['assignee_block']))
7274
selected_option = modal_values['assignee_block'][keys[0]]['selected_option']
7375
if selected_option is not None:
74-
task_params['assignee'] = selected_option['value']
76+
assignee = selected_option['value']
77+
78+
# Initiator of the request
79+
initiator = payload_json["user"]["id"]
80+
81+
task_params = InitGithubTaskParam(title, body, handle_immediately, assignee, initiator)
7582

7683
return task_params
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
Defiens a dataclass for the parameters returned by Github after task creation
3+
"""
4+
5+
from dataclasses import dataclass
6+
7+
8+
@dataclass
9+
class CreatedGithubTaskParam:
10+
"""
11+
Dataclass for all the parameters allowing to initiate a task
12+
"""
13+
item_id: str
14+
item_database_id: int
15+
project_number: int

sources/models/InitGithubTaskParam.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
Defiens a dataclass for the parameters to pass to GithubTaskHandler.init_github_task
3+
"""
4+
5+
from dataclasses import dataclass
6+
7+
8+
@dataclass
9+
class InitGithubTaskParam:
10+
"""
11+
Dataclass for all the parameters allowing to initiate a task
12+
"""
13+
title: str
14+
body: str
15+
handle_immediately: bool = False
16+
assignee: str = 'no-assignee'
17+
initiator: str = None

tests/integration/GithubTaskInitTest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from freezegun import freeze_time
99
from sources.handlers.GithubTaskHandler import GithubTaskHandler
1010
from sources.factories.GithubGQLCallFactory import GithubGQLCallFactory
11+
from sources.models.InitGithubTaskParam import InitGithubTaskParam
1112

1213
# pylint: disable=unused-argument
1314

@@ -88,7 +89,7 @@ def test_init_github_task_all_fields(mock_sendrequest):
8889
Test init_github_task with mandatory fields
8990
"""
9091
github_task_handler = GithubTaskHandler()
91-
task_params = {"title": "the_title", "body": "the_body", "handle_immediately": True, "assignee": 'the_assignee'}
92+
task_params = InitGithubTaskParam(title="the_title", body="the_body", handle_immediately=True, assignee='the_assignee')
9293
github_task_handler.init_github_task('app_context', task_params)
9394

9495
mock_sendrequest.assert_called()

tests/unit/GithubGQLCallFactoryTest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ def mock_send_gql_request_create_task_mandatory(*args, **kwargs):
6262
addProjectV2DraftIssue(input: $task) {
6363
projectItem {
6464
id
65+
databaseId
66+
project {
67+
number
68+
}
6569
}
6670
}
6771
}
@@ -84,6 +88,10 @@ def mock_send_gql_request_create_task_assignee(*args, **kwargs):
8488
addProjectV2DraftIssue(input: $task) {
8589
projectItem {
8690
id
91+
databaseId
92+
project {
93+
number
94+
}
8795
}
8896
}
8997
}
@@ -106,6 +114,10 @@ def mock_send_gql_request_create_task_no_assignee(*args, **kwargs):
106114
addProjectV2DraftIssue(input: $task) {
107115
projectItem {
108116
id
117+
databaseId
118+
project {
119+
number
120+
}
109121
}
110122
}
111123
}

0 commit comments

Comments
 (0)