mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-03 12:25:31 +00:00
Implement OnCalendar schedules in the "Add Check" dialog
This commit is contained in:
parent
f33c296349
commit
0abfe58cef
8 changed files with 128 additions and 35 deletions
hc/front
static
templates/front
|
@ -85,9 +85,11 @@ class NameTagsForm(forms.Form):
|
|||
|
||||
|
||||
class AddCheckForm(NameTagsForm):
|
||||
kind = forms.ChoiceField(choices=(("simple", "simple"), ("cron", "cron")))
|
||||
kind = forms.ChoiceField(
|
||||
choices=(("simple", "simple"), ("cron", "cron"), ("oncalendar", "oncalendar"))
|
||||
)
|
||||
timeout = forms.IntegerField(min_value=60, max_value=31536000)
|
||||
schedule = forms.CharField(max_length=100, validators=[CronValidator()])
|
||||
schedule = forms.CharField(required=False, max_length=100)
|
||||
tz = forms.CharField(max_length=36, validators=[TimezoneValidator()])
|
||||
grace = forms.IntegerField(min_value=60, max_value=31536000)
|
||||
|
||||
|
@ -97,6 +99,21 @@ class AddCheckForm(NameTagsForm):
|
|||
def clean_grace(self) -> td:
|
||||
return td(seconds=self.cleaned_data["grace"])
|
||||
|
||||
def clean_schedule(self):
|
||||
kind = self.cleaned_data.get("kind")
|
||||
validator = None
|
||||
if kind == "cron":
|
||||
validator = CronValidator()
|
||||
elif kind == "oncalendar":
|
||||
validator = OnCalendarValidator()
|
||||
else:
|
||||
# If kind is not cron or oncalendar, ignore the passed in value
|
||||
# and use "* * * * *" instead.
|
||||
return "* * * * *"
|
||||
|
||||
validator(self.cleaned_data["schedule"])
|
||||
return self.cleaned_data["schedule"]
|
||||
|
||||
|
||||
class FilteringRulesForm(forms.Form):
|
||||
filter_subject = forms.BooleanField(required=False)
|
||||
|
|
|
@ -19,7 +19,6 @@ class AddCheckTestCase(BaseTestCase):
|
|||
"kind": "simple",
|
||||
"timeout": "120",
|
||||
"grace": "60",
|
||||
"schedule": "* * * * *",
|
||||
"tz": "Europe/Riga",
|
||||
}
|
||||
payload.update(kwargs)
|
||||
|
@ -50,11 +49,24 @@ class AddCheckTestCase(BaseTestCase):
|
|||
|
||||
def test_it_saves_cron_schedule(self) -> None:
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, self._payload(kind="cron"))
|
||||
r = self.client.post(self.url, self._payload(kind="cron", schedule="0 0 * * *"))
|
||||
|
||||
check = Check.objects.get()
|
||||
self.assertEqual(check.project, self.project)
|
||||
self.assertEqual(check.kind, "cron")
|
||||
self.assertEqual(check.schedule, "0 0 * * *")
|
||||
|
||||
self.assertRedirects(r, self.redirect_url)
|
||||
|
||||
def test_it_saves_oncalendar_schedule(self) -> None:
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
payload = self._payload(kind="oncalendar", schedule="12:34")
|
||||
r = self.client.post(self.url, payload)
|
||||
|
||||
check = Check.objects.get()
|
||||
self.assertEqual(check.project, self.project)
|
||||
self.assertEqual(check.kind, "oncalendar")
|
||||
self.assertEqual(check.schedule, "12:34")
|
||||
|
||||
self.assertRedirects(r, self.redirect_url)
|
||||
|
||||
|
@ -81,12 +93,21 @@ class AddCheckTestCase(BaseTestCase):
|
|||
|
||||
def test_it_validates_cron_expression(self) -> None:
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, self._payload(schedule="* * *"))
|
||||
r = self.client.post(self.url, self._payload(kind="cron", schedule="* * *"))
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def test_it_validates_cron_expression_with_no_matches(self) -> None:
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, self._payload(schedule="* * */100 * MON#2"))
|
||||
r = self.client.post(
|
||||
self.url, self._payload(kind="cron", schedule="* * */100 * MON#2")
|
||||
)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def test_it_validates_oncalendar_expression(self) -> None:
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(
|
||||
self.url, self._payload(kind="oncalendar", schedule="12:345")
|
||||
)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def test_it_validates_tz(self) -> None:
|
||||
|
|
|
@ -10,16 +10,28 @@ class ValidateScheduleTestCase(BaseTestCase):
|
|||
super().setUp()
|
||||
self.url = "/checks/validate_schedule/"
|
||||
|
||||
def _url(self, schedule: str) -> str:
|
||||
return "/checks/validate_schedule/?" + urlencode({"schedule": schedule})
|
||||
def _url(self, schedule: str, kind: str) -> str:
|
||||
params = {"schedule": schedule, "kind": kind}
|
||||
return "/checks/validate_schedule/?" + urlencode(params)
|
||||
|
||||
def test_it_works(self) -> None:
|
||||
r = self.client.get(self._url("* * * * *"))
|
||||
def test_it_validates_cron_schedule(self) -> None:
|
||||
r = self.client.get(self._url("* * * * *", "cron"))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(r.json()["result"])
|
||||
|
||||
def test_it_validates_oncalendar_schedule(self) -> None:
|
||||
r = self.client.get(self._url("12:34", "oncalendar"))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(r.json()["result"])
|
||||
|
||||
def test_it_rejects_bad_schedules(self) -> None:
|
||||
# Bad cron schedules
|
||||
for v in ["a", "* * * *", "1-1000 * * * *", "0 0 */100 * MON#2"]:
|
||||
r = self.client.get(self._url(v))
|
||||
r = self.client.get(self._url(v, "cron"))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertFalse(r.json()["result"])
|
||||
# Bad oncalendar schedules
|
||||
for v in ["12:345"]:
|
||||
r = self.client.get(self._url(v, "oncalendar"))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertFalse(r.json()["result"])
|
||||
|
|
|
@ -665,14 +665,23 @@ def oncalendar_preview(request: HttpRequest) -> HttpResponse:
|
|||
|
||||
|
||||
def validate_schedule(request: HttpRequest) -> HttpResponse:
|
||||
kind = request.GET.get("kind", "")
|
||||
iterator: type[CronSim] | type[OnCalendar]
|
||||
if kind == "cron":
|
||||
iterator = CronSim
|
||||
elif kind == "oncalendar":
|
||||
iterator = OnCalendar
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
schedule = request.GET.get("schedule", "")
|
||||
result = True
|
||||
try:
|
||||
# Does cronsim accept the schedule?
|
||||
it = CronSim(schedule, now())
|
||||
# Does cronsim/oncalendar accept the schedule?
|
||||
it = iterator(schedule, now())
|
||||
# Can it calculate the next datetime?
|
||||
next(it)
|
||||
except (CronSimError, StopIteration):
|
||||
except (CronSimError, OnCalendarError, StopIteration):
|
||||
result = False
|
||||
|
||||
return JsonResponse({"result": result})
|
||||
|
|
|
@ -81,16 +81,27 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Add Check Modal */
|
||||
|
||||
#add-check-modal .modal-body {
|
||||
padding: 15px 40px;
|
||||
}
|
||||
|
||||
.simple .create-cron-extra {
|
||||
#add-check-kind label {
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.create-simple-extra,
|
||||
.create-cron-extra,
|
||||
.create-oncalendar-extra {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cron .create-simple-extra {
|
||||
display: none;
|
||||
.simple .create-simple-extra,
|
||||
.cron .create-cron-extra,
|
||||
.oncalendar .create-oncalendar-extra {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#add-check-period,
|
||||
|
|
|
@ -3,7 +3,8 @@ $(function () {
|
|||
var modal = $("#add-check-modal");
|
||||
var period = document.getElementById("add-check-period");
|
||||
var periodUnit = document.getElementById("add-check-period-unit");
|
||||
var scheduleField = document.getElementById("add-check-schedule");
|
||||
var cronField = document.getElementById("add-check-schedule");
|
||||
var onCalendarField = document.getElementById("add-check-schedule-oncalendar");
|
||||
var grace = document.getElementById("add-check-grace");
|
||||
var graceUnit = document.getElementById("add-check-grace-unit");
|
||||
|
||||
|
@ -25,15 +26,14 @@ $(function () {
|
|||
|
||||
function updateScheduleExtras() {
|
||||
var kind = $('#add-check-modal input[name=kind]:checked').val();
|
||||
modal.removeClass("cron").removeClass("simple").addClass(kind);
|
||||
|
||||
if (kind == "simple" && !scheduleField.checkValidity()) {
|
||||
scheduleField.setCustomValidity("");
|
||||
scheduleField.value = "* * * * *";
|
||||
}
|
||||
modal.removeClass("simple").removeClass("cron").removeClass("oncalendar").addClass(kind);
|
||||
// Include cron schedule in POST data only if kind = "cron"
|
||||
cronField.disabled = kind != "cron";
|
||||
// Include OnCalendar schedule in POST data only if kind = "oncalendar"
|
||||
onCalendarField.disabled = kind != "oncalendar";
|
||||
}
|
||||
|
||||
// Show and hide fields when user clicks simple/cron radio buttons
|
||||
// Show and hide fields when user clicks simple/cron/oncalendar radio buttons
|
||||
$("#add-check-modal input[type=radio][name=kind]").change(updateScheduleExtras);
|
||||
|
||||
modal.on("shown.bs.modal", function() {
|
||||
|
@ -69,22 +69,26 @@ $(function () {
|
|||
|
||||
var currentSchedule = "";
|
||||
function validateSchedule() {
|
||||
var schedule = scheduleField.value;
|
||||
var kind = $('#add-check-modal input[name=kind]:checked').val();
|
||||
if (kind == "simple") return;
|
||||
|
||||
var field = kind == "cron" ? cronField : onCalendarField;
|
||||
|
||||
// Return early if the schedule has not changed
|
||||
if (schedule == currentSchedule)
|
||||
if (field.value == currentSchedule)
|
||||
return;
|
||||
|
||||
currentSchedule = schedule;
|
||||
currentSchedule = field.value;
|
||||
var token = $('input[name=csrfmiddlewaretoken]').val();
|
||||
$.getJSON(base + "/checks/validate_schedule/", {schedule: schedule}, function(data) {
|
||||
if (schedule != currentSchedule)
|
||||
var payload = {kind: kind, schedule: field.value};
|
||||
$.getJSON(base + "/checks/validate_schedule/", payload, function(data) {
|
||||
if (field.value != currentSchedule)
|
||||
return; // ignore stale results
|
||||
|
||||
scheduleField.setCustomValidity(data.result ? "" : "Please enter a valid cron expression");
|
||||
field.setCustomValidity(data.result ? "" : "Please enter a valid expression");
|
||||
});
|
||||
}
|
||||
|
||||
$("#add-check-schedule").on("keyup change", validateSchedule);
|
||||
|
||||
$("#add-check-schedule-oncalendar").on("keyup change", validateSchedule);
|
||||
});
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
<label class="col-sm-3 control-label">
|
||||
Schedule
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div id="add-check-kind" class="col-sm-9">
|
||||
<label class="radio-container">
|
||||
<input type="radio" name="kind" value="simple" checked>
|
||||
<span class="radiomark"></span>
|
||||
|
@ -83,6 +83,11 @@
|
|||
<span class="radiomark"></span>
|
||||
Cron
|
||||
</label>
|
||||
<label class="radio-container">
|
||||
<input type="radio" name="kind" value="oncalendar">
|
||||
<span class="radiomark"></span>
|
||||
OnCalendar
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -126,7 +131,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group create-cron-extra">
|
||||
<div class="form-group create-oncalendar-extra">
|
||||
<label for="add-check-schedule-oncalendar" class="col-sm-3 control-label">
|
||||
OnCalendar Expression(s)
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<textarea
|
||||
id="add-check-schedule-oncalendar"
|
||||
name="schedule"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
maxlength="100">*-*-* *:*:*</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group create-cron-extra create-oncalendar-extra">
|
||||
<label for="add-check-tz" class="col-sm-3 control-label">
|
||||
Time Zone
|
||||
</label>
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% if check.kind == "cron" %}
|
||||
{% if check.kind == "cron" or check.kind == "oncalendar" %}
|
||||
<tr>
|
||||
<th>Time Zone</th>
|
||||
<td>
|
||||
|
|
Loading…
Add table
Reference in a new issue