0
0
Fork 0
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:
Pēteris Caune 2023-12-08 11:59:55 +02:00
parent f33c296349
commit 0abfe58cef
No known key found for this signature in database
GPG key ID: E28D7679E9A9EDE2
8 changed files with 128 additions and 35 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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