4 Commits

Author SHA1 Message Date
7a552715fe (ui) Clarify run action flows
Move run cancellation and review actions out of the page header into
dedicated action panels with clearer operator copy and consistent form
button styling.

Refs #25
2026-05-21 14:25:26 +02:00
0f0de5dc30 (ui) Standardize list filter actions
Give run, snapshot, schedule, purged snapshot, and log filters the same
responsive form layout with consistent Apply/Clear actions.

Refs #25
2026-05-21 14:22:11 +02:00
1604f0f6f4 (ui) Clarify destructive action flows
Make retention apply, incomplete cleanup, and SSH key deletion visibly
destructive with warning copy, danger styling, and consistent cancel actions
while keeping the existing confirmation requirements intact.

Refs #25
2026-05-21 14:17:07 +02:00
af548f11c4 (ui) Standardize primary form actions
Add shared form action styling and consistent Cancel links across config,
schedule, and SSH key forms so create/edit flows behave predictably.

Refs #25
2026-05-21 14:13:05 +02:00
15 changed files with 156 additions and 36 deletions

View File

@@ -280,6 +280,17 @@
} }
button.secondary:hover, button.secondary:hover,
.button-link.secondary:hover { background: #eef3f8; } .button-link.secondary:hover { background: #eef3f8; }
button.danger,
.button-link.danger {
background: var(--failed);
border-color: var(--failed);
color: #fff;
}
button.danger:hover,
.button-link.danger:hover {
background: #842828;
border-color: #842828;
}
button.compact, button.compact,
.button-link.compact { .button-link.compact {
font-size: 12px; font-size: 12px;
@@ -679,6 +690,30 @@
.message.error { border-color: #e8b4b4; background: #fff0f0; color: var(--failed); } .message.error { border-color: #e8b4b4; background: #fff0f0; color: var(--failed); }
.message.warning { border-color: #e7cf8a; background: #fff8df; color: var(--running); } .message.warning { border-color: #e7cf8a; background: #fff8df; color: var(--running); }
.form-grid { display: grid; gap: 15px; max-width: 720px; } .form-grid { display: grid; gap: 15px; max-width: 720px; }
.filter-form {
align-items: end;
display: grid;
gap: 15px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
max-width: none;
}
.form-actions {
align-items: center;
border-top: 1px solid var(--border);
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 4px;
padding-top: 15px;
}
.form-actions .button-link.secondary { margin-left: auto; }
.filter-form .form-actions {
border-top: 0;
justify-content: flex-end;
margin-top: 0;
padding-top: 0;
}
.filter-form .form-actions .button-link.secondary { margin-left: 0; }
.field { display: grid; gap: 6px; } .field { display: grid; gap: 6px; }
.field label { font-weight: 700; } .field label { font-weight: 700; }
.field input[type="text"], .field input[type="number"], .field select, .field textarea { .field input[type="text"], .field input[type="number"], .field select, .field textarea {
@@ -736,6 +771,7 @@
.host-control-grid { grid-template-columns: 1fr; } .host-control-grid { grid-template-columns: 1fr; }
.schedule-row { grid-template-columns: 1fr; } .schedule-row { grid-template-columns: 1fr; }
.schedule-time { justify-items: start; text-align: left; } .schedule-time { justify-items: start; text-align: left; }
.form-actions .button-link.secondary { margin-left: 0; }
.host-card-header { display: grid; } .host-card-header { display: grid; }
.host-card-status { justify-content: flex-start; max-width: none; } .host-card-status { justify-content: flex-start; max-width: none; }
.host-card-layout { grid-template-columns: 1fr; } .host-card-layout { grid-template-columns: 1fr; }

View File

@@ -33,8 +33,9 @@
</div> </div>
{% endfor %} {% endfor %}
<div class="actions"> <div class="form-actions">
<button type="submit">Save global config</button> <button type="submit">Save global config</button>
<a class="button-link secondary" href="{% url 'dashboard' %}">Cancel</a>
</div> </div>
</form> </form>
</section> </section>

View File

@@ -433,7 +433,7 @@
</div> </div>
{% endfor %} {% endfor %}
<div class="actions"> <div class="form-actions">
<button type="submit" {% if not can_queue_dry_run and not can_queue_real_backup %}disabled{% endif %}>Queue with options</button> <button type="submit" {% if not can_queue_dry_run and not can_queue_real_backup %}disabled{% endif %}>Queue with options</button>
</div> </div>
</form> </form>

View File

@@ -33,8 +33,13 @@
</div> </div>
{% endfor %} {% endfor %}
<div class="actions"> <div class="form-actions">
<button type="submit">{% if host %}Save config{% else %}Create host{% endif %}</button> <button type="submit">{% if host %}Save config{% else %}Create host{% endif %}</button>
{% if host %}
<a class="button-link secondary" href="{% url 'host_detail' host.host %}">Cancel</a>
{% else %}
<a class="button-link secondary" href="{% url 'dashboard' %}">Cancel</a>
{% endif %}
</div> </div>
</form> </form>
</section> </section>

View File

@@ -16,7 +16,7 @@
<section class="panel"> <section class="panel">
<h2>Filter</h2> <h2>Filter</h2>
<form method="get" class="form-grid"> <form method="get" class="filter-form">
<div class="field"> <div class="field">
<label for="unit">Unit</label> <label for="unit">Unit</label>
<select id="unit" name="unit"> <select id="unit" name="unit">
@@ -54,8 +54,9 @@
<label for="q">Message contains</label> <label for="q">Message contains</label>
<input id="q" name="q" value="{{ query }}"> <input id="q" name="q" value="{{ query }}">
</div> </div>
<div class="actions"> <div class="form-actions">
<button type="submit">Filter logs</button> <button type="submit">Filter logs</button>
<a class="button-link secondary" href="{% url 'logs' %}">Clear</a>
</div> </div>
</form> </form>
</section> </section>

View File

@@ -16,7 +16,7 @@
<section class="panel"> <section class="panel">
<h2>Filters</h2> <h2>Filters</h2>
<form method="get" class="form-grid"> <form method="get" class="filter-form">
<div class="field"> <div class="field">
<label for="host">Host</label> <label for="host">Host</label>
<select id="host" name="host"> <select id="host" name="host">
@@ -35,7 +35,7 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="actions"> <div class="form-actions">
<button type="submit">Apply filters</button> <button type="submit">Apply filters</button>
<a class="button-link secondary" href="{% url 'purged_snapshots' %}">Clear</a> <a class="button-link secondary" href="{% url 'purged_snapshots' %}">Clear</a>
</div> </div>

View File

@@ -104,8 +104,12 @@
</section> </section>
{% if plan.delete %} {% if plan.delete %}
<section class="panel"> <section class="panel highlight warning">
<h2>Apply Retention</h2> <h2>Apply Retention</h2>
<p class="muted">
This permanently deletes the snapshot directories listed in Would Delete. Confirm the host and delete count
before applying the plan.
</p>
<form method="post" action="{% url 'apply_host_retention' host.host %}" class="form-grid"> <form method="post" action="{% url 'apply_host_retention' host.host %}" class="form-grid">
{% csrf_token %} {% csrf_token %}
{{ apply_form.non_field_errors }} {{ apply_form.non_field_errors }}
@@ -138,8 +142,9 @@
<div class="helptext">{{ apply_form.confirm_delete_count.help_text }}</div> <div class="helptext">{{ apply_form.confirm_delete_count.help_text }}</div>
</div> </div>
<div class="actions"> <div class="form-actions">
<button type="submit">Apply retention</button> <button type="submit" class="danger">Apply retention</button>
<a class="button-link secondary" href="{% url 'host_retention_plan' host.host %}?kind={{ kind }}">Cancel</a>
</div> </div>
</form> </form>
</section> </section>
@@ -200,6 +205,10 @@
</table> </table>
<h3>Cleanup Incomplete Snapshots</h3> <h3>Cleanup Incomplete Snapshots</h3>
<p class="muted">
This deletes only incomplete snapshot directories and their tracking records. Successful manual and scheduled
snapshots are not touched.
</p>
<form method="post" action="{% url 'cleanup_host_incomplete_snapshots' host.host %}" class="form-grid"> <form method="post" action="{% url 'cleanup_host_incomplete_snapshots' host.host %}" class="form-grid">
{% csrf_token %} {% csrf_token %}
{{ incomplete_cleanup_form.non_field_errors }} {{ incomplete_cleanup_form.non_field_errors }}
@@ -225,8 +234,9 @@
<div class="helptext">{{ incomplete_cleanup_form.confirm_delete_count.help_text }}</div> <div class="helptext">{{ incomplete_cleanup_form.confirm_delete_count.help_text }}</div>
</div> </div>
<div class="actions"> <div class="form-actions">
<button type="submit">Delete incomplete snapshots</button> <button type="submit" class="danger">Delete incomplete snapshots</button>
<a class="button-link secondary" href="{% url 'host_retention_plan' host.host %}?kind={{ kind }}">Cancel</a>
</div> </div>
</form> </form>
</section> </section>

View File

@@ -11,20 +11,6 @@
</div> </div>
<section class="actions" aria-label="Run actions"> <section class="actions" aria-label="Run actions">
<a class="button-link" href="{% url 'host_detail' run.host.host %}">Back to host</a> <a class="button-link" href="{% url 'host_detail' run.host.host %}">Back to host</a>
{% if can_cancel %}
<form method="post" action="{% url 'cancel_run' run.id %}">
{% csrf_token %}
<button type="submit" class="secondary">Cancel run</button>
</form>
{% endif %}
{% if run.status == "failed" or run.status == "warning" %}
{% if not run.reviewed_at %}
<form method="post" action="{% url 'resolve_run_review' run.id %}">
{% csrf_token %}
<button type="submit" class="secondary">Mark reviewed</button>
</form>
{% endif %}
{% endif %}
</section> </section>
</header> </header>
@@ -35,6 +21,22 @@
<div class="metric"><div class="label">Rsync</div><div class="value">{{ run.rsync_exit_code|default:"" }}</div></div> <div class="metric"><div class="label">Rsync</div><div class="value">{{ run.rsync_exit_code|default:"" }}</div></div>
</section> </section>
{% if can_cancel %}
<section class="panel highlight warning">
<h2>Run Control</h2>
<p>
Cancelling a queued run stops it immediately. Cancelling a running run asks the worker to stop
and records the cancellation request on this run.
</p>
<form method="post" action="{% url 'cancel_run' run.id %}">
{% csrf_token %}
<div class="form-actions">
<button type="submit" class="danger">Cancel run</button>
</div>
</form>
</section>
{% endif %}
{% if failure %} {% if failure %}
<section class="panel highlight failed"> <section class="panel highlight failed">
<h2>Failure</h2> <h2>Failure</h2>
@@ -46,6 +48,21 @@
</section> </section>
{% endif %} {% endif %}
{% if run.status == "failed" or run.status == "warning" %}
{% if not run.reviewed_at %}
<section class="panel highlight warning">
<h2>Review Required</h2>
<p>Mark this run as reviewed after you have checked the failure or warning and no longer need it in the action queue.</p>
<form method="post" action="{% url 'resolve_run_review' run.id %}">
{% csrf_token %}
<div class="form-actions">
<button type="submit" class="secondary">Mark reviewed</button>
</div>
</form>
</section>
{% endif %}
{% endif %}
{% if run.reviewed_at %} {% if run.reviewed_at %}
<section class="panel highlight success"> <section class="panel highlight success">
<h2>Review</h2> <h2>Review</h2>

View File

@@ -16,7 +16,7 @@
<section class="panel"> <section class="panel">
<h2>Filters</h2> <h2>Filters</h2>
<form method="get" class="form-grid"> <form method="get" class="filter-form">
<div class="field"> <div class="field">
<label for="status">Status</label> <label for="status">Status</label>
<select id="status" name="status"> <select id="status" name="status">
@@ -52,7 +52,7 @@
<option value="reviewed" {% if selected_review == "reviewed" %}selected{% endif %}>Reviewed</option> <option value="reviewed" {% if selected_review == "reviewed" %}selected{% endif %}>Reviewed</option>
</select> </select>
</div> </div>
<div class="actions"> <div class="form-actions">
<button type="submit">Apply filters</button> <button type="submit">Apply filters</button>
<a class="button-link secondary" href="{% url 'runs_list' %}">Clear</a> <a class="button-link secondary" href="{% url 'runs_list' %}">Clear</a>
</div> </div>

View File

@@ -30,8 +30,9 @@
</div> </div>
{% endfor %} {% endfor %}
<div class="actions"> <div class="form-actions">
<button type="submit">Save schedule</button> <button type="submit">Save schedule</button>
<a class="button-link secondary" href="{% url 'host_detail' host.host %}">Cancel</a>
</div> </div>
</form> </form>
</section> </section>

View File

@@ -16,7 +16,7 @@
<section class="panel"> <section class="panel">
<h2>Filters</h2> <h2>Filters</h2>
<form method="get" class="form-grid"> <form method="get" class="filter-form">
<div class="field"> <div class="field">
<label for="host">Host</label> <label for="host">Host</label>
<select id="host" name="host"> <select id="host" name="host">
@@ -42,7 +42,7 @@
<option value="no" {% if selected_prune == "no" %}selected{% endif %}>Prune disabled</option> <option value="no" {% if selected_prune == "no" %}selected{% endif %}>Prune disabled</option>
</select> </select>
</div> </div>
<div class="actions"> <div class="form-actions">
<button type="submit">Apply filters</button> <button type="submit">Apply filters</button>
<a class="button-link secondary" href="{% url 'schedules_list' %}">Clear</a> <a class="button-link secondary" href="{% url 'schedules_list' %}">Clear</a>
</div> </div>

View File

@@ -16,7 +16,7 @@
<section class="panel"> <section class="panel">
<h2>Filters</h2> <h2>Filters</h2>
<form method="get" class="form-grid"> <form method="get" class="filter-form">
<div class="field"> <div class="field">
<label for="host">Host</label> <label for="host">Host</label>
<select id="host" name="host"> <select id="host" name="host">
@@ -44,7 +44,7 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="actions"> <div class="form-actions">
<button type="submit">Apply filters</button> <button type="submit">Apply filters</button>
<a class="button-link secondary" href="{% url 'snapshots_list' %}">Clear</a> <a class="button-link secondary" href="{% url 'snapshots_list' %}">Clear</a>
</div> </div>

View File

@@ -41,8 +41,9 @@
</div> </div>
{% endfor %} {% endfor %}
<div class="actions"> <div class="form-actions">
<button type="submit">{% if credential %}Save SSH key{% else %}Create SSH key{% endif %}</button> <button type="submit">{% if credential %}Save SSH key{% else %}Create SSH key{% endif %}</button>
<a class="button-link secondary" href="{% url 'ssh_credentials' %}">Cancel</a>
</div> </div>
</form> </form>
</section> </section>
@@ -64,7 +65,10 @@
<label for="confirm_name">Confirm key name</label> <label for="confirm_name">Confirm key name</label>
<input id="confirm_name" name="confirm_name" type="text" {% if credential.hosts.exists or credential.global_configs.exists %}disabled{% endif %}> <input id="confirm_name" name="confirm_name" type="text" {% if credential.hosts.exists or credential.global_configs.exists %}disabled{% endif %}>
</div> </div>
<button type="submit" class="danger" {% if credential.hosts.exists or credential.global_configs.exists %}disabled{% endif %}>Delete SSH key</button> <div class="form-actions">
<button type="submit" class="danger" {% if credential.hosts.exists or credential.global_configs.exists %}disabled{% endif %}>Delete SSH key</button>
<a class="button-link secondary" href="{% url 'ssh_credentials' %}">Cancel</a>
</div>
</form> </form>
</section> </section>
{% endif %} {% endif %}

View File

@@ -29,8 +29,9 @@
</div> </div>
{% endfor %} {% endfor %}
<div class="actions"> <div class="form-actions">
<button type="submit">Generate SSH key</button> <button type="submit">Generate SSH key</button>
<a class="button-link secondary" href="{% url 'ssh_credentials' %}">Cancel</a>
</div> </div>
</form> </form>
</section> </section>

View File

@@ -233,6 +233,9 @@ class ViewTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Runs") self.assertContains(response, "Runs")
self.assertContains(response, "Review queued, running, completed") self.assertContains(response, "Review queued, running, completed")
self.assertContains(response, "Apply filters")
self.assertContains(response, reverse("runs_list"))
self.assertContains(response, "Clear")
self.assertContains(response, f"Run {failed.id}") self.assertContains(response, f"Run {failed.id}")
self.assertContains(response, "web-01") self.assertContains(response, "web-01")
self.assertContains(response, "needed") self.assertContains(response, "needed")
@@ -275,6 +278,9 @@ class ViewTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Snapshots") self.assertContains(response, "Snapshots")
self.assertContains(response, "Browse discovered scheduled, manual, and incomplete snapshots") self.assertContains(response, "Browse discovered scheduled, manual, and incomplete snapshots")
self.assertContains(response, "Apply filters")
self.assertContains(response, reverse("snapshots_list"))
self.assertContains(response, "Clear")
self.assertContains(response, manual.dirname) self.assertContains(response, manual.dirname)
self.assertContains(response, "web-01") self.assertContains(response, "web-01")
self.assertNotContains(response, scheduled.dirname) self.assertNotContains(response, scheduled.dirname)
@@ -291,6 +297,9 @@ class ViewTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Schedules") self.assertContains(response, "Schedules")
self.assertContains(response, "Review configured backup schedules") self.assertContains(response, "Review configured backup schedules")
self.assertContains(response, "Apply filters")
self.assertContains(response, reverse("schedules_list"))
self.assertContains(response, "Clear")
self.assertContains(response, "web-01") self.assertContains(response, "web-01")
self.assertContains(response, "15 2 * * *") self.assertContains(response, "15 2 * * *")
self.assertContains(response, "success") self.assertContains(response, "success")
@@ -428,6 +437,9 @@ class ViewTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Logs") self.assertContains(response, "Logs")
self.assertContains(response, "Filter pobsync service logs") self.assertContains(response, "Filter pobsync service logs")
self.assertContains(response, "Filter logs")
self.assertContains(response, reverse("logs"))
self.assertContains(response, "Clear")
self.assertContains(response, "web-01 failed backup run 12") self.assertContains(response, "web-01 failed backup run 12")
self.assertNotContains(response, "web-02 failed backup run 12") self.assertNotContains(response, "web-02 failed backup run 12")
self.assertNotContains(response, "started") self.assertNotContains(response, "started")
@@ -458,6 +470,9 @@ class ViewTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Purged Snapshots") self.assertContains(response, "Purged Snapshots")
self.assertContains(response, "Audit trail for snapshots removed") self.assertContains(response, "Audit trail for snapshots removed")
self.assertContains(response, "Apply filters")
self.assertContains(response, reverse("purged_snapshots"))
self.assertContains(response, "Clear")
self.assertContains(response, "20260518-021500Z__OLDSNAP") self.assertContains(response, "20260518-021500Z__OLDSNAP")
self.assertContains(response, "outside retention policy") self.assertContains(response, "outside retention policy")
self.assertContains(response, "Scheduled") self.assertContains(response, "Scheduled")
@@ -542,6 +557,21 @@ class ViewTests(TestCase):
self.assertEqual(credential.private_key, "UPLOADED PRIVATE KEY\n") self.assertEqual(credential.private_key, "UPLOADED PRIVATE KEY\n")
self.assertEqual(credential.public_key, "DERIVED PUBLIC KEY") self.assertEqual(credential.public_key, "DERIVED PUBLIC KEY")
def test_ssh_credential_forms_render_cancel_actions(self) -> None:
self.client.force_login(self.staff_user)
credential = SshCredential.objects.create(name="backup-key")
create_response = self.client.get(reverse("create_ssh_credential"))
edit_response = self.client.get(reverse("edit_ssh_credential", args=[credential.id]))
generate_response = self.client.get(reverse("generate_ssh_credential"))
for response in (create_response, edit_response, generate_response):
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Cancel")
self.assertContains(response, reverse("ssh_credentials"))
self.assertContains(edit_response, "Delete SSH key")
self.assertContains(edit_response, 'class="danger"', html=False)
def test_ssh_credentials_view_generates_filesystem_key(self) -> None: def test_ssh_credentials_view_generates_filesystem_key(self) -> None:
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
with TemporaryDirectory() as tmp, override_settings(POBSYNC_HOME=str(Path(tmp) / "home")): with TemporaryDirectory() as tmp, override_settings(POBSYNC_HOME=str(Path(tmp) / "home")):
@@ -746,6 +776,8 @@ class ViewTests(TestCase):
self.assertContains(response, f'value="{credential.id}" selected') self.assertContains(response, f'value="{credential.id}" selected')
self.assertContains(response, "--archive") self.assertContains(response, "--archive")
self.assertContains(response, "/proc/***") self.assertContains(response, "/proc/***")
self.assertContains(response, "Cancel")
self.assertContains(response, reverse("dashboard"))
def test_global_config_form_renders_static_container_backup_root_on_edit(self) -> None: def test_global_config_form_renders_static_container_backup_root_on_edit(self) -> None:
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
@@ -1635,8 +1667,11 @@ class ViewTests(TestCase):
response = self.client.get(reverse("run_detail", args=[run.id])) response = self.client.get(reverse("run_detail", args=[run.id]))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Run Control")
self.assertContains(response, "Cancelling a queued run stops it immediately")
self.assertContains(response, "Cancel run") self.assertContains(response, "Cancel run")
self.assertContains(response, reverse("cancel_run", args=[run.id])) self.assertContains(response, reverse("cancel_run", args=[run.id]))
self.assertContains(response, 'class="danger"', html=False)
def test_run_detail_renders_worker_execution_metadata(self) -> None: def test_run_detail_renders_worker_execution_metadata(self) -> None:
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
@@ -1820,6 +1855,9 @@ class ViewTests(TestCase):
self.assertNotContains(response, "<div class=\"label\">Source</div>", html=True) self.assertNotContains(response, "<div class=\"label\">Source</div>", html=True)
self.assertContains(response, "Confirm delete count") self.assertContains(response, "Confirm delete count")
self.assertContains(response, "Type 1 to confirm the current number of planned deletions.") self.assertContains(response, "Type 1 to confirm the current number of planned deletions.")
self.assertContains(response, "This permanently deletes the snapshot directories listed in Would Delete.")
self.assertContains(response, 'class="danger"', html=False)
self.assertContains(response, "Cancel")
def test_retention_plan_warns_when_scheduled_prune_limit_is_exceeded(self) -> None: def test_retention_plan_warns_when_scheduled_prune_limit_is_exceeded(self) -> None:
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
@@ -1895,6 +1933,8 @@ class ViewTests(TestCase):
self.assertContains(response, "excluded from retention cleanup") self.assertContains(response, "excluded from retention cleanup")
self.assertContains(response, "Delete incomplete snapshots") self.assertContains(response, "Delete incomplete snapshots")
self.assertContains(response, "Type 1 to confirm the current number of incomplete snapshots.") self.assertContains(response, "Type 1 to confirm the current number of incomplete snapshots.")
self.assertContains(response, "This deletes only incomplete snapshot directories")
self.assertContains(response, 'class="danger"', html=False)
def test_incomplete_cleanup_deletes_incomplete_snapshot_after_confirmation(self) -> None: def test_incomplete_cleanup_deletes_incomplete_snapshot_after_confirmation(self) -> None:
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
@@ -2142,6 +2182,8 @@ class ViewTests(TestCase):
self.assertContains(response, "evaluated by the pobsync scheduler service") self.assertContains(response, "evaluated by the pobsync scheduler service")
self.assertContains(response, "15 2 * * *") self.assertContains(response, "15 2 * * *")
self.assertContains(response, "Save schedule") self.assertContains(response, "Save schedule")
self.assertContains(response, "Cancel")
self.assertContains(response, reverse("host_detail", args=[host.host]))
def test_schedule_form_creates_schedule(self) -> None: def test_schedule_form_creates_schedule(self) -> None:
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
@@ -2243,6 +2285,8 @@ class ViewTests(TestCase):
self.assertContains(response, "/srv") self.assertContains(response, "/srv")
self.assertContains(response, "*.tmp") self.assertContains(response, "*.tmp")
self.assertContains(response, "--numeric-ids") self.assertContains(response, "--numeric-ids")
self.assertContains(response, "Cancel")
self.assertContains(response, reverse("host_detail", args=[host.host]))
def test_host_config_form_renders_effective_config_check(self) -> None: def test_host_config_form_renders_effective_config_check(self) -> None:
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)