Browse Source

Add "Transfer to Another Project" dialog in check's Details page.

pull/217/head
Pēteris Caune 6 years ago
parent
commit
c4c657f5d4
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
8 changed files with 214 additions and 6 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +1
    -1
      hc/api/models.py
  3. +74
    -0
      hc/front/tests/test_transfer.py
  4. +1
    -0
      hc/front/urls.py
  5. +25
    -0
      hc/front/views.py
  6. +21
    -0
      static/js/details.js
  7. +22
    -5
      templates/front/details.html
  8. +69
    -0
      templates/front/transfer_modal.html

+ 1
- 0
CHANGELOG.md View File

@ -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


+ 1
- 1
hc/api/models.py View File

@ -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
- 0
hc/front/tests/test_transfer.py View 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="[email protected]", 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="[email protected]", 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="[email protected]", 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="[email protected]", 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="[email protected]", 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="[email protected]", 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)

+ 1
- 0
hc/front/urls.py View File

@ -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"),
] ]


+ 25
- 0
hc/front/views.py View File

@ -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)


+ 21
- 0
static/js/details.js View File

@ -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);
});
}); });

+ 22
- 5
templates/front/details.html View File

@ -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&hellip;</button>
</div> </div>
</div> </div>
@ -163,14 +170,21 @@
</div> </div>
<div class="details-block"> <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"> <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&hellip;</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
- 0
templates/front/transfer_modal.html View 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">&times;</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…
Cancel
Save