From e4d0103544b651e248a5e9059406051ae8dec4ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Thu, 18 Oct 2018 12:20:33 +0300 Subject: [PATCH] Trello integration WIP --- CHANGELOG.md | 1 + hc/api/models.py | 26 +++++++++- hc/api/transports.py | 17 +++++++ hc/front/urls.py | 2 + hc/front/views.py | 53 ++++++++++++++++++++ hc/settings.py | 3 ++ static/css/icomoon.css | 16 +++--- static/fonts/icomoon.eot | Bin 9796 -> 9948 bytes static/fonts/icomoon.svg | 1 + static/fonts/icomoon.ttf | Bin 9632 -> 9784 bytes static/fonts/icomoon.woff | Bin 9708 -> 9860 bytes static/img/integrations/trello.png | Bin 0 -> 1411 bytes static/js/add_trello.js | 32 ++++++++++++ templates/front/channels.html | 18 ++++++- templates/front/welcome.html | 6 +++ templates/integrations/add_trello.html | 39 ++++++++++++++ templates/integrations/trello_settings.html | 37 ++++++++++++++ templates/integrations/trello_title.html | 5 ++ 18 files changed, 248 insertions(+), 8 deletions(-) create mode 100644 static/img/integrations/trello.png create mode 100644 static/js/add_trello.js create mode 100644 templates/integrations/add_trello.html create mode 100644 templates/integrations/trello_settings.html create mode 100644 templates/integrations/trello_title.html 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 e91ace1ff4ccd602bfc08286c7c7aa524874cfa6..bf911d45d81639d0a5d20c408690eb60e70761b2 100644 GIT binary patch delta 415 zcmX@&bH|tMjv51lh1x_mGnU(}^{Nvc%K0@I7#Q{daYAx#VnMa%#A9;x4RRB?85kH3 zFfb_gWTYmh2-`WWWnfVD0je^~019xhv1kMNEkM3XMs7((i9Kza=L>IWagS zx(Fzx1LV8pCRP+M7%_eZ@(X}`g}lVv)R|tKzk&P-Ks(wB@{3Ctn1M13Y7!uM24-f) zx06#C?HL6pFJX*j6x__j6s=efRPp}+3j^~327aI#L!eGYB{oq(5iw&$BQsM$6E#K= zHf=^FH6YW>n$bv1R1iaex#2H^skgT&Gl=}hp(43gN<~F#ucQj&J}?CqU_Rh&ict3F zhop*%B$KO@1yJSR-HZ$%E~6_*-@n~JH-MbYz@S>+dOM!q<|_j?3z&cX?SGJ07(n#o o24!oPqN=*U{DqT@*8sUlM{nO zyw(8uQ-FM@+{B6k21CZrK>iINUm-6sH+7~L=WihY2hfhTg8bqV24{1 + \ No newline at end of file diff --git a/static/fonts/icomoon.ttf b/static/fonts/icomoon.ttf index 41f23e6fef7637e5290e03d355d21b09be99e97b..4a702973f8fd39558ee0606b2d7ee4d37e36b02f 100644 GIT binary patch delta 426 zcmZ4By~C%TfsuiMft#U$ftkU;KUm+0Ux-ZuD6$8L6OwZi3#vUUd>I%RWq|xS>50V! zKw1FE-vOjK(sL@)8ssK&1NjFS7!-RlQWH~z?VQ#!Fev)~<;^mH0vv2C+CY8_kgt-F zTT)SZlKmc#zXHf_$;nT4ObiZ*E&|Hw00mrf6DtZBj2J%y`5=oF@)C1XXL@n|2J$BW zEom#rFD?N(6bRHLK=KUC%x@<91M>j}exL$Fpf*J% zHc>$lF=Is|GgCnmHAWFOZAK+EAk)m6(MU{G5JP~u;V*-!x3?)Xi2TQ)BDq&eMMY|_ zqzdCcFa;K1KHzPNQ1<7Cq>73pldF^kQ03p sD+4zRn1B84e~>>IK=kB0O6H7;lSPypCQndy;}AFo)}*-kp7J3^009eM#Q*>R delta 291 zcmdntv%tHafsuiMft#U$ftkU;KUm+0Ux-Z&D6$8L6OwZi3#vUTd>I%RWq|xS>50V! zKw1FE-vOjK(sL@)Iubgwf&2pu3<^IoQWH}IUu_L%U{C_dn`Hn6IM`UUfcy_YzDh=J zNk!>N_InHr$|691Lr#9OV`6ZK*BYR}6rg}pZem3NgCXN*ApZuCuaK9Rn>y2r^EZ(H z187NGL4I)w(4jz}+5nPgU}ku7W9Tg`bpeRU_0^>)J*MNX&@PlD$+?LI3=E7JK;bzcT=J^|${eKnxH)Swq>JQE_sDa>L|1%5EG2$H1BtH*2XJ GVgvwu$z>k^ delta 337 zcmZqied8@s?(gQtz{mgus&5#$!SsU39~ng^F|kb45w2HD&P^;}U|`Gu%FF@bYL5!v z^u%JI7!wPS&jG~(={c2YKrtQ$28ACW+>y|kospWD!oZ*mQfCIjg0HrQX8;9(Vm3g& z3J7zsv1n!FmQ(=65`g>$5H3B*elI6K8K}-r`2kSC350_~yw>Co3QYE4jAazqyoxbeQJE1a z0R-~PuJOn7+k9o MgG+exeU+t*0DMnKYybcN diff --git a/static/img/integrations/trello.png b/static/img/integrations/trello.png new file mode 100644 index 0000000000000000000000000000000000000000..a980aef7de5ffd883ff5e2b07adbe0bf1ca8944e GIT binary patch literal 1411 zcmb7?eN@s{7{{-sOMF@LWh-rNW}I5)+-fy7Tf@S{GBdMTW>#dGCoAV00&3`%`LY?Q zl$6zC>C#MRmX$SmQ3OQA5X5+?si-A_3MnWe`=Onk{@dB_-21)X@8@~$x#xGzlNTBi zXlLzW4FIqULLtMTlWp2oiy*z46I~5}d39)TcmSv}0)h|}NdSQg3R?kz4G66uhXZhs zp#mHO5Ly5p;w>P9Zi*SrCQYCBI2yoG0iFhMkkU;H2PqBUsU{9pq4>@~1gb&`RU!WG zg4*UG#24U#fpp=bDb6>%)Au>#=Wo61q?%!=<~Zsi9Mu9(v&7RrNT)4Mr&(ptVHtGm zOu9`b-S!;A?i}Mo0%Hk*;gH?znBDC3Q_Ckmw>al8U2>SqFECf+v)uDpD+^fg0`{6h z&f1F{Ph#8GMcj2o-1WuWZ;H7aO4>J;w0}!#_a?RbT<-9_+_C9OhhJ&O=F-mZ$emlr zo&MK&Td(o9mGO{eUE8mB1>We|b&DTzi@)dh?!C9W!^;Kx$^}1E2qG#3`~MI|-Vq+W zBaEu-IaJwmxT@#qT@kukbnKoe=3ejdn%>yj-uOE4>HFd{_r+)H#R>J2A0J2(A4rlO zN|GN+QyQeHkECgjW!T3u+!Gn$*{fVyUmmS5pWau%==+7yf3cP|_+V zvE*0S@>2F-S=-Qc?obu~&0YSRd)*_of{{Okin<=f{T@ZVNbx{4+R!`t_=WO`SlK96 zK9wk+NmNZz6;-C9zZ_${9BX+s&gvWI^iOaHCfeod4!OE>P|X`ucMWNt4{7+rn(kqZ z;Pu-Vib=`nq;z!Zm1^p5m9}q8+drn2kLv~}bVC!mVYTkHdU`}NJvup~oSYe(ni<#X zC$xICPOs7Fb$Y|J-Y_$3(9arX4RZ#=9P}BD##2iYs-W>TOAZU!3lKxSCrkmoGY|=K zEnQJifF+;~BmrRKXxe7rYS}UfE=~^G6|lJ9+5rZ9F`n?{(By{(A^pSC#7f!j5C+1D zHSuJ3v8qu);S@xfXQjCq0e%T33UQ2r-#>!(qU0@umxTCri?IV*4B_zf+rMDM4VCLQ za^NE|75k6@QTbtl+wn)7C?4lk<%wSVy{wCGZt~be-eHUs)<%>s4XgbA^ORv^`09^Y z{$VFCIOW;OW8vkQC^8!{wKB-ky(rfL6+`s2LtSPgnzox;SP?_nZ4JCC0|{S~vNg5g zV5)N2p;M2th-BrBo!-Yr_W0RlNh5Dc8$b1qekhtFk|(*R-DF2^?P6mB!| z%iXg*Pg51Jx>O?GJNWdX7}o^m$P6jS(cYDyiQD>t^2m*Li0;z9;__(wqZtKo`btuT&WVi@OF`} zjlgJ6Ct`#Z@xiFt|9-#q{4)!mXmob4xm$6m8zrvZWrfXImb+JOdfKKf@b$TO4tNgx z;RHTqXLpH{PsGD_phSVPmq_$^6z^i=udwIu|J|HDoGiM!T0_CQJ@*gxC`@)M&kT%n zaOON}b`rZXKSMh>=i)f@qlq1bvrAjTajqoa9S#NM*i|Iopqjb#FQUInTGiI%Y?HJl pKNCZ0wU_NoDPtR1u_-sF9r0@d_~`EF4bZrOpnwo$&DJB?{{Subu*CoX literal 0 HcmV?d00001 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 %}