From 002bc9b0837c9828058ff09966fde2a74e3b2349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= <cuu508@gmail.com> Date: Wed, 14 Jun 2023 15:06:37 +0300 Subject: [PATCH] Decouple check's name from slug, allow users to set hand-picked slugs --- CHANGELOG.md | 1 + hc/front/forms.py | 1 + hc/front/tests/test_add_check.py | 3 +- hc/front/tests/test_update_name.py | 5 ++-- hc/front/views.py | 6 ++-- static/css/slug-suggestions.css | 4 +++ static/js/checks.js | 1 + static/js/slug-suggestions.js | 40 ++++++++++++++++++++++++++ templates/base.html | 1 + templates/front/add_check_modal.html | 26 ++++++++++++++++- templates/front/details.html | 1 + templates/front/my_checks.html | 1 + templates/front/my_checks_desktop.html | 3 +- templates/front/update_name_modal.html | 29 ++++++++++++++++++- 14 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 static/css/slug-suggestions.css create mode 100644 static/js/slug-suggestions.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ece64bc8..ebe4378e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Improvements - Configure logging to log unhandled exceptions to console even when DEBUG=False (#835) - Make hc.lib.emails raise exceptions when EMAIL_ settings are not set +- Decouple check's name from slug, allow users to set hand-picked slugs ## v2.9.2 - 2023-06-05 diff --git a/hc/front/forms.py b/hc/front/forms.py index 621c18d2..3cfc65e4 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -69,6 +69,7 @@ class HeadersField(forms.Field): class NameTagsForm(forms.Form): name = forms.CharField(max_length=100, required=False) + slug = forms.SlugField(max_length=100, required=False) tags = forms.CharField(max_length=500, required=False) desc = forms.CharField(required=False) diff --git a/hc/front/tests/test_add_check.py b/hc/front/tests/test_add_check.py index 6f77e46c..d5a6dca3 100644 --- a/hc/front/tests/test_add_check.py +++ b/hc/front/tests/test_add_check.py @@ -14,6 +14,7 @@ class AddCheckTestCase(BaseTestCase): def _payload(self, **kwargs): payload = { "name": "Test", + "slug": "custom-slug", "tags": "foo bar", "kind": "simple", "timeout": "120", @@ -31,7 +32,7 @@ class AddCheckTestCase(BaseTestCase): check = Check.objects.get() self.assertEqual(check.project, self.project) self.assertEqual(check.name, "Test") - self.assertEqual(check.slug, "test") + self.assertEqual(check.slug, "custom-slug") self.assertEqual(check.tags, "foo bar") self.assertEqual(check.kind, "simple") self.assertEqual(check.timeout.total_seconds(), 120) diff --git a/hc/front/tests/test_update_name.py b/hc/front/tests/test_update_name.py index 25640b2a..185784fb 100644 --- a/hc/front/tests/test_update_name.py +++ b/hc/front/tests/test_update_name.py @@ -14,12 +14,13 @@ class UpdateNameTestCase(BaseTestCase): def test_it_works(self): self.client.login(username="alice@example.org", password="password") - r = self.client.post(self.url, data={"name": "Alice Was Here"}) + form = {"name": "Alice Was Here", "slug": "custom-slug"} + r = self.client.post(self.url, data=form) self.assertRedirects(r, self.redirect_url) self.check.refresh_from_db() self.assertEqual(self.check.name, "Alice Was Here") - self.assertEqual(self.check.slug, "alice-was-here") + self.assertEqual(self.check.slug, "custom-slug") def test_redirect_preserves_querystring(self): referer = self.redirect_url + "?tag=foo" diff --git a/hc/front/views.py b/hc/front/views.py index 98794b26..d678b525 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -481,7 +481,8 @@ def add_check(request, code): return HttpResponseBadRequest() check = Check(project=project) - check.set_name_slug(form.cleaned_data["name"]) + check.name = form.cleaned_data["name"] + check.slug = form.cleaned_data["slug"] check.tags = form.cleaned_data["tags"] check.kind = form.cleaned_data["kind"] check.timeout = form.cleaned_data["timeout"] @@ -504,7 +505,8 @@ def update_name(request, code): form = forms.NameTagsForm(request.POST) if form.is_valid(): - check.set_name_slug(form.cleaned_data["name"]) + check.name = form.cleaned_data["name"] + check.slug = form.cleaned_data["slug"] check.tags = form.cleaned_data["tags"] check.desc = form.cleaned_data["desc"] check.save() diff --git a/static/css/slug-suggestions.css b/static/css/slug-suggestions.css new file mode 100644 index 00000000..b157be50 --- /dev/null +++ b/static/css/slug-suggestions.css @@ -0,0 +1,4 @@ +.slug-help-block code { + color: var(--text-muted); + background: var(--pre-bg); +} \ No newline at end of file diff --git a/static/js/checks.js b/static/js/checks.js index 00fd323a..363f80ca 100644 --- a/static/js/checks.js +++ b/static/js/checks.js @@ -7,6 +7,7 @@ $(function () { $("#update-name-form").attr("action", url); $("#update-name-input").val(this.dataset.name); + $("#update-slug-input").val(this.dataset.slug); var tagsSelectize = document.getElementById("update-tags-input").selectize; tagsSelectize.setValue(this.dataset.tags.split(" ")); diff --git a/static/js/slug-suggestions.js b/static/js/slug-suggestions.js new file mode 100644 index 00000000..d874dd7f --- /dev/null +++ b/static/js/slug-suggestions.js @@ -0,0 +1,40 @@ +$(function () { + function slugify(text) { + return text + .normalize("NFKD") + .split("") + .map(ch => ch.charCodeAt(0) < 256 ? ch : "") + .join("") + .toLowerCase() + .replace(/[^\w\s-]/g, "") + .replace(/[-\s]+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, ""); + } + + $(".with-slug-suggestions").each(function() { + var nameInput = $("input[name='name']", this); + var slugInput = $("input[name='slug']", this); + var btn = $(".use-suggested-slug", this); + var help = $(".slug-help-block", this); + + function update() { + var suggested = slugify(nameInput.val()); + if (suggested) { + help.html(`Suggested value: <code>${suggested}</code>`); + } else { + help.text("Allowed characters: a-z, A-Z, 0-9, hyphens, underscores."); + } + + btn.attr("disabled", !suggested); + } + + $(nameInput).on("keyup change", update); + $(this).on("shown.bs.modal", update); + + btn.click(function() { + slugInput.val(slugify(nameInput.val())); + }); + }); + +}); diff --git a/templates/base.html b/templates/base.html index 9619b15c..7833a028 100644 --- a/templates/base.html +++ b/templates/base.html @@ -59,6 +59,7 @@ <link rel="stylesheet" href="{% static 'css/signal_form.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/project.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/signup.css' %}" type="text/css"> + <link rel="stylesheet" href="{% static 'css/slug-suggestions.css' %}" type="text/css"> {% endcompress %} </head> <body class="page-{{ page }}{% if request.user.is_authenticated and request.profile.theme == 'dark' %} dark{% endif%}"> diff --git a/templates/front/add_check_modal.html b/templates/front/add_check_modal.html index f79eb84c..56e03238 100644 --- a/templates/front/add_check_modal.html +++ b/templates/front/add_check_modal.html @@ -1,4 +1,4 @@ -<div id="add-check-modal" class="modal simple"> +<div id="add-check-modal" class="modal simple with-slug-suggestions"> <div class="modal-dialog"> <form class="form-horizontal" method="post" action="{% url 'hc-add-check' project.code %}"> {% csrf_token %} @@ -28,6 +28,30 @@ </div> </div> + <div class="form-group"> + <label for="add-check-slug" class="col-sm-3 control-label"> + Slug + </label> + <div class="col-sm-9"> + <div class="input-group"> + <input + id="add-check-slug" + name="slug" + type="text" + maxlength="100" + value="{{ check.slug }}" + pattern="[a-zA-Z0-9_-]+" + class="form-control" /> + <span class="input-group-btn"> + <button + class="btn btn-default use-suggested-slug" + type="button">Use Suggested</button> + </span> + </div> + <span class="help-block slug-help-block"></span> + </div> + </div> + <div class="form-group"> <label for="add-check-tags" class="col-sm-3 control-label"> Tags diff --git a/templates/front/details.html b/templates/front/details.html index bc7c5fa0..a8a662ae 100644 --- a/templates/front/details.html +++ b/templates/front/details.html @@ -363,5 +363,6 @@ <script src="{% static 'js/ping_details.js' %}"></script> <script src="{% static 'js/details.js' %}"></script> <script src="{% static 'js/initialize-timezone-selects.js' %}"></script> +<script src="{% static 'js/slug-suggestions.js' %}"></script> {% endcompress %} {% endblock %} diff --git a/templates/front/my_checks.html b/templates/front/my_checks.html index c44bd3e1..a108ec2b 100644 --- a/templates/front/my_checks.html +++ b/templates/front/my_checks.html @@ -102,5 +102,6 @@ <script src="{% static 'js/checks.js' %}"></script> <script src="{% static 'js/add-check-modal.js' %}"></script> <script src="{% static 'js/initialize-timezone-selects.js' %}"></script> +<script src="{% static 'js/slug-suggestions.js' %}"></script> {% endcompress %} {% endblock %} diff --git a/templates/front/my_checks_desktop.html b/templates/front/my_checks_desktop.html index 9f36abb7..ac0df4b7 100644 --- a/templates/front/my_checks_desktop.html +++ b/templates/front/my_checks_desktop.html @@ -66,6 +66,7 @@ </td> <td> <div data-name="{{ check.name }}" + data-slug="{{ check.slug }}" data-tags="{{ check.tags }}" data-desc="{{ check.desc }}" class="my-checks-name {% if not check.name %}unnamed{% endif %}"> @@ -77,7 +78,7 @@ </td> <td class="hidden-xs hidden-sm hidden-md"> {% if project.show_slugs and not check.slug %} - <span class="unavailable">unavailable, set name first</span> + <span class="unavailable">unavailable, slug not set</span> {% else %} <span class="my-checks-url">{{ check.url|format_ping_endpoint }}</span> {% if project.show_slugs and check.slug in ambiguous %} diff --git a/templates/front/update_name_modal.html b/templates/front/update_name_modal.html index 434332e3..dc4ebe37 100644 --- a/templates/front/update_name_modal.html +++ b/templates/front/update_name_modal.html @@ -1,4 +1,4 @@ -<div id="update-name-modal" class="modal"> +<div id="update-name-modal" class="modal with-slug-suggestions"> <div class="modal-dialog"> <form id="update-name-form" @@ -33,6 +33,33 @@ </div> </div> + <div class="form-group"> + <label for="update-slug-input" class="col-sm-2 control-label"> + Slug + </label> + <div class="col-sm-10"> + <div class="input-group"> + <input + id="update-slug-input" + name="slug" + type="text" + maxlength="100" + value="{{ check.slug }}" + pattern="[a-zA-Z0-9_-]+" + data-src-id="update-name-input" + data-btn-id="use-suggested" + data-preview-id="slug-help" + class="form-control" /> + <span class="input-group-btn"> + <button + class="btn btn-default use-suggested-slug" + type="button">Use Suggested</button> + </span> + </div> + <span class="help-block slug-help-block"></span> + </div> + </div> + <div class="form-group"> <label for="update-tags-input" class="col-sm-2 control-label"> Tags