Skip to content

Commit

Permalink
Add TemplateService
Browse files Browse the repository at this point in the history
This service carries out functionality on individual templates, like
creating the modal for a user to specify template variables and for
rendering out the template.

This plums the block action triggered by a template selection menu
through the TemplateService and into the TemplateVariablesModal to
create the modal.
  • Loading branch information
jonathansick committed Sep 30, 2024
1 parent b32c539 commit 1fd109c
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 68 deletions.
12 changes: 11 additions & 1 deletion src/templatebot/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from templatebot.services.slackblockactions import SlackBlockActionsService
from templatebot.services.slackmessage import SlackMessageService
from templatebot.services.slackview import SlackViewService
from templatebot.services.template import TemplateService
from templatebot.services.templaterepo import TemplateRepoService
from templatebot.storage.repo import RepoManager
from templatebot.storage.slack import SlackWebApiClient
Expand Down Expand Up @@ -94,7 +95,10 @@ def create_slack_message_service(self) -> SlackMessageService:
def create_slack_block_actions_service(self) -> SlackBlockActionsService:
"""Create a new Slack block actions handling service."""
return SlackBlockActionsService(
logger=self._logger, slack_client=self.create_slack_web_client()
logger=self._logger,
slack_client=self.create_slack_web_client(),
repo_manager=self._process_context.repo_manager,
template_service=self.create_template_service(),
)

def create_slack_view_service(self) -> SlackViewService:
Expand All @@ -110,3 +114,9 @@ def create_template_repo_service(self) -> TemplateRepoService:
repo_manager=self._process_context.repo_manager,
slack_client=self.create_slack_web_client(),
)

def create_template_service(self) -> TemplateService:
"""Create a new template service."""
return TemplateService(
logger=self._logger, slack_client=self.create_slack_web_client()
)
149 changes: 82 additions & 67 deletions src/templatebot/services/slackblockactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,16 @@
SlackStaticSelectAction,
)
from structlog.stdlib import BoundLogger
from templatekit.repo import FileTemplate, ProjectTemplate

from templatebot.constants import SELECT_PROJECT_TEMPLATE_ACTION
from templatebot.storage.slack import SlackWebApiClient
from templatebot.storage.slack._models import SlackChatUpdateMessageRequest
from templatebot.storage.slack.blockkit import (
SlackInputBlock,
SlackMrkdwnTextObject,
SlackOptionObject,
SlackPlainTextInputElement,
SlackPlainTextObject,
SlackSectionBlock,
SlackStaticSelectElement,
from templatebot.config import config
from templatebot.constants import (
SELECT_FILE_TEMPLATE_ACTION,
SELECT_PROJECT_TEMPLATE_ACTION,
)
from templatebot.storage.slack.views import SlackModalView
from templatebot.services.template import TemplateService
from templatebot.storage.repo import RepoManager
from templatebot.storage.slack import SlackWebApiClient

__all__ = ["SlackBlockActionsService"]

Expand All @@ -30,10 +26,16 @@ class SlackBlockActionsService:
"""A service for processing Slack block actions."""

def __init__(
self, logger: BoundLogger, slack_client: SlackWebApiClient
self,
logger: BoundLogger,
slack_client: SlackWebApiClient,
template_service: TemplateService,
repo_manager: RepoManager,
) -> None:
self._logger = logger
self._slack_client = slack_client
self._template_service = template_service
self._repo_manager = repo_manager

async def handle_block_actions(
self, payload: SquarebotSlackBlockActionsValue
Expand All @@ -44,6 +46,10 @@ async def handle_block_actions(
await self.handle_project_template_selection(
action=action, payload=payload
)
elif action.action_id == SELECT_FILE_TEMPLATE_ACTION:
await self.handle_file_template_selection(
action=action, payload=payload
)

async def handle_project_template_selection(
self,
Expand Down Expand Up @@ -71,61 +77,70 @@ async def handle_project_template_selection(
raise ValueError("No message in payload")
original_message_ts = payload.message.ts

updated_messsage = SlackChatUpdateMessageRequest(
channel=original_message_channel,
ts=original_message_ts,
text=(
f"We'll create a project with the {selected_option.text.text} "
"template"
),
)
await self._slack_client.update_message(updated_messsage)
git_ref = "main"

demo_section = SlackSectionBlock(
text=SlackMrkdwnTextObject(
text=f"Let's create a {selected_option.text.text} project."
),
)
demo_select_input = SlackInputBlock(
label=SlackPlainTextObject(text="License"),
element=SlackStaticSelectElement(
placeholder=SlackPlainTextObject(text="Choose a license…"),
action_id="select_license",
options=[
SlackOptionObject(
text=SlackPlainTextObject(text="MIT"),
value="mit",
),
SlackOptionObject(
text=SlackPlainTextObject(text="GPLv3"),
value="gplv3",
),
],
),
block_id="license",
hint=SlackPlainTextObject(text="MIT is preferred."),
)
demo_text_input = SlackInputBlock(
label=SlackPlainTextObject(text="Project name"),
element=SlackPlainTextInputElement(
placeholder=SlackPlainTextObject(text="Enter a project name…"),
action_id="project_name",
min_length=3,
),
block_id="project_name",
)
modal = SlackModalView(
title=SlackPlainTextObject(text="Set up your project"),
blocks=[demo_section, demo_select_input, demo_text_input],
submit=SlackPlainTextObject(text="Create project"),
close=SlackPlainTextObject(text="Cancel"),
template = self._repo_manager.get_repo(gitref=git_ref)[
selected_option.value
]
if not isinstance(template, ProjectTemplate):
raise TypeError(
f"Expected {selected_option.value} template to be a "
f"ProjectTemplate, but got {type(template)}"
)

await self._template_service.show_project_template_modal(
user_id=payload.user.id,
trigger_id=payload.trigger_id,
message_ts=original_message_ts,
channel_id=original_message_channel,
template=template,
git_ref=git_ref,
repo_url=str(config.template_repo_url),
)
response = await self._slack_client.open_view(
trigger_id=payload.trigger_id, view=modal

async def handle_file_template_selection(
self,
*,
action: SlackBlockActionBase,
payload: SquarebotSlackBlockActionsValue,
) -> None:
"""Handle a file template selection."""
if not isinstance(action, SlackStaticSelectAction):
raise TypeError(
f"Expected action for {SELECT_FILE_TEMPLATE_ACTION} to be "
f"a SlackStaticSelectAction, but got {type(action)}"
)
selected_option = action.selected_option
self._logger.debug(
"Selected file template",
value=selected_option.value,
text=selected_option.text.text,
)
if not response["ok"]:
self._logger.error(
"Failed to open view",
response=response,
payload=payload.model_dump(mode="json"),

if not payload.channel:
raise ValueError("No channel in payload")
original_message_channel = payload.channel.id
if not payload.message:
raise ValueError("No message in payload")
original_message_ts = payload.message.ts

git_ref = "main"

template = self._repo_manager.get_repo(gitref=git_ref)[
selected_option.value
]
if not isinstance(template, FileTemplate):
raise TypeError(
f"Expected {selected_option.value} template to be a "
f"ProjectTemplate, but got {type(template)}"
)

await self._template_service.show_file_template_modal(
user_id=payload.user.id,
trigger_id=payload.trigger_id,
message_ts=original_message_ts,
channel_id=original_message_channel,
template=template,
git_ref=git_ref,
repo_url=str(config.template_repo_url),
)
122 changes: 122 additions & 0 deletions src/templatebot/services/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Template service."""

from __future__ import annotations

from structlog.stdlib import BoundLogger
from templatekit.repo import FileTemplate, ProjectTemplate

from templatebot.storage.slack import (
SlackChatUpdateMessageRequest,
SlackWebApiClient,
)
from templatebot.storage.slack.variablesmodal import TemplateVariablesModal

__all__ = ["TemplateService"]


class TemplateService:
"""A service for operating with templates.
Features include:
- Having a user configure a template through a Slack modal view
- Rendering a template with user-provided values and running the
configuration of that repository and LSST the Docs services.
"""

def __init__(
self, *, logger: BoundLogger, slack_client: SlackWebApiClient
) -> None:
self._logger = logger
self._slack_client = slack_client

async def show_file_template_modal(
self,
*,
user_id: str,
trigger_id: str,
message_ts: str,
channel_id: str,
template: FileTemplate,
git_ref: str,
repo_url: str,
) -> None:
"""Show a modal for selecting a file template."""
if len(template.config["dialog_fields"]) == 0:
await self._respond_with_nonconfigurable_content(
template=template,
channel_id=channel_id,
trigger_message_ts=message_ts,
)
else:
await self._open_template_modal(
template=template,
trigger_id=trigger_id,
git_ref=git_ref,
repo_url=repo_url,
trigger_message_ts=message_ts,
trigger_channel_id=channel_id,
)

async def show_project_template_modal(
self,
*,
user_id: str,
trigger_id: str,
message_ts: str,
channel_id: str,
template: ProjectTemplate,
git_ref: str,
repo_url: str,
) -> None:
"""Show a modal for selecting a project template."""
await self._open_template_modal(
template=template,
trigger_id=trigger_id,
git_ref=git_ref,
repo_url=repo_url,
trigger_message_ts=message_ts,
trigger_channel_id=channel_id,
)

async def _open_template_modal(
self,
*,
template: FileTemplate | ProjectTemplate,
trigger_id: str,
git_ref: str,
repo_url: str,
trigger_message_ts: str | None = None,
trigger_channel_id: str | None = None,
) -> None:
"""Open a modal for configuring a template."""
modal_view = TemplateVariablesModal.create(
template=template,
git_ref=git_ref,
repo_url=repo_url,
trigger_message_ts=trigger_message_ts,
trigger_channel_id=trigger_channel_id,
)
await self._slack_client.open_view(
trigger_id=trigger_id, view=modal_view
)

async def _respond_with_nonconfigurable_content(
self,
*,
template: FileTemplate,
channel_id: str,
trigger_message_ts: str,
) -> None:
"""Respond with non-configurable content."""
# TODO(jonathansick): render the template and send it back to the user
await self._slack_client.update_message(
message_update_request=SlackChatUpdateMessageRequest(
channel=channel_id,
ts=trigger_message_ts,
text=(
f"The {template.name} template does not require "
"configuration."
),
)
)

0 comments on commit 1fd109c

Please sign in to comment.