From c4c657f5d45de5d04c9f1a31393da8873bfc468e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Thu, 31 Jan 2019 22:09:46 +0200 Subject: [PATCH] Add "Transfer to Another Project" dialog in check's Details page. --- CHANGELOG.md | 1 + hc/api/models.py | 2 +- hc/front/tests/test_transfer.py | 74 +++++++++++++++++++++++++++++ hc/front/urls.py | 1 + hc/front/views.py | 25 ++++++++++ static/js/details.js | 21 ++++++++ templates/front/details.html | 27 +++++++++-- templates/front/transfer_modal.html | 69 +++++++++++++++++++++++++++ 8 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 hc/front/tests/test_transfer.py create mode 100644 templates/front/transfer_modal.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e16571..aaaae345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Add "Email Settings..." dialog and "Subject Must Contain" setting - Database schema: add the Project model - Move project-specific settings to a new "Project Settings" page +- Add a "Transfer to Another Project..." dialog ## 1.4.0 - 2018-12-25 diff --git a/hc/api/models.py b/hc/api/models.py index 4e4db9b9..91cffca6 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -169,7 +169,7 @@ class Check(models.Model): def assign_all_channels(self): channels = Channel.objects.filter(project=self.project) - self.channel_set.add(*channels) + self.channel_set.set(channels) def tags_list(self): return [t.strip() for t in self.tags.split(" ") if t.strip()] diff --git a/hc/front/tests/test_transfer.py b/hc/front/tests/test_transfer.py new file mode 100644 index 00000000..f8f18e3a --- /dev/null +++ b/hc/front/tests/test_transfer.py @@ -0,0 +1,74 @@ +from hc.api.models import Channel, Check +from hc.test import BaseTestCase + + +class TrabsferTestCase(BaseTestCase): + def setUp(self): + super(TrabsferTestCase, self).setUp() + + self.check = Check.objects.create(project=self.bobs_project) + self.url = "/checks/%s/transfer/" % self.check.code + + def test_it_serves_form(self): + self.client.login(username="bob@example.org", password="password") + r = self.client.get(self.url) + self.assertContains(r, "Transfer to Another Project") + + def test_it_works(self): + self.bobs_profile.current_project = self.bobs_project + self.bobs_profile.save() + + self.client.login(username="bob@example.org", password="password") + payload = {"project": self.project.code} + r = self.client.post(self.url, payload, follow=True) + self.assertRedirects(r, "/checks/%s/details/" % self.check.code) + self.assertContains(r, "Check transferred successfully") + + check = Check.objects.get() + self.assertEqual(check.project, self.project) + + # Bob's current project should have been updated + self.bobs_profile.refresh_from_db() + self.assertEqual(self.bobs_profile.current_project, self.project) + + def test_it_obeys_check_limit(self): + # Alice's projects cannot accept checks due to limits: + self.profile.check_limit = 0 + self.profile.save() + + self.client.login(username="bob@example.org", password="password") + payload = {"project": self.project.code} + r = self.client.post(self.url, payload) + self.assertEqual(r.status_code, 400) + + def test_it_reassigns_channels(self): + alices_mail = Channel.objects.create(kind="email", + project=self.project) + + bobs_mail = Channel.objects.create(kind="email", + project=self.bobs_project) + + self.check.channel_set.add(bobs_mail) + + self.client.login(username="bob@example.org", password="password") + payload = {"project": self.project.code} + self.client.post(self.url, payload) + + # alices_mail should be the only assigned channel: + self.assertEqual(self.check.channel_set.get(), alices_mail) + + def test_it_checks_check_ownership(self): + self.client.login(username="charlie@example.org", password="password") + + # Charlie tries to transfer Alice's check into his project + payload = {"project": self.charlies_project.code} + r = self.client.post(self.url, payload) + self.assertEqual(r.status_code, 404) + + def test_it_checks_project_access(self): + self.client.login(username="alice@example.org", password="password") + + # Alice tries to transfer her check into Charlie's project + payload = {"project": self.charlies_project.code} + r = self.client.post(self.url, payload) + self.assertEqual(r.status_code, 404) diff --git a/hc/front/urls.py b/hc/front/urls.py index b82f22af..7c6b4d4e 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -12,6 +12,7 @@ check_urls = [ path('log/', views.log, name="hc-log"), path('status/', views.status_single), path('last_ping/', views.ping_details, name="hc-last-ping"), + path('transfer/', views.transfer, name="hc-transfer"), path('channels//enabled', views.switch_channel, name="hc-switch-channel"), path('pings//', views.ping_details, name="hc-ping-details"), ] diff --git a/hc/front/views.py b/hc/front/views.py index d043e210..9a5f136b 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -461,6 +461,31 @@ def details(request, code): return render(request, "front/details.html", ctx) +@login_required +def transfer(request, code): + check = _get_check_for_user(request, code) + + if request.method == "POST": + target_project = _get_project_for_user(request, request.POST["project"]) + if target_project.num_checks_available() <= 0: + return HttpResponseBadRequest() + + check.project = target_project + check.save() + + check.assign_all_channels() + + request.profile.current_project = target_project + request.profile.save() + + messages.success(request, "Check transferred successfully!") + + return redirect("hc-details", code) + + ctx = {"check": check} + return render(request, "front/transfer_modal.html", ctx) + + @login_required def status_single(request, code): check = _get_check_for_user(request, code) diff --git a/static/js/details.js b/static/js/details.js index 1f141dbd..6187ed75 100644 --- a/static/js/details.js +++ b/static/js/details.js @@ -117,4 +117,25 @@ $(function () { var format = ev.target.getAttribute("data-format"); switchDateFormat(format); }); + + + var transferFormLoadStarted = false; + $("#transfer-btn").on("mouseenter click", function() { + if (transferFormLoadStarted) + return; + + transferFormLoadStarted = true; + $.get(this.dataset.url, function(data) { + $("#transfer-modal" ).html(data); + $("#target-project").selectpicker(); + }); + }); + + // Enable the submit button in transfer form when user selects + // the target project: + $("#transfer-modal").on("change", "#target-project", function() { + $("#transfer-confirm").prop("disabled", !this.value); + }); + + }); diff --git a/templates/front/details.html b/templates/front/details.html index 07a5bd63..72ce0b20 100644 --- a/templates/front/details.html +++ b/templates/front/details.html @@ -12,14 +12,21 @@ {{ check.name_then_code }} + {{ check.project }} {% for tag in check.tags_list %} {{ tag }} {% endfor %} + {% if messages %} +
+ {% for message in messages %} +

{{ message }}

+ {% endfor %} +
+ {% endif %}
- {% if check.desc %}

Description

@@ -139,7 +146,7 @@ data-grace="{{ check.grace.total_seconds }}" data-schedule="{{ check.schedule }}" data-tz="{{ check.tz }}"> - Change Schedule + Change Schedule…
@@ -163,14 +170,21 @@
-

Remove

-

Permanently remove this check from your account.

+

Danger Zone

+

Transfer to a different project, or permanently remove this check.

+
+ + class="btn btn-sm btn-default">Remove
@@ -210,6 +224,9 @@ + + {% include "front/update_name_modal.html" %} {% include "front/update_timeout_modal.html" %} {% include "front/show_usage_modal.html" %} diff --git a/templates/front/transfer_modal.html b/templates/front/transfer_modal.html new file mode 100644 index 00000000..29af8e7e --- /dev/null +++ b/templates/front/transfer_modal.html @@ -0,0 +1,69 @@ +{% load hc_extras %} + + \ No newline at end of file