Skip to content

Commit

Permalink
As a user, I want to be notified of the task I created so that I have…
Browse files Browse the repository at this point in the history
… a link to it

Fixes #14
  • Loading branch information
MathieuLamiot committed Aug 8, 2023
1 parent 9f71980 commit b8c8142
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 63 deletions.
19 changes: 15 additions & 4 deletions sources/factories/GithubGQLCallFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import gql
from gql.transport.requests import RequestsHTTPTransport
import sources.utils.Constants as cst
from sources.models.CreatedGithubTaskParam import CreatedGithubTaskParam


class GithubGQLCallFactory():
Expand Down Expand Up @@ -209,6 +210,10 @@ def create_github_task(self, app_context, mutation_param):
addProjectV2DraftIssue(input: $task) {
projectItem {
id
databaseId
project {
number
}
}
}
}
Expand All @@ -223,9 +228,15 @@ def create_github_task(self, app_context, mutation_param):

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

project_item = {}
try:
# pylint: disable-next=unsubscriptable-object
project_item_id = response['addProjectV2DraftIssue']['projectItem']['id']
# pylint: disable=unsubscriptable-object
project_item = CreatedGithubTaskParam(
response['addProjectV2DraftIssue']['projectItem']['id'],
response['addProjectV2DraftIssue']['projectItem']['databaseId'],
response['addProjectV2DraftIssue']['projectItem']['project']['number']
)
# pylint: enable=unsubscriptable-object
except KeyError:
project_item_id = None
return project_item_id
project_item = None
return project_item
29 changes: 29 additions & 0 deletions sources/factories/SlackFactoryAbstract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
This file contains an abstract class to provide basic functions for Slack factories, like handling tokens.
"""

from abc import ABCMeta
from flask import current_app
import sources.utils.Constants as cst


class SlackFactoryAbstract(metaclass=ABCMeta):
"""
Class managing the business logic related to Github ProjectV2 items
"""
def __init__(self):
"""
The handler instanciates the objects it needed to complete the processing of the request.
"""
self.__slack_bot_user_token = None

def _get_slack_bot_user_token(self, app_context):
"""
Returns the Slack Bot User token of the app.
If not retrieved yet, it is retrieved from the Flask app configuration.
"""
if self.__slack_bot_user_token is None:
app_context.push() # The factory usually runs in a dedicated thread, so Flask app context must be applied.
self.__slack_bot_user_token = current_app.config[cst.APP_CONFIG_TOKEN_SLACK_BOT_USER_TOKEN]
return self.__slack_bot_user_token
30 changes: 30 additions & 0 deletions sources/factories/SlackMessageFactory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
This module defines the factory for Slack messages (DM, public, etc.)
"""
import requests
from sources.factories.SlackFactoryAbstract import SlackFactoryAbstract


class SlackMessageFactory(SlackFactoryAbstract):
"""
Class managing the business logic related to Github ProjectV2 items
"""
def __init__(self):
"""
The handler instanciates the objects it needed to complete the processing of the request.
"""
SlackFactoryAbstract.__init__(self)
self.post_message_url = 'https://slack.com/api/chat.postMessage'

def post_message(self, app_context, channel, text):
"""
Sends a message 'text' to the 'channel' as the app.
"""
request_open_view_header = {"Content-type": "application/json",
"Authorization": "Bearer " + self._get_slack_bot_user_token(app_context)}
request_open_view_payload = {}
request_open_view_payload['channel'] = channel
request_open_view_payload['text'] = text
requests.post(url=self.post_message_url, headers=request_open_view_header,
json=request_open_view_payload, timeout=3000)
19 changes: 4 additions & 15 deletions sources/factories/SlackModalFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,19 @@
import json
from pathlib import Path
import requests
from flask import current_app
import sources.utils.Constants as cst
from sources.factories.SlackFactoryAbstract import SlackFactoryAbstract


class SlackModalFactory():
class SlackModalFactory(SlackFactoryAbstract):
"""
Class capable of creating and opening modal views for Slack users.
"""

def __init__(self):
SlackFactoryAbstract.__init__(self)
self.open_view_url = 'https://slack.com/api/views.open'
self.__slack_bot_user_token = None
self.__assignee_list = None

def __get_slack_bot_user_token(self, app_context):
"""
Returns the Slack Bot User token of the app.
If not retrieved yet, it is retrieved from the Flask app configuration.
"""
if self.__slack_bot_user_token is None:
app_context.push() # The factory usually runs in a dedicated thread, so Flask app context must be applied.
self.__slack_bot_user_token = current_app.config[cst.APP_CONFIG_TOKEN_SLACK_BOT_USER_TOKEN]
return self.__slack_bot_user_token

def __get_assignee_list(self):
"""
Generate the list of options for the drop-down select of assignee, from thegithub.json config file.
Expand Down Expand Up @@ -152,7 +141,7 @@ def create_github_task_modal(self, app_context, trigger_id):
"private_metadata": "",
"callback_id": "ttl_create_github_task_modal_submit"
}'''
request_open_view_header = {"Authorization": "Bearer " + self.__get_slack_bot_user_token(app_context)}
request_open_view_header = {"Authorization": "Bearer " + self._get_slack_bot_user_token(app_context)}
request_open_view_payload = {}
request_open_view_payload['view'] = view
request_open_view_payload['trigger_id'] = trigger_id
Expand Down
47 changes: 30 additions & 17 deletions sources/handlers/GithubTaskHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"""

from sources.factories.GithubGQLCallFactory import GithubGQLCallFactory
from sources.factories.SlackMessageFactory import SlackMessageFactory
from sources.models.InitGithubTaskParam import InitGithubTaskParam


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

def init_github_task(self, app_context, task_params):
def get_task_link(self, project_number, item_number):
"""
Generates the URL to display a Github task of WP Media from the project number and the project item number
"""
# return f"https://github.com/orgs/wp-media/projects/{project_number}?pane=issue&itemId={item_number}"
return f"https://github.com/users/MathieuLamiot/projects/{project_number}?pane=issue&itemId={item_number}"

def init_github_task(self, app_context, task_params: InitGithubTaskParam):
"""
Create a GitHub task in the configured project according to the task parameters.
To do so, a GQL Mutation is requested to the GitHub API.
task_params:
task_params
- title (Mandatory): Title of the task
- body (Mandatory): Description of the task
"""
mutation_param = {}
# Check mandatory parameters
if 'title' not in task_params:
if task_params.title is None:
raise TypeError('Missing title in task_params')
mutation_param['title'] = task_params['title']
mutation_param['title'] = task_params.title

if 'body' not in task_params:
if task_params.body is None:
raise TypeError('Missing body in task_params')
mutation_param['body'] = task_params['body']
mutation_param['body'] = task_params.body

# Check optional parameters
handle_immediately = False
if 'handle_immediately' in task_params:
handle_immediately = task_params['handle_immediately']

assignee_id = None
if 'assignee' in task_params and 'no-assignee' != task_params['assignee']:
assignee = task_params['assignee']
assignee_id = self.github_gql_call_factory.get_user_id_from_login(app_context, assignee)
if 'no-assignee' != task_params.assignee:
assignee_id = self.github_gql_call_factory.get_user_id_from_login(app_context, task_params.assignee)
if assignee_id is not None:
mutation_param['assigneeIds'] = [assignee_id]

# Create the task and retrieve its ID
project_item_id = self.github_gql_call_factory.create_github_task(app_context, mutation_param)
project_item = self.github_gql_call_factory.create_github_task(app_context, mutation_param)

if project_item is not None:

# Send notifications to Slack
if task_params.initiator is not None:
text = "You created a Github task: " + self.get_task_link(
project_item.project_number, project_item.item_database_id)
self.slack_message_factory.post_message(app_context, task_params.initiator, text)

if project_item_id is not None:
# Set the task to Todo
self.github_gql_call_factory.set_task_to_initial_status(app_context, project_item_id)
self.github_gql_call_factory.set_task_to_initial_status(app_context, project_item.item_id)

if handle_immediately:
if task_params.handle_immediately:
# Set the task to the current sprint
self.github_gql_call_factory.set_task_to_current_sprint(app_context, project_item_id)
self.github_gql_call_factory.set_task_to_current_sprint(app_context, project_item.item_id)
17 changes: 12 additions & 5 deletions sources/handlers/SlackViewSubmissionHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from threading import Thread
from flask import current_app
from sources.handlers.GithubTaskHandler import GithubTaskHandler
from sources.models.InitGithubTaskParam import InitGithubTaskParam


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

# Title component
task_params['title'] = modal_values['title_block']['task_title']['value']
title = modal_values['title_block']['task_title']['value']

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

# Immediate component
handle_immediately = False
keys = list(dict.keys(modal_values['immediately_block']))
selected_options = modal_values['immediately_block'][keys[0]]['selected_options']
for selected_option in selected_options:
if 'handle_immediately' == selected_option['value']:
task_params['handle_immediately'] = True
handle_immediately = True

# Assignee component
assignee = 'no-assignee'
keys = list(dict.keys(modal_values['assignee_block']))
selected_option = modal_values['assignee_block'][keys[0]]['selected_option']
if selected_option is not None:
task_params['assignee'] = selected_option['value']
assignee = selected_option['value']

# Initiator of the request
initiator = payload_json["user"]["id"]

task_params = InitGithubTaskParam(title, body, handle_immediately, assignee, initiator)

return task_params
15 changes: 15 additions & 0 deletions sources/models/CreatedGithubTaskParam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
Defiens a dataclass for the parameters returned by Github after task creation
"""

from dataclasses import dataclass


@dataclass
class CreatedGithubTaskParam:
"""
Dataclass for all the parameters allowing to initiate a task
"""
item_id: str
item_database_id: int
project_number: int
17 changes: 17 additions & 0 deletions sources/models/InitGithubTaskParam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Defiens a dataclass for the parameters to pass to GithubTaskHandler.init_github_task
"""

from dataclasses import dataclass


@dataclass
class InitGithubTaskParam:
"""
Dataclass for all the parameters allowing to initiate a task
"""
title: str
body: str
handle_immediately: bool = False
assignee: str = 'no-assignee'
initiator: str = None
3 changes: 2 additions & 1 deletion tests/integration/GithubTaskInitTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from freezegun import freeze_time
from sources.handlers.GithubTaskHandler import GithubTaskHandler
from sources.factories.GithubGQLCallFactory import GithubGQLCallFactory
from sources.models.InitGithubTaskParam import InitGithubTaskParam

# pylint: disable=unused-argument

Expand Down Expand Up @@ -88,7 +89,7 @@ def test_init_github_task_all_fields(mock_sendrequest):
Test init_github_task with mandatory fields
"""
github_task_handler = GithubTaskHandler()
task_params = {"title": "the_title", "body": "the_body", "handle_immediately": True, "assignee": 'the_assignee'}
task_params = InitGithubTaskParam(title="the_title", body="the_body", handle_immediately=True, assignee='the_assignee')
github_task_handler.init_github_task('app_context', task_params)

mock_sendrequest.assert_called()
12 changes: 12 additions & 0 deletions tests/unit/GithubGQLCallFactoryTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ def mock_send_gql_request_create_task_mandatory(*args, **kwargs):
addProjectV2DraftIssue(input: $task) {
projectItem {
id
databaseId
project {
number
}
}
}
}
Expand All @@ -84,6 +88,10 @@ def mock_send_gql_request_create_task_assignee(*args, **kwargs):
addProjectV2DraftIssue(input: $task) {
projectItem {
id
databaseId
project {
number
}
}
}
}
Expand All @@ -106,6 +114,10 @@ def mock_send_gql_request_create_task_no_assignee(*args, **kwargs):
addProjectV2DraftIssue(input: $task) {
projectItem {
id
databaseId
project {
number
}
}
}
}
Expand Down
Loading

0 comments on commit b8c8142

Please sign in to comment.