diff --git a/CHANGELOG.md b/CHANGELOG.md index b8164f25..91b0887e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Improved layout and styling in "Login" page. - Separate "sign Up" and "Log In" forms. - "My Checks" page: support filtering checks by query string parameters. +- Added Trello integration ### Bug Fixes - Timezones were missing in the "Change Schedule" dialog, fixed. diff --git a/hc/api/models.py b/hc/api/models.py index 9248bf7f..a6e62f99 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -40,7 +40,8 @@ CHANNEL_KINDS = (("email", "Email"), ("discord", "Discord"), ("telegram", "Telegram"), ("sms", "SMS"), - ("zendesk", "Zendesk")) + ("zendesk", "Zendesk"), + ("trello", "Trello")) PO_PRIORITIES = { -2: "lowest", @@ -300,6 +301,8 @@ class Channel(models.Model): return transports.Sms(self) elif self.kind == "zendesk": return transports.Zendesk(self) + elif self.kind == "trello": + return transports.Trello(self) else: raise NotImplementedError("Unknown channel kind: %s" % self.kind) @@ -502,6 +505,27 @@ class Channel(models.Model): doc = json.loads(self.value) return doc["label"] + @property + def trello_token(self): + assert self.kind == "trello" + if self.value.startswith("{"): + doc = json.loads(self.value) + return doc["token"] + + @property + def trello_board_list(self): + assert self.kind == "trello" + if self.value.startswith("{"): + doc = json.loads(self.value) + return doc["board_name"], doc["list_name"] + + @property + def trello_list_id(self): + assert self.kind == "trello" + if self.value.startswith("{"): + doc = json.loads(self.value) + return doc["list_id"] + class Notification(models.Model): class Meta: diff --git a/hc/api/transports.py b/hc/api/transports.py index 58e27c50..9748586e 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -424,3 +424,20 @@ class Zendesk(HttpTransport): return self.notify_down(check) if check.status == "up": return self.notify_up(check) + + +class Trello(HttpTransport): + URL = 'https://api.trello.com/1/cards' + + def is_noop(self, check): + return check.status != "down" + + def notify(self, check): + params = { + "idList": self.channel.trello_list_id, + "name": tmpl("trello_title.html", check=check), + "key": settings.TRELLO_APP_KEY, + "token": self.channel.trello_token + } + + return self.post(self.URL, params=params) diff --git a/hc/front/urls.py b/hc/front/urls.py index 140e5a60..20e430b0 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -35,6 +35,8 @@ channel_urls = [ path('add_telegram/', views.add_telegram, name="hc-add-telegram"), path('add_sms/', views.add_sms, name="hc-add-sms"), path('add_zendesk/', views.add_zendesk, name="hc-add-zendesk"), + path('add_trello/', views.add_trello, name="hc-add-trello"), + path('add_trello/settings/', views.trello_settings, name="hc-trello-settings"), path('/checks/', views.channel_checks, name="hc-channel-checks"), path('/remove/', views.remove_channel, name="hc-remove-channel"), path('/verify//', views.verify_email, diff --git a/hc/front/views.py b/hc/front/views.py index fa32054d..b95e747d 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -156,6 +156,7 @@ def index(request): "enable_telegram": settings.TELEGRAM_TOKEN is not None, "enable_sms": settings.TWILIO_AUTH is not None, "enable_pd": settings.PD_VENDOR_KEY is not None, + "enable_trello": settings.TRELLO_APP_KEY is not None, "registration_open": settings.REGISTRATION_OPEN } @@ -461,6 +462,7 @@ def channels(request): "enable_sms": settings.TWILIO_AUTH is not None, "enable_pd": settings.PD_VENDOR_KEY is not None, "enable_zendesk": settings.ZENDESK_CLIENT_ID is not None, + "enable_trello": settings.TRELLO_APP_KEY is not None, "use_payments": settings.USE_PAYMENTS } @@ -1058,3 +1060,54 @@ def add_zendesk(request): ctx = {"page": "channels"} return render(request, "integrations/add_zendesk.html", ctx) + + +@login_required +def add_trello(request): + if settings.TRELLO_APP_KEY is None: + raise Http404("trello integration is not available") + + if request.method == "POST": + channel = Channel(user=request.team.user, kind="trello") + channel.value = request.POST["settings"] + channel.save() + + channel.assign_all_checks() + return redirect("hc-channels") + + authorize_url = "https://trello.com/1/authorize?" + urlencode({ + "expiration": "never", + "name": settings.SITE_NAME, + "scope": "read,write", + "response_type": "token", + "key": settings.TRELLO_APP_KEY, + "return_url": settings.SITE_ROOT + reverse("hc-add-trello") + }) + + ctx = { + "page": "channels", + "authorize_url": authorize_url + } + + return render(request, "integrations/add_trello.html", ctx) + + +@login_required +@require_POST +def trello_settings(request): + token = request.POST.get("token") + + url = "https://api.trello.com/1/members/me/boards?" + urlencode({ + "key": settings.TRELLO_APP_KEY, + "token": token, + "fields": "id,name", + "lists": "open", + "list_fields": "id,name" + }) + + r = requests.get(url) + ctx = { + "token": token, + "data": r.json() + } + return render(request, "integrations/trello_settings.html", ctx) diff --git a/hc/settings.py b/hc/settings.py index 9a40b3d9..c3c5609a 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -169,6 +169,9 @@ PD_VENDOR_KEY = None ZENDESK_CLIENT_ID = None ZENDESK_CLIENT_SECRET = None +# Trello +TRELLO_APP_KEY = None + if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")): from .local_settings import * else: diff --git a/static/css/icomoon.css b/static/css/icomoon.css index 6eab7a32..37163a2c 100644 --- a/static/css/icomoon.css +++ b/static/css/icomoon.css @@ -1,10 +1,10 @@ @font-face { font-family: 'icomoon'; - src: url('../fonts/icomoon.eot?7cu72n'); - src: url('../fonts/icomoon.eot?7cu72n#iefix') format('embedded-opentype'), - url('../fonts/icomoon.ttf?7cu72n') format('truetype'), - url('../fonts/icomoon.woff?7cu72n') format('woff'), - url('../fonts/icomoon.svg?7cu72n#icomoon') format('svg'); + src: url('../fonts/icomoon.eot?b4dy0b'); + src: url('../fonts/icomoon.eot?b4dy0b#iefix') format('embedded-opentype'), + url('../fonts/icomoon.ttf?b4dy0b') format('truetype'), + url('../fonts/icomoon.woff?b4dy0b') format('woff'), + url('../fonts/icomoon.svg?b4dy0b#icomoon') format('svg'); font-weight: normal; font-style: normal; } @@ -76,6 +76,10 @@ content: "\e906"; color: #2ca5e0; } +.icon-trello:before { + content: "\e911"; + color: #0079bf; +} .icon-zendesk:before { content: "\e907"; } @@ -120,4 +124,4 @@ } .icon-delete:before { content: "\e900"; -} \ No newline at end of file +} diff --git a/static/fonts/icomoon.eot b/static/fonts/icomoon.eot index e91ace1f..bf911d45 100644 Binary files a/static/fonts/icomoon.eot and b/static/fonts/icomoon.eot differ diff --git a/static/fonts/icomoon.svg b/static/fonts/icomoon.svg index 4795746b..2f1a742a 100644 --- a/static/fonts/icomoon.svg +++ b/static/fonts/icomoon.svg @@ -35,4 +35,5 @@ + \ No newline at end of file diff --git a/static/fonts/icomoon.ttf b/static/fonts/icomoon.ttf index 41f23e6f..4a702973 100644 Binary files a/static/fonts/icomoon.ttf and b/static/fonts/icomoon.ttf differ diff --git a/static/fonts/icomoon.woff b/static/fonts/icomoon.woff index 9e099996..f8ee06fc 100644 Binary files a/static/fonts/icomoon.woff and b/static/fonts/icomoon.woff differ diff --git a/static/img/integrations/trello.png b/static/img/integrations/trello.png new file mode 100644 index 00000000..a980aef7 Binary files /dev/null and b/static/img/integrations/trello.png differ diff --git a/static/js/add_trello.js b/static/js/add_trello.js new file mode 100644 index 00000000..9ebbdf88 --- /dev/null +++ b/static/js/add_trello.js @@ -0,0 +1,32 @@ +$(function() { + function updateSettings() { + var opt = $('#list-selector').find(":selected"); + $("#settings").val(JSON.stringify({ + "token": $("#settings").data("token"), + "list_id": opt.data("listId"), + "board_name": opt.data("boardName"), + "list_name": opt.data("listName") + })); + } + + var tokenMatch = window.location.hash.match(/token=(\w+)/); + if (tokenMatch) { + $(".jumbotron").hide(); + $("integration-settings").text("Loading..."); + + token = tokenMatch[1]; + var csrf = $('input[name=csrfmiddlewaretoken]').val(); + $.ajax({ + url: "/integrations/add_trello/settings/", + type: "post", + headers: {"X-CSRFToken": csrf}, + data: {token: token}, + success: function(data) { + $("#integration-settings" ).html(data); + updateSettings(); + } + }); + } + + $("#integration-settings").on("change", "#list-selector", updateSettings); +}); diff --git a/templates/front/channels.html b/templates/front/channels.html index 7a647300..74c233ed 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -108,6 +108,11 @@ {% else %} {{ ch.sms_number }} {% endif %} + {% elif ch.kind == "trello" %} + board + {{ ch.trello_board_list|first }}, + list + {{ ch.trello_board_list|last }} {% else %} {{ ch.value }} {% endif %} @@ -288,7 +293,7 @@ {% if enable_zendesk %}
  • Discord icon + class="icon" alt="Zendesk icon" />

    Zendesk Support

    Create a Zendesk support ticket when a check goes down.

    @@ -296,6 +301,17 @@ Add Integration
  • {% endif %} + {% if enable_trello %} +
  • + Trello icon + +

    Trello

    +

    Create a Trello card when a check goes down.

    + + Add Integration +
  • + {% endif %}