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/
{{ project.api_key_readonly }}
Related links:
+ {% endif %} + {% if project.ping_key %} +
+ Ping key:
+
{{ project.ping_key }}+ + {% endif %} +
See also: