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
|
- Add "Email Settings..." dialog and "Subject Must Contain" setting
|
||||||
- Database schema: add the Project model
|
- Database schema: add the Project model
|
||||||
- Move project-specific settings to a new "Project Settings" page
|
- Move project-specific settings to a new "Project Settings" page
|
||||||
|
- Add a "Transfer to Another Project..." dialog
|
||||||
|
|
||||||
|
|
||||||
## 1.4.0 - 2018-12-25
|
## 1.4.0 - 2018-12-25
|
||||||
|
@ -169,7 +169,7 @@ class Check(models.Model):
|
|||||||
|
|
||||||
def assign_all_channels(self):
|
def assign_all_channels(self):
|
||||||
channels = Channel.objects.filter(project=self.project)
|
channels = Channel.objects.filter(project=self.project)
|
||||||
self.channel_set.add(*channels)
|
self.channel_set.set(channels)
|
||||||
|
|
||||||
def tags_list(self):
|
def tags_list(self):
|
||||||
return [t.strip() for t in self.tags.split(" ") if t.strip()]
|
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('log/', views.log, name="hc-log"),
|
||||||
path('status/', views.status_single),
|
path('status/', views.status_single),
|
||||||
path('last_ping/', views.ping_details, name="hc-last-ping"),
|
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('channels/<uuid:channel_code>/enabled', views.switch_channel, name="hc-switch-channel"),
|
||||||
path('pings/<int:n>/', views.ping_details, name="hc-ping-details"),
|
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)
|
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
|
@login_required
|
||||||
def status_single(request, code):
|
def status_single(request, code):
|
||||||
check = _get_check_for_user(request, code)
|
check = _get_check_for_user(request, code)
|
||||||
|
@ -117,4 +117,25 @@ $(function () {
|
|||||||
var format = ev.target.getAttribute("data-format");
|
var format = ev.target.getAttribute("data-format");
|
||||||
switchDateFormat(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 }}
|
{{ check.name_then_code }}
|
||||||
<button id="edit-name" class="btn btn-sm btn-default">Edit</button>
|
<button id="edit-name" class="btn btn-sm btn-default">Edit</button>
|
||||||
</h1>
|
</h1>
|
||||||
|
<span class="label label-tag">{{ check.project }}</span>
|
||||||
{% for tag in check.tags_list %}
|
{% for tag in check.tags_list %}
|
||||||
<span class="label label-tag">{{ tag }}</span>
|
<span class="label label-tag">{{ tag }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</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">
|
<div class="col-sm-5">
|
||||||
|
|
||||||
{% if check.desc %}
|
{% if check.desc %}
|
||||||
<div class="details-block">
|
<div class="details-block">
|
||||||
<h2>Description</h2>
|
<h2>Description</h2>
|
||||||
@ -139,7 +146,7 @@
|
|||||||
data-grace="{{ check.grace.total_seconds }}"
|
data-grace="{{ check.grace.total_seconds }}"
|
||||||
data-schedule="{{ check.schedule }}"
|
data-schedule="{{ check.schedule }}"
|
||||||
data-tz="{{ check.tz }}">
|
data-tz="{{ check.tz }}">
|
||||||
Change Schedule</button>
|
Change Schedule…</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -163,14 +170,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="details-block">
|
<div class="details-block">
|
||||||
<h2>Remove</h2>
|
<h2>Danger Zone</h2>
|
||||||
<p>Permanently remove this check from your account.</p>
|
<p>Transfer to a different project, or permanently remove this check.</p>
|
||||||
|
|
||||||
<div class="text-right">
|
<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
|
<button
|
||||||
id="details-remove-check"
|
id="details-remove-check"
|
||||||
data-toggle="modal"
|
data-toggle="modal"
|
||||||
data-target="#remove-check-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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -210,6 +224,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="transfer-modal" class="modal">
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include "front/update_name_modal.html" %}
|
{% include "front/update_name_modal.html" %}
|
||||||
{% include "front/update_timeout_modal.html" %}
|
{% include "front/update_timeout_modal.html" %}
|
||||||
{% include "front/show_usage_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