From 5b9008e321f16e99ce85815c1323e7bd3ac6aa1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C4=93teris=20Caune?=
Date: Fri, 27 Aug 2021 21:48:11 +0300
Subject: [PATCH] Implement alternative ping URLs, WIP
---
.../migrations/0045_auto_20210908_1257.py | 23 ++++++++++++++++
hc/accounts/models.py | 13 +++++++--
hc/accounts/tests/test_signup.py | 1 +
hc/accounts/views.py | 3 ++-
hc/api/migrations/0079_auto_20210907_0918.py | 22 +++++++++++++++
hc/api/migrations/0080_fill_slug.py | 19 +++++++++++++
hc/api/models.py | 27 +++++++++++++++++--
hc/api/tests/test_create_check.py | 2 ++
hc/api/tests/test_get_check.py | 8 +++---
hc/api/tests/test_update_check.py | 1 +
hc/api/urls.py | 1 +
hc/api/views.py | 7 ++++-
hc/front/tests/test_copy.py | 2 ++
hc/front/tests/test_update_name.py | 1 +
hc/front/views.py | 8 ++++--
static/css/my_checks_desktop.css | 12 +++++++--
templates/accounts/project.html | 12 +++++++--
templates/docs/api.html | 10 +++++++
templates/docs/api.md | 10 +++++++
templates/front/my_checks_desktop.html | 18 ++++++++++---
20 files changed, 182 insertions(+), 18 deletions(-)
create mode 100644 hc/accounts/migrations/0045_auto_20210908_1257.py
create mode 100644 hc/api/migrations/0079_auto_20210907_0918.py
create mode 100644 hc/api/migrations/0080_fill_slug.py
diff --git a/hc/accounts/migrations/0045_auto_20210908_1257.py b/hc/accounts/migrations/0045_auto_20210908_1257.py
new file mode 100644
index 00000000..b2925640
--- /dev/null
+++ b/hc/accounts/migrations/0045_auto_20210908_1257.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.6 on 2021-09-08 12:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0044_auto_20210730_0942'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='project',
+ name='ping_key',
+ field=models.CharField(blank=True, max_length=128, null=True, unique=True),
+ ),
+ migrations.AddField(
+ model_name='project',
+ name='show_slugs',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/hc/accounts/models.py b/hc/accounts/models.py
index 88268aed..a615db8e 100644
--- a/hc/accounts/models.py
+++ b/hc/accounts/models.py
@@ -322,6 +322,8 @@ class Project(models.Model):
api_key = models.CharField(max_length=128, blank=True, db_index=True)
api_key_readonly = models.CharField(max_length=128, blank=True, db_index=True)
badge_key = models.CharField(max_length=150, unique=True)
+ ping_key = models.CharField(max_length=128, blank=True, null=True, unique=True)
+ show_slugs = models.BooleanField(default=False)
def __str__(self):
return self.name or self.owner.email
@@ -337,8 +339,15 @@ class Project(models.Model):
return self.owner_profile.num_checks_available()
def set_api_keys(self):
- self.api_key = token_urlsafe(nbytes=24)
- self.api_key_readonly = token_urlsafe(nbytes=24)
+ def pick(nbytes=24):
+ while True:
+ candidate = token_urlsafe(nbytes)
+ if candidate[0] not in "-_" and candidate[-1] not in "-_":
+ return candidate
+
+ self.api_key = pick()
+ self.api_key_readonly = pick()
+ self.ping_key = pick(16)
self.save()
def invite_suggestions(self):
diff --git a/hc/accounts/tests/test_signup.py b/hc/accounts/tests/test_signup.py
index 89e97459..d55ca632 100644
--- a/hc/accounts/tests/test_signup.py
+++ b/hc/accounts/tests/test_signup.py
@@ -39,6 +39,7 @@ class SignupTestCase(TestCase):
# And check should be associated with the new user
check = Check.objects.get()
self.assertEqual(check.name, "My First Check")
+ self.assertEqual(check.slug, "my-first-check")
self.assertEqual(check.project, project)
# A channel should have been created
diff --git a/hc/accounts/views.py b/hc/accounts/views.py
index f5fab7cd..c094f7b9 100644
--- a/hc/accounts/views.py
+++ b/hc/accounts/views.py
@@ -79,7 +79,7 @@ def _make_user(email, tz=None, with_project=True):
project.save()
check = Check(project=project)
- check.name = "My First Check"
+ check.set_name_slug("My First Check")
check.save()
channel = Channel(project=project)
@@ -336,6 +336,7 @@ def project(request, code):
project.api_key = ""
project.api_key_readonly = ""
+ project.ping_key = None
project.save()
ctx["api_keys_revoked"] = True
diff --git a/hc/api/migrations/0079_auto_20210907_0918.py b/hc/api/migrations/0079_auto_20210907_0918.py
new file mode 100644
index 00000000..3a7cf93a
--- /dev/null
+++ b/hc/api/migrations/0079_auto_20210907_0918.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.2.6 on 2021-09-07 09:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0078_sms_values'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='check',
+ name='slug',
+ field=models.CharField(blank=True, max_length=100),
+ ),
+ migrations.AddIndex(
+ model_name='check',
+ index=models.Index(fields=['project_id', 'slug'], name='api_check_project_slug'),
+ ),
+ ]
diff --git a/hc/api/migrations/0080_fill_slug.py b/hc/api/migrations/0080_fill_slug.py
new file mode 100644
index 00000000..1ce0e0ee
--- /dev/null
+++ b/hc/api/migrations/0080_fill_slug.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.6 on 2021-09-07 09:19
+
+from django.db import migrations
+from django.utils.text import slugify
+
+
+def fill_slug(apps, schema_editor):
+ Check = apps.get_model("api", "CHeck")
+ for c in Check.objects.exclude(name="").only("name"):
+ Check.objects.filter(id=c.id).update(slug=slugify(c.name))
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("api", "0079_auto_20210907_0918"),
+ ]
+
+ operations = [migrations.RunPython(fill_slug, migrations.RunPython.noop)]
diff --git a/hc/api/models.py b/hc/api/models.py
index 243264a2..b27f352d 100644
--- a/hc/api/models.py
+++ b/hc/api/models.py
@@ -12,6 +12,7 @@ from django.core.signing import TimestampSigner
from django.db import models
from django.urls import reverse
from django.utils import timezone
+from django.utils.text import slugify
from hc.accounts.models import Project
from hc.api import transports
from hc.lib import emails
@@ -68,6 +69,7 @@ def isostring(dt):
class Check(models.Model):
name = models.CharField(max_length=100, blank=True)
+ slug = models.CharField(max_length=100, blank=True)
tags = models.CharField(max_length=500, blank=True)
code = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
desc = models.TextField(blank=True)
@@ -100,7 +102,8 @@ class Check(models.Model):
fields=["alert_after"],
name="api_check_aa_not_down",
condition=~models.Q(status="down"),
- )
+ ),
+ models.Index(fields=["project_id", "slug"], name="api_check_project_slug"),
]
def __str__(self):
@@ -112,8 +115,23 @@ class Check(models.Model):
return str(self.code)
+ def relative_url(self):
+ if self.project.show_slugs:
+ if not self.slug:
+ return None
+
+ key = self.project.ping_key
+ # If ping_key is not set, use dummy placeholder
+ if key is None:
+ key = "{ping_key}"
+ return key + "/" + self.slug
+
+ return str(self.code)
+
def url(self):
- return settings.PING_ENDPOINT + str(self.code)
+ s = self.relative_url()
+ if s:
+ return settings.PING_ENDPOINT + s
def details_url(self):
return settings.SITE_ROOT + reverse("hc-details", args=[self.code])
@@ -128,6 +146,10 @@ class Check(models.Model):
if self.last_duration and self.last_duration < MAX_DELTA:
return self.last_duration
+ def set_name_slug(self, name):
+ self.name = name
+ self.slug = slugify(name)
+
def get_grace_start(self, with_started=True):
""" Return the datetime when the grace period starts.
@@ -224,6 +246,7 @@ class Check(models.Model):
result = {
"name": self.name,
+ "slug": self.slug,
"tags": self.tags,
"desc": self.desc,
"grace": int(self.grace.total_seconds()),
diff --git a/hc/api/tests/test_create_check.py b/hc/api/tests/test_create_check.py
index d1375f6d..2326c51a 100644
--- a/hc/api/tests/test_create_check.py
+++ b/hc/api/tests/test_create_check.py
@@ -28,6 +28,7 @@ class CreateCheckTestCase(BaseTestCase):
doc = r.json()
assert "ping_url" in doc
self.assertEqual(doc["name"], "Foo")
+ self.assertEqual(doc["slug"], "foo")
self.assertEqual(doc["tags"], "bar,baz")
self.assertEqual(doc["last_ping"], None)
self.assertEqual(doc["n_pings"], 0)
@@ -38,6 +39,7 @@ class CreateCheckTestCase(BaseTestCase):
check = Check.objects.get()
self.assertEqual(check.name, "Foo")
+ self.assertEqual(check.slug, "foo")
self.assertEqual(check.tags, "bar,baz")
self.assertEqual(check.methods, "")
self.assertEqual(check.timeout.total_seconds(), 3600)
diff --git a/hc/api/tests/test_get_check.py b/hc/api/tests/test_get_check.py
index facd1792..dd2b78b5 100644
--- a/hc/api/tests/test_get_check.py
+++ b/hc/api/tests/test_get_check.py
@@ -11,7 +11,8 @@ class GetCheckTestCase(BaseTestCase):
self.now = now().replace(microsecond=0)
- self.a1 = Check(project=self.project, name="Alice 1")
+ self.a1 = Check(project=self.project)
+ self.a1.set_name_slug("Alice 1")
self.a1.timeout = td(seconds=3600)
self.a1.grace = td(seconds=900)
self.a1.n_pings = 0
@@ -33,8 +34,9 @@ class GetCheckTestCase(BaseTestCase):
self.assertEqual(r["Access-Control-Allow-Origin"], "*")
doc = r.json()
- self.assertEqual(len(doc), 15)
+ self.assertEqual(len(doc), 16)
+ self.assertEqual(doc["slug"], "alice-1")
self.assertEqual(doc["timeout"], 3600)
self.assertEqual(doc["grace"], 900)
self.assertEqual(doc["ping_url"], self.a1.url())
@@ -61,7 +63,7 @@ class GetCheckTestCase(BaseTestCase):
self.assertEqual(r["Access-Control-Allow-Origin"], "*")
doc = r.json()
- self.assertEqual(len(doc), 15)
+ self.assertEqual(len(doc), 16)
self.assertEqual(doc["timeout"], 3600)
self.assertEqual(doc["grace"], 900)
diff --git a/hc/api/tests/test_update_check.py b/hc/api/tests/test_update_check.py
index 8c7f324a..a42f4c5d 100644
--- a/hc/api/tests/test_update_check.py
+++ b/hc/api/tests/test_update_check.py
@@ -38,6 +38,7 @@ class UpdateCheckTestCase(BaseTestCase):
doc = r.json()
assert "ping_url" in doc
self.assertEqual(doc["name"], "Foo")
+ self.assertEqual(doc["slug"], "foo")
self.assertEqual(doc["tags"], "bar,baz")
self.assertEqual(doc["desc"], "My description")
self.assertEqual(doc["n_pings"], 0)
diff --git a/hc/api/urls.py b/hc/api/urls.py
index 8aeeb222..833fa2c4 100644
--- a/hc/api/urls.py
+++ b/hc/api/urls.py
@@ -33,6 +33,7 @@ urlpatterns = [
path("ping//fail", views.ping, {"action": "fail"}, name="hc-fail"),
path("ping//start", views.ping, {"action": "start"}, name="hc-start"),
path("ping//", views.ping),
+ path("ping//", views.ping_by_slug),
path("api/v1/checks/", views.checks),
path("api/v1/checks/", views.single, name="hc-api-single"),
path("api/v1/checks/", views.get_check_by_unique_key),
diff --git a/hc/api/views.py b/hc/api/views.py
index 9f40779a..97aee228 100644
--- a/hc/api/views.py
+++ b/hc/api/views.py
@@ -55,6 +55,11 @@ def ping(request, code, action="success", exitstatus=None):
return response
+def ping_by_slug(request, ping_key, slug, action="success", exitstatus=None):
+ check = get_object_or_404(Check, slug=slug, project__ping_key=ping_key)
+ return ping(request, check.code, action, exitstatus)
+
+
def _lookup(project, spec):
unique_fields = spec.get("unique", [])
if unique_fields:
@@ -108,7 +113,7 @@ def _update(check, spec):
need_save = True
if "name" in spec and check.name != spec["name"]:
- check.name = spec["name"]
+ check.set_name_slug(spec["name"])
need_save = True
if "tags" in spec and check.tags != spec["tags"]:
diff --git a/hc/front/tests/test_copy.py b/hc/front/tests/test_copy.py
index ff98b7b7..75f84eff 100644
--- a/hc/front/tests/test_copy.py
+++ b/hc/front/tests/test_copy.py
@@ -7,6 +7,7 @@ class CopyCheckTestCase(BaseTestCase):
super().setUp()
self.check = Check(project=self.project)
self.check.name = "Foo"
+ self.check.slug = "foo"
self.check.subject = "success-keyword"
self.check.subject_fail = "failure-keyword"
self.check.methods = "POST"
@@ -21,6 +22,7 @@ class CopyCheckTestCase(BaseTestCase):
self.assertContains(r, "This is a brand new check")
copy = Check.objects.get(name="Foo (copy)")
+ self.assertEqual(copy.slug, "foo-copy")
self.assertEqual(copy.subject, "success-keyword")
self.assertEqual(copy.subject_fail, "failure-keyword")
self.assertEqual(copy.methods, "POST")
diff --git a/hc/front/tests/test_update_name.py b/hc/front/tests/test_update_name.py
index a27283ca..c9028eca 100644
--- a/hc/front/tests/test_update_name.py
+++ b/hc/front/tests/test_update_name.py
@@ -17,6 +17,7 @@ class UpdateNameTestCase(BaseTestCase):
self.check.refresh_from_db()
self.assertEqual(self.check.name, "Alice Was Here")
+ self.assertEqual(self.check.slug, "alice-was-here")
def test_team_access_works(self):
payload = {"name": "Bob Was Here"}
diff --git a/hc/front/views.py b/hc/front/views.py
index e2ad126b..6314b145 100644
--- a/hc/front/views.py
+++ b/hc/front/views.py
@@ -174,6 +174,10 @@ def my_checks(request, code):
request.profile.sort = request.GET["sort"]
request.profile.save()
+ if request.GET.get("urls") in ("uuid", "slug") and rw:
+ project.show_slugs = request.GET["urls"] == "slug"
+ project.save()
+
if request.session.get("last_project_id") != project.id:
request.session["last_project_id"] = project.id
@@ -402,7 +406,7 @@ def update_name(request, code):
form = forms.NameTagsForm(request.POST)
if form.is_valid():
- check.name = form.cleaned_data["name"]
+ check.set_name_slug(form.cleaned_data["name"])
check.tags = form.cleaned_data["tags"]
check.desc = form.cleaned_data["desc"]
check.save()
@@ -694,7 +698,7 @@ def copy(request, code):
new_name = check.name[:90] + "... (copy)"
copied = Check(project=check.project)
- copied.name = new_name
+ copied.set_name_slug(new_name)
copied.desc, copied.tags = check.desc, check.tags
copied.subject, copied.subject_fail = check.subject, check.subject_fail
copied.methods = check.methods
diff --git a/static/css/my_checks_desktop.css b/static/css/my_checks_desktop.css
index e572fbe6..6dcd9643 100644
--- a/static/css/my_checks_desktop.css
+++ b/static/css/my_checks_desktop.css
@@ -6,7 +6,7 @@
background-color: var(--table-bg-hover);
}
-.my-checks-name.unnamed {
+.my-checks-name.unnamed, .url span.unavailable {
color: var(--text-muted);
font-style: italic;
}
@@ -132,7 +132,7 @@ tr:hover .copy-link {
white-space: nowrap;
}
-#checks-table .btn {
+#checks-table td .btn {
border-color: transparent;
background-color: transparent;
font-size: 20px;
@@ -159,3 +159,11 @@ tr:hover .copy-link {
color: #FFF;
outline: none;
}
+
+#url-style-switcher a {
+ color: var(--text-muted);
+}
+
+#url-style-switcher a.active {
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/templates/accounts/project.html b/templates/accounts/project.html
index 324a253f..7ca8f6e7 100644
--- a/templates/accounts/project.html
+++ b/templates/accounts/project.html
@@ -89,9 +89,17 @@
API key (read-only):
{{ project.api_key_readonly }}
- Related links:
+ {% endif %}
+ {% if project.ping_key %}
+
+ Ping key:
+
{{ project.ping_key }}
+
+ {% endif %}
+ See also:
- {% endif %}