forked from GithubBackups/healthchecks
Add "Transfer to Another Project" dialog in check's Details page.
This commit is contained in:
parent
543a8c7c6a
commit
c4c657f5d4
@ -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
|
||||
|
@ -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()]
|
||||
|
74
hc/front/tests/test_transfer.py
Normal file
74
hc/front/tests/test_transfer.py
Normal file
@ -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)
|
@ -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/<uuid:channel_code>/enabled', views.switch_channel, name="hc-switch-channel"),
|
||||
path('pings/<int:n>/', views.ping_details, name="hc-ping-details"),
|
||||
]
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
@ -12,14 +12,21 @@
|
||||
{{ check.name_then_code }}
|
||||
<button id="edit-name" class="btn btn-sm btn-default">Edit</button>
|
||||
</h1>
|
||||
<span class="label label-tag">{{ check.project }}</span>
|
||||
{% for tag in check.tags_list %}
|
||||
<span class="label label-tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if messages %}
|
||||
<div class="col-sm-12">
|
||||
{% for message in messages %}
|
||||
<p class="alert alert-{{ message.tags }}">{{ message }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-sm-5">
|
||||
|
||||
{% if check.desc %}
|
||||
<div class="details-block">
|
||||
<h2>Description</h2>
|
||||
@ -139,7 +146,7 @@
|
||||
data-grace="{{ check.grace.total_seconds }}"
|
||||
data-schedule="{{ check.schedule }}"
|
||||
data-tz="{{ check.tz }}">
|
||||
Change Schedule</button>
|
||||
Change Schedule…</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -163,14 +170,21 @@
|
||||
</div>
|
||||
|
||||
<div class="details-block">
|
||||
<h2>Remove</h2>
|
||||
<p>Permanently remove this check from your account.</p>
|
||||
<h2>Danger Zone</h2>
|
||||
<p>Transfer to a different project, or permanently remove this check.</p>
|
||||
|
||||
<div class="text-right">
|
||||
<button
|
||||
id="transfer-btn"
|
||||
data-toggle="modal"
|
||||
data-target="#transfer-modal"
|
||||
data-url="{% url 'hc-transfer' check.code %}"
|
||||
class="btn btn-sm btn-default">Transfer to Another Project…</button>
|
||||
<button
|
||||
id="details-remove-check"
|
||||
data-toggle="modal"
|
||||
data-target="#remove-check-modal"
|
||||
class="btn btn-sm btn-default">Remove This Check</button>
|
||||
class="btn btn-sm btn-default">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -210,6 +224,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="transfer-modal" class="modal">
|
||||
</div>
|
||||
|
||||
{% include "front/update_name_modal.html" %}
|
||||
{% include "front/update_timeout_modal.html" %}
|
||||
{% include "front/show_usage_modal.html" %}
|
||||
|
69
templates/front/transfer_modal.html
Normal file
69
templates/front/transfer_modal.html
Normal file
@ -0,0 +1,69 @@
|
||||
{% load hc_extras %}
|
||||
|
||||
<div class="modal-dialog">
|
||||
<form
|
||||
action="{% url 'hc-transfer' check.code %}"
|
||||
class="form-horizontal"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="update-timeout-title">Transfer to Another Project</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="update-name-input" class="col-sm-4 control-label">
|
||||
Check
|
||||
</label>
|
||||
<div class="col-sm-7">
|
||||
<p class="form-control-static">{{ check.name_then_code }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="update-name-input" class="col-sm-4 control-label">
|
||||
Target Project
|
||||
</label>
|
||||
<div class="col-sm-7">
|
||||
<select
|
||||
id="target-project"
|
||||
name="project"
|
||||
title="Select..."
|
||||
class="form-control selectpicker">
|
||||
{% for project in request.profile.projects.all %}
|
||||
{% if project == check.project %}
|
||||
<option disabled data-subtext="(current project)">
|
||||
{{ project }}
|
||||
</option>
|
||||
{% elif project.num_checks_available > 0 %}
|
||||
<option value="{{ project.code }}">{{ project }}</option>
|
||||
{% else %}
|
||||
<option disabled data-subtext="(at check limit)">
|
||||
{{ project }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-warning">
|
||||
<strong>Integrations will get reset.</strong>
|
||||
The check will lose its current notification
|
||||
channels, and will be assigned all notification
|
||||
channels of the target project.
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button
|
||||
id="transfer-confirm"
|
||||
disabled
|
||||
type="submit"
|
||||
class="btn btn-primary">Transfer</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
Loading…
x
Reference in New Issue
Block a user