Building a first slack command 🎉
Do you need a reliable partner in tech for your next project?
Setup a Slack application
- Go to https://api.slack.com/apps and create your app.
- Go to your app on api.slack.com and create
SLACK_CLIENT_ID
,SLACK_CLIENT_SECRET
andSLACK_SIGNING_SECRET
with a correspondent values insettings.py
. We will use this later on e.g. when using Incoming Web Hooks. Secret should be provided as ENV variable and shouldn't be part of the codebase. We will pass it to our process together with aDATABASE_URL
.
1SLACK_CLIENT_ID = '5130182099.926289676294'
2SLACK_CLIENT_SECRET = os.getenv('SLACK_CLIENT_SECRET')
3SLACK_SIGNING_SECRET = os.getenv('SLACK_SIGNING_SECRET')
- Install Python Slack bindings. You'll need them later on.
pipenv install slackclient
- Click on Slash Commands > Create New Command
- We will create command
/teamwork-latest
that will show last 5 project reports in our app.You need to fill request URL at this point. However during development, you usually can't easily expose your localhost to the world so that Slack can access it. Here's where ngrok comes handy. It will create a public URL and map it to your localhost. Please install it and setup an account. By runningngrok http 8000
it will give you a public URL that will route to your local Django instance.In my case I'll fill outhttp://slack-tutorial-app.ngrok.io/slack/commands/teamwork-latest/
After you've created your command, install your Slack app into your Slack
workspace. Once you run
/teamwork-latest
you should see the following
error./teamwork-latest failed with the error "dispatch_failed"
That is because we haven't implemented the endpoint yet! So let's do it.
- Let's create a Slack application
pipenv run ./manage.py startapp slack_app
and add slack_app
to
INSTALLED_APPS
- And route all URLs prefixed with
/slack
to our slack app by addingpath('slack/', include('slack_app.urls'))
tourlpatterns
inteamwork/urls.py
- Configure slack routing. Create
urls.py
inslack_app
folder.
1from django.urls import path
2
3from . import commands
4
5urlpatterns = [
6 path('commands/teamwork-latest/', commands.teamwork_latest),
7]
- Last, but not least, create a file for commands and a command itself.
slack/commands.py
1from django.http import JsonResponse
2from django.views.decorators.csrf import csrf_exempt
3
4
5@csrf_exempt
6def teamwork_latest(request):
7 return JsonResponse({
8 "blocks": [
9 {
10 "type": "section",
11 "text": {
12 "type": "plain_text",
13 "text": "Hello World :tada:.",
14 "emoji": True
15 }
16 }
17 ]
18 })
Generating response
We've returned simple message, but you can build much more. Have a look
at Slack's
Block Kit Builder at
what UI you can render as a response. The only disadvantage of it is
that it's not Open Source :disappointed_relieved:.
However, in order to return something valuable, we need to generate the
output based on the content in our db. So let's do it 🎉! ... by
writing tests first 💪
- Let's create our expected output in Block Kit Builder e.g. https://api.slack.com/tools/block-kit-builder?mode=message&blocks=%5B%7B%22type%22%3A%22section%22%2C%22text%22%3A%7B%22type%22%3A%22mrkdwn%22%2C%22text%22%3A%22Project%3A%20Example%20Project%5Cn%20Reported%20Hours%3A%20_8.0h_%5Cn%20Description%3A%20I%27ve%20written%20unit-tests%2C%20because%20it%20makes%20the%20whole%20development%20process%20faster%20%3A)%22%7D%7D%2C%7B%22type%22%3A%22divider%22%7D%2C%7B%22type%22%3A%22section%22%2C%22text%22%3A%7B%22type%22%3A%22mrkdwn%22%2C%22text%22%3A%22Project%3A%20Important%20Project%5Cn%20Reported%20Hours%3A%20_4.0h_%5Cn%20Description%3A%20Writing%20React%20components%20and%20making%20Storybook%20cleanup%22%7D%7D%5D
- In
slack_app
application, create ablocks
folder where we will store all methods responsible for generating Slack blocks. Insideblocks
, let's createslack_commands.py
andslack_commands_spec.py
.
Our function will accept a Django request object (so that we can later use it to build absolute urls) and expect a list of blocks. Let's create an interface so that we can
create unit-tests.
slack_commands.py
1from typing import List
2
3def get_teamwork_latest_blocks(request) -> List:
4 []
tests_slack_commands.py
1from django.test import RequestFactory, TestCase
2
3from core.models import User
4from projects.models import Project, ProjectReport
5from .slack_commands import get_teamwork_latest_blocks
6
7
8class SlackCommandsTests(TestCase):
9 maxDiff = None
10
11 def setUp(self):
12 self.request_factory = RequestFactory()
13 self.request_factory.defaults['SERVER_NAME'] = 'scrumie.com'
14
15 user = User.objects.create(username='Test User')
16
17 project_1 = Project.objects.create(name='Example Project')
18 project_2 = Project.objects.create(name='React Project')
19
20 ProjectReport.objects.create(
21 project=project_1,
22 description="I've written unit-tests, because it makes the whole development process faster :)",
23 hours=8.0,
24 user=user,
25 )
26
27 ProjectReport.objects.create(
28 project=project_2,
29 description="Writing React components and making Storybook cleanup",
30 hours=4.0,
31 user=user,
32 )
33
34 def test_simple_teamwork_latest_command(self):
35 expected_blocks = [
36 {
37 "type": "section",
38 "text": {
39 "type": "mrkdwn",
40 "text": "*Project*: Example Project\n *Reported Hours*: _8.0h_\n *Description*: I've written unit-tests, because it makes the whole development process faster :)"
41 }
42 },
43 {
44 "type": "divider"
45 },
46 {
47 "type": "section",
48 "text": {
49 "type": "mrkdwn",
50 "text": "*Project*: React Project\n *Reported Hours*: _4.0h_\n *Description*: Writing React components and making Storybook cleanup"
51 }
52 }
53 ]
54
55 request = self.request_factory
56
57 self.assertEqual(expected_blocks, list(get_teamwork_latest_blocks(request)))
It's good practise to write a test that fails first and later fix the methods so that the test passes.
You can run tests with
pipenv run ./manage.py test
(this time without passing
DATABASE_URL
), we don't need to connect to our db for a test run.Tip: You can usefind . -name '*.py' | entr pipenv run ./manage.py test
to react to file changes.
The test should fail until you implement the method. Take the challenge and do it yourself or
...
1from itertools import chain
2from typing import List
3
4from projects.models import ProjectReport
5
6
7def get_teamwork_latest_blocks(request, max_items=5) -> List:
8 reports = ProjectReport.objects.all().order_by('id')[:max_items]
9 if reports:
10 return list(chain.from_iterable(
11 (
12 {
13 "type": "section",
14 "text": {
15 "type": "mrkdwn",
16 "text": f"*Project*: {report.project.name}\n *Reported Hours*: _{report.hours}h_\n *Description*: " +
17 report.description
18 }
19 },
20 {
21 "type": "divider",
22 }
23 ) for report in reports
24 ))[:-1]
25
26 else:
27 return [
28 {
29 "type": "section",
30 "text":{
31 "type": "mrkdwn",
32 "text": "No Project Reports"
33 }
34 }
35 ]
Now, please write more tests to try to cover all corner-cases.
- What if user has no reports?
- What if there is huge number of it? (Slack limits the number of blocks to 50)
- What if ...
Let's connect our block function to the actual view
This part is fairly easy, we just replace our mocked blocks with a function we just created.
1from django.http import JsonResponse
2from django.views.decorators.csrf import csrf_exempt
3
4from slack_app.blocks.slack_commands import get_teamwork_latest_blocks
5
6
7@csrf_exempt
8def teamwork_latest(request):
9 return JsonResponse({
10 "blocks": get_teamwork_latest_blocks(request)
11 })
If you now go to
/admin
and try to create some Project and Project Reports, write /teamwork-latest
in Slack and you should see the following.Text preview of a message
One last problem with our command is how it's going to be presented in a notifications. Type
/teamwork-latest
and quickly switch your window.
On Mac you'll see the following. (I'm sorry, but I can't currently test this on other platforms).To fix this, just add
text
property to our response.1@csrf_exempt
2def teamwork_latest(request):
3 return JsonResponse({
4 "blocks": get_teamwork_latest_blocks(request),
5 "text": "See recent work reports :page_with_curl:"
6 })
🎉 Now you're ready to go ahead and implement your own business logic with Slack Commands.
Let’s stay connected
Do you want the latest and greatest from our blog straight to your inbox? Chuck us your email address and get informed.
You can unsubscribe any time. For more details, review our privacy policy