diff --git a/src/core/jinja2/core/artwork_detail.jinja2 b/src/core/jinja2/core/artwork_detail.jinja2
new file mode 100644
index 00000000..d4b27ebc
--- /dev/null
+++ b/src/core/jinja2/core/artwork_detail.jinja2
@@ -0,0 +1,14 @@
+{% extends '/core/base.jinja2' %}
+{% block content %}
+
+
{{ artwork.title }}
+
{{ _("Author") }}: {{ artwork.author }}
+ {% if artwork.marker %}
+
{{ _("Marker") }}: {{ artwork.marker.title }}
+ {% endif %}
+ {% if artwork.augmented %}
+
{{ _("Object") }}: {{ artwork.augmented.title }}
+ {% endif %}
+
{{ _("Uploaded") }}: {{ artwork.uploaded_at }}
+
+{% endblock %}
diff --git a/src/core/jinja2/core/marker_detail.jinja2 b/src/core/jinja2/core/marker_detail.jinja2
new file mode 100644
index 00000000..bc37cf51
--- /dev/null
+++ b/src/core/jinja2/core/marker_detail.jinja2
@@ -0,0 +1,11 @@
+{% extends '/core/base.jinja2' %}
+{% block content %}
+
+
{{ marker.title }}
+
{{ _("Author") }}: {{ marker.author }}
+ {% if marker.source %}
+

+ {% endif %}
+
{{ _("Uploaded") }}: {{ marker.uploaded_at }}
+
+{% endblock %}
diff --git a/src/core/jinja2/core/object_detail.jinja2 b/src/core/jinja2/core/object_detail.jinja2
new file mode 100644
index 00000000..0abc83da
--- /dev/null
+++ b/src/core/jinja2/core/object_detail.jinja2
@@ -0,0 +1,14 @@
+{% extends '/core/base.jinja2' %}
+{% block content %}
+
+
{{ object.title }}
+
{{ _("Author") }}: {{ object.author }}
+ {% if object.source %}
+
+ {% endif %}
+ {% if object.thumbnail %}
+

+ {% endif %}
+
{{ _("Uploaded") }}: {{ object.uploaded_at }}
+
+{% endblock %}
diff --git a/src/core/migrations/0028_add_slug_to_marker_object_artwork.py b/src/core/migrations/0028_add_slug_to_marker_object_artwork.py
new file mode 100644
index 00000000..e4218e4a
--- /dev/null
+++ b/src/core/migrations/0028_add_slug_to_marker_object_artwork.py
@@ -0,0 +1,28 @@
+# Generated manually to add slug fields to Marker, Object, and Artwork models
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0027_sound_soundevent_remove_artwork_insert_insert_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='marker',
+ name='slug',
+ field=models.SlugField(blank=True, max_length=80, unique=True),
+ ),
+ migrations.AddField(
+ model_name='object',
+ name='slug',
+ field=models.SlugField(blank=True, max_length=80, unique=True),
+ ),
+ migrations.AddField(
+ model_name='artwork',
+ name='slug',
+ field=models.SlugField(blank=True, max_length=80, unique=True),
+ ),
+ ]
diff --git a/src/core/models.py b/src/core/models.py
index 702dcede..87ec90ac 100644
--- a/src/core/models.py
+++ b/src/core/models.py
@@ -6,6 +6,7 @@
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.urls import reverse
+from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django_extensions.db.models import TimeStampedModel
from fast_html import a, audio, b, div, h1, img, p, render, span, video
@@ -219,10 +220,22 @@ class Marker(TimeStampedModel, ContentMixin):
# Save the file size of the Marker, so we avoid making requests to S3 / MinIO to check for it.
file_size = models.IntegerField(default=0, blank=True, null=True)
+ slug = models.SlugField(unique=True, blank=True, max_length=80)
def save(self, *args, **kwargs):
+ if not self.slug:
+ base_slug = slugify(self.title) or "marker"
+ slug = base_slug
+ counter = 1
+ while Marker.objects.filter(slug=slug).exclude(pk=self.pk).exists():
+ slug = f"{base_slug}-{counter}"
+ counter += 1
+ self.slug = slug
super().save(*args, **kwargs)
+ def get_absolute_url(self):
+ return reverse("marker-detail", kwargs={"slug": self.slug})
+
def __str__(self):
return self.source.name
@@ -321,6 +334,21 @@ class Object(TimeStampedModel, ContentMixin):
blank=True,
null=True,
)
+ slug = models.SlugField(unique=True, blank=True, max_length=80)
+
+ def save(self, *args, **kwargs):
+ if not self.slug:
+ base_slug = slugify(self.title) or "object"
+ slug = base_slug
+ counter = 1
+ while Object.objects.filter(slug=slug).exclude(pk=self.pk).exists():
+ slug = f"{base_slug}-{counter}"
+ counter += 1
+ self.slug = slug
+ super().save(*args, **kwargs)
+
+ def get_absolute_url(self):
+ return reverse("object-detail", kwargs={"slug": self.slug})
def __str__(self):
return self.source.name
@@ -433,6 +461,21 @@ class Artwork(TimeStampedModel, ContentMixin):
scale_y = models.FloatField(default=1.0)
position_x = models.FloatField(default=0.0)
position_y = models.FloatField(default=0.0)
+ slug = models.SlugField(unique=True, blank=True, max_length=80)
+
+ def save(self, *args, **kwargs):
+ if not self.slug:
+ base_slug = slugify(self.title) or "artwork"
+ slug = base_slug
+ counter = 1
+ while Artwork.objects.filter(slug=slug).exclude(pk=self.pk).exists():
+ slug = f"{base_slug}-{counter}"
+ counter += 1
+ self.slug = slug
+ super().save(*args, **kwargs)
+
+ def get_absolute_url(self):
+ return reverse("artwork-detail", kwargs={"slug": self.slug})
@property
def exhibits_count(self):
@@ -515,6 +558,9 @@ class Exhibit(TimeStampedModel, ContentMixin, models.Model):
def __str__(self):
return self.name
+ def get_absolute_url(self):
+ return reverse("exhibit", kwargs={"slug": self.slug})
+
@property
def artworks_count(self):
return self.artworks.count()
diff --git a/src/core/urls.py b/src/core/urls.py
index abc40c6d..112b98da 100644
--- a/src/core/urls.py
+++ b/src/core/urls.py
@@ -15,6 +15,7 @@
service_worker,
)
from core.views.views import (
+ artwork_detail,
artwork_preview,
collection,
create_artwork,
@@ -29,8 +30,10 @@
exhibit_detail,
exhibit_select,
get_element,
+ marker_detail,
marker_preview,
marker_upload,
+ object_detail,
object_upload,
related_content,
see_all,
@@ -41,6 +44,7 @@
path("", home_new, name="home"),
path("arviewer/", home_old, name="home-old"),
path("artwork/", artwork_preview, name="artwork-preview"),
+ path("artworks//", artwork_detail, name="artwork-detail"),
path("artworks/create/", create_artwork, name="create-artwork"),
path("artworks/edit/", edit_artwork, name="edit-artwork"),
path("collection/", collection, name="collection"),
@@ -55,8 +59,10 @@
path("exhibits/edit-mr/", create_or_edit_mr_exhibit, name="edit-mr-exhibit"),
path("generator/", marker_generator, name="marker-generator"),
path("marker/", marker_preview, name="marker-preview"),
+ path("markers//", marker_detail, name="marker-detail"),
path("markers/edit/", edit_marker, name="edit-marker"),
path("markers/upload/", marker_upload, name="marker-upload"),
+ path("objects//", object_detail, name="object-detail"),
path("objects/edit/", edit_object, name="edit-object"),
path("objects/upload/", object_upload, name="object-upload"),
path("sounds/edit/", edit_sound, name="edit-sound"),
diff --git a/src/core/views/views.py b/src/core/views/views.py
index 208c552a..3079c172 100644
--- a/src/core/views/views.py
+++ b/src/core/views/views.py
@@ -404,6 +404,27 @@ def artwork_preview(request):
return render(request, "core/exhibit.jinja2", ctx)
+@require_http_methods(["GET"])
+def marker_detail(request, slug):
+ marker = get_object_or_404(Marker, slug=slug)
+ ctx = {"marker": marker}
+ return render(request, "core/marker_detail.jinja2", ctx)
+
+
+@require_http_methods(["GET"])
+def object_detail(request, slug):
+ obj = get_object_or_404(Object, slug=slug)
+ ctx = {"object": obj}
+ return render(request, "core/object_detail.jinja2", ctx)
+
+
+@require_http_methods(["GET"])
+def artwork_detail(request, slug):
+ artwork = get_object_or_404(Artwork, slug=slug)
+ ctx = {"artwork": artwork}
+ return render(request, "core/artwork_detail.jinja2", ctx)
+
+
@login_required
def get_element(request):
if request.htmx: