From a167bd7a023fd17fdb86d84bcb6ad65496964533 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 04:34:32 +0000 Subject: [PATCH 01/14] Add .PHONY declarations to Makefile targets All Makefile targets are aliases to other commands, not file-producing rules, so they should be declared as .PHONY to prevent conflicts with files of the same name and to avoid unnecessary stat checks. Closes #883 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 23d2d2e8..34cc79f6 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ RUNNING_CONTAINER := $(shell docker compose ps --services --filter "status=running" | grep django ) +.PHONY: test test-ui test-all lint check migrations migrate collectstatic gen translate_es translate_pt + test: @if [ -n "${RUNNING_CONTAINER}" ]; then \ docker compose exec django pytest src/core src/users src/blog; \ From 84bab2ee2d2d40b624832247eba95a88e935419a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 13:28:41 +0000 Subject: [PATCH 02/14] Add indexes on created/modified for timestamped models (#891) Every ModelAdmin orders by -created, and blog models use get_latest_by='modified'. Declaring these as Meta.indexes lets makemigrations produce standard AddIndex operations without touching the django-extensions TimeStampedModel inheritance. Also adds a composite (exhibit_type, -created) index on Exhibit to cover filter+order patterns in users profile views. Closes #891 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- ...blog_clippi_created_34f17d_idx_and_more.py | 50 ++++++++++++ src/blog/models.py | 21 +++++ ...core_artwor_created_f9769d_idx_and_more.py | 81 +++++++++++++++++++ src/core/models.py | 36 +++++++++ 4 files changed, 188 insertions(+) create mode 100644 src/blog/migrations/0012_clipping_blog_clippi_created_34f17d_idx_and_more.py create mode 100644 src/core/migrations/0028_artwork_core_artwor_created_f9769d_idx_and_more.py diff --git a/src/blog/migrations/0012_clipping_blog_clippi_created_34f17d_idx_and_more.py b/src/blog/migrations/0012_clipping_blog_clippi_created_34f17d_idx_and_more.py new file mode 100644 index 00000000..ce21fa1f --- /dev/null +++ b/src/blog/migrations/0012_clipping_blog_clippi_created_34f17d_idx_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 6.0.4 on 2026-04-19 13:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0011_update_formatted_bodies"), + ("users", "0012_profileevent_profile_insert_insert_and_more"), + ] + + operations = [ + migrations.AddIndex( + model_name="clipping", + index=models.Index( + fields=["-created"], name="blog_clippi_created_34f17d_idx" + ), + ), + migrations.AddIndex( + model_name="clipping", + index=models.Index( + fields=["-modified"], name="blog_clippi_modifie_4aff76_idx" + ), + ), + migrations.AddIndex( + model_name="post", + index=models.Index( + fields=["-created"], name="blog_post_created_76eb4c_idx" + ), + ), + migrations.AddIndex( + model_name="post", + index=models.Index( + fields=["-modified"], name="blog_post_modifie_763e41_idx" + ), + ), + migrations.AddIndex( + model_name="postimage", + index=models.Index( + fields=["-created"], name="blog_postim_created_77e950_idx" + ), + ), + migrations.AddIndex( + model_name="postimage", + index=models.Index( + fields=["-modified"], name="blog_postim_modifie_ac830e_idx" + ), + ), + ] diff --git a/src/blog/models.py b/src/blog/models.py index 9438c267..f1bd24d5 100644 --- a/src/blog/models.py +++ b/src/blog/models.py @@ -30,6 +30,13 @@ class PostImage(TimeStampedModel): def __str__(self): return self.file.name.lstrip(IMAGE_BASE_PATH) + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"]), + models.Index(fields=["-modified"]), + ] + class Clipping(TimeStampedModel): id = models.BigAutoField(primary_key=True) @@ -42,6 +49,13 @@ class Clipping(TimeStampedModel): def __str__(self): return self.title + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"]), + models.Index(fields=["-modified"]), + ] + class Post(TimeStampedModel): id = models.BigAutoField(primary_key=True) @@ -92,3 +106,10 @@ def __str__(self): def get_absolute_url(self): return f"/memories/{self.slug}/" + + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"]), + models.Index(fields=["-modified"]), + ] diff --git a/src/core/migrations/0028_artwork_core_artwor_created_f9769d_idx_and_more.py b/src/core/migrations/0028_artwork_core_artwor_created_f9769d_idx_and_more.py new file mode 100644 index 00000000..11089b0d --- /dev/null +++ b/src/core/migrations/0028_artwork_core_artwor_created_f9769d_idx_and_more.py @@ -0,0 +1,81 @@ +# Generated by Django 6.0.4 on 2026-04-19 13:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0027_sound_soundevent_remove_artwork_insert_insert_and_more"), + ("users", "0012_profileevent_profile_insert_insert_and_more"), + ] + + operations = [ + migrations.AddIndex( + model_name="artwork", + index=models.Index( + fields=["-created"], name="core_artwor_created_f9769d_idx" + ), + ), + migrations.AddIndex( + model_name="artwork", + index=models.Index( + fields=["-modified"], name="core_artwor_modifie_f2886a_idx" + ), + ), + migrations.AddIndex( + model_name="exhibit", + index=models.Index( + fields=["-created"], name="core_exhibi_created_a7c2a1_idx" + ), + ), + migrations.AddIndex( + model_name="exhibit", + index=models.Index( + fields=["-modified"], name="core_exhibi_modifie_4e6366_idx" + ), + ), + migrations.AddIndex( + model_name="exhibit", + index=models.Index( + fields=["exhibit_type", "-created"], + name="core_exhibi_exhibit_8400bd_idx", + ), + ), + migrations.AddIndex( + model_name="marker", + index=models.Index( + fields=["-created"], name="core_marker_created_5613b5_idx" + ), + ), + migrations.AddIndex( + model_name="marker", + index=models.Index( + fields=["-modified"], name="core_marker_modifie_99c7c6_idx" + ), + ), + migrations.AddIndex( + model_name="object", + index=models.Index( + fields=["-created"], name="core_object_created_69c39d_idx" + ), + ), + migrations.AddIndex( + model_name="object", + index=models.Index( + fields=["-modified"], name="core_object_modifie_f5a679_idx" + ), + ), + migrations.AddIndex( + model_name="sound", + index=models.Index( + fields=["-created"], name="core_sound_created_ab0a8d_idx" + ), + ), + migrations.AddIndex( + model_name="sound", + index=models.Index( + fields=["-modified"], name="core_sound_modifie_bd78b7_idx" + ), + ), + ] diff --git a/src/core/models.py b/src/core/models.py index 7406e2ae..937c4808 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -201,6 +201,13 @@ def as_html_thumbnail(self, editable=False): return render(div(elements, style="margin: 10px auto;")) + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"]), + models.Index(fields=["-modified"]), + ] + class ExhibitTypes(models.TextChoices): AR = "AR", "Augmented Reality" @@ -284,6 +291,13 @@ def as_html_thumbnail(self, editable: bool = False): to_render.append(lower_menu) return render(to_render) + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"]), + models.Index(fields=["-modified"]), + ] + class ObjectExtensions(models.TextChoices): GIF = "gif", "GIF" @@ -408,6 +422,13 @@ def as_html_thumbnail(self, editable=False): return render(to_render) + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"]), + models.Index(fields=["-modified"]), + ] + @pghistory.track() class Artwork(TimeStampedModel, ContentMixin): @@ -491,6 +512,13 @@ def as_html_thumbnail(self, editable=False): ) return render(div(elements, class_="artwork-elements flex")) + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"]), + models.Index(fields=["-modified"]), + ] + @pghistory.track() class Exhibit(TimeStampedModel, ContentMixin, models.Model): @@ -599,6 +627,14 @@ def as_html_thumbnail(self, editable=False): ] return render(elements) + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"]), + models.Index(fields=["-modified"]), + models.Index(fields=["exhibit_type", "-created"]), + ] + @receiver(post_delete, sender=Object) @receiver(post_delete, sender=Marker) From 4fca6509e6687285b9dd3bd399a74dff93e4e046 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 01:31:39 +0000 Subject: [PATCH 03/14] Escape user-provided text in fast_html rendering (#888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fast_html does not escape text content or attribute values, and every as_html() / as_html_thumbnail() output is rendered with | safe in the jinja2 templates. User-controlled fields (title, author, name, slug, username) could therefore inject HTML/JS. Wrap user strings with django.utils.html.escape before passing them to fast_html on Sound, Marker, Object, and Exhibit. Also remove the | safe filter from post.excerpt in post_preview.jinja2 — excerpt is a plain TextField, so default jinja autoescaping is the correct behavior (post.formatted_body is separately sanitized by ProseEditor). Closes #888 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/blog/jinja2/blog/post_preview.jinja2 | 2 +- src/core/models.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/blog/jinja2/blog/post_preview.jinja2 b/src/blog/jinja2/blog/post_preview.jinja2 index 49b4ffb7..1968b972 100644 --- a/src/blog/jinja2/blog/post_preview.jinja2 +++ b/src/blog/jinja2/blog/post_preview.jinja2 @@ -4,7 +4,7 @@ {{ post.title }}

- {{ post.excerpt[:PREVIEW_SIZE] | safe }}... {{ _("Read More") }} + {{ post.excerpt[:PREVIEW_SIZE] }}... {{ _("Read More") }}

{% endfor %} diff --git a/src/core/models.py b/src/core/models.py index 937c4808..a6ef3e74 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.html import escape 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 @@ -178,7 +179,7 @@ def used_in_html_string(self): def as_html(self): attributes = { "id": self.id, - "title": self.title, + "title": escape(self.title), "src": self.file.url, } return render( @@ -190,7 +191,7 @@ def as_html(self): def as_html_thumbnail(self, editable=False): elements = [ - span(self.title, style="display:block;"), + span(escape(self.title), style="display:block;"), self.as_html(), ] if editable and not self.is_used_by_other_user(): @@ -254,7 +255,7 @@ def is_used_by_other_user(self): def as_html(self, height: int = None, width: int = None): attributes = { "id": self.id, - "title": self.title, + "title": escape(self.title), "src": self.source.url, } return render( @@ -378,7 +379,7 @@ def is_3d(self): def as_html(self, height: int = None, width: int = None): attributes = { "id": self.id, - "title": self.title, + "title": escape(self.title), "src": self.source.url, } if height: @@ -569,7 +570,9 @@ def content_type(self): def as_html_thumbnail(self, editable=False): link_to_exhibit = reverse("exhibit-detail", query={"id": self.id}) - exhibit_title = a(h1(self.name, class_="exhibit-name"), href=link_to_exhibit) + exhibit_title = a( + h1(escape(self.name), class_="exhibit-name"), href=link_to_exhibit + ) media_stats = [] if self.exhibit_type == ExhibitTypes.AR: media_stats.append( @@ -601,14 +604,17 @@ def as_html_thumbnail(self, editable=False): ) ) exhibit_info = [ - p([{_("Created by ")}, b(self.owner.user.username)], class_="by"), + p( + [{_("Created by ")}, b(escape(self.owner.user.username))], + class_="by", + ), p(self.date, class_="exbDate"), div(media_stats), ] button_see_this_exhibit = a( _("See this Exhibition"), - href=f"/{self.slug}/", + href=f"/{escape(self.slug)}/", class_="gotoExb", ) From 7671ca2155e99e0216751b47b26155e2f0b2839c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 01:45:57 +0000 Subject: [PATCH 04/14] Fix inverted last_page logic in blog_index (#857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit page.has_previous() returns True for every page except the first, so using it as last_page indicator was exactly backwards. Switch to not page.has_next() to match the intended semantics. The context key is currently not consumed by any template, so this has no visible effect — but fixes the misleading code. Closes #857 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/blog/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blog/views.py b/src/blog/views.py index 80665008..91372493 100644 --- a/src/blog/views.py +++ b/src/blog/views.py @@ -28,7 +28,7 @@ def blog_index(request): "next_page_number": page_number + 1, "posts": posts, "PREVIEW_SIZE": PREVIEW_SIZE, - "last_page": page.has_previous(), + "last_page": not page.has_next(), "total_pages": paginator.num_pages, "page_url": "/memories/", "blog_categories": Category.objects.all(), From 59e599ee6a62250c1ff438ca1cd81e43955d9722 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 01:46:51 +0000 Subject: [PATCH 05/14] Return 404 instead of 500 on missing post_detail (#856) post_detail was calling Post.objects.get(pk=pk) directly, so an invalid pk bubbled up DoesNotExist as an uncaught 500. Switch to get_object_or_404 to respond with a proper 404. Closes #856 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/blog/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/blog/views.py b/src/blog/views.py index 91372493..179c76e2 100644 --- a/src/blog/views.py +++ b/src/blog/views.py @@ -1,5 +1,5 @@ from django.core.paginator import Paginator -from django.shortcuts import render +from django.shortcuts import get_object_or_404, render from blog.models import Category, Clipping, Post, PostStatus @@ -40,7 +40,9 @@ def blog_index(request): def post_detail(request, pk): - post = Post.objects.prefetch_related("images", "categories").get(pk=pk) + post = get_object_or_404( + Post.objects.prefetch_related("images", "categories"), pk=pk + ) context = {"post": post, "images": post.images.all()} From ea9a3ac9c05502c79e0437404ea7e810661296ea Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 01:47:29 +0000 Subject: [PATCH 06/14] Remove broken Post.get_absolute_url (#855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The method referenced self.slug, but Post has no slug field, so any caller would hit AttributeError. It was never invoked anywhere in the codebase — leftover from development, per @pablodiegoss — so drop it instead of synthesizing a URL pattern. Closes #855 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/blog/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/blog/models.py b/src/blog/models.py index f1bd24d5..2d6dc22e 100644 --- a/src/blog/models.py +++ b/src/blog/models.py @@ -104,9 +104,6 @@ class Post(TimeStampedModel): def __str__(self): return self.title - def get_absolute_url(self): - return f"/memories/{self.slug}/" - class Meta: get_latest_by = "modified" indexes = [ From 5ef29e6ff51878c8f69180a58b8e49fdcf06c713 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 01:48:31 +0000 Subject: [PATCH 07/14] Remove unused ?user= path from profile view (#854) The profile view accepted ?user= from the query string, used it as a raw Profile lookup key, then fell back to request.user if empty. Nothing in the templates or tests hits the ?user= path, so an invalid value would just crash the request. Per @pablodiegoss, drop the dead branch instead of adding exception handling for something nobody calls. Profiles are auto-created on User post_save, so .get(user=request.user) after @login_required is safe. Closes #854 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/users/views.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/users/views.py b/src/users/views.py index 07ad2abc..e2b0c436 100644 --- a/src/users/views.py +++ b/src/users/views.py @@ -88,11 +88,6 @@ class ResetPasswordView(SuccessMessageMixin, PasswordResetView): @login_required @require_http_methods(["GET"]) def profile(request): - user = request.GET.get("user") - - if not user: - user = request.user - profile = Profile.objects.prefetch_related( "exhibits__artworks", "artworks__exhibits", @@ -103,7 +98,7 @@ def profile(request): "sounds__artworks", "sounds__ar_objects", "sounds__exhibits", - ).get(user=user) + ).get(user=request.user) ar_exhibits = ( profile.exhibits.filter(exhibit_type=ExhibitTypes.AR).all().order_by("-created") From ba328a2acbf402ffa3e63345433bdbcdfbd70c1d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 03:39:45 +0000 Subject: [PATCH 08/14] Fix data URI MIME type to match PNG payload (#852) MarkerGeneratorAPIView.post() saves the generated marker with format='PNG' but labeled the base64 data URI as image/jpeg. Strict browsers and any downstream MIME-checking code got a mismatch. Closes #852 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/core/views/api_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/views/api_views.py b/src/core/views/api_views.py index bf1cada2..a817724c 100644 --- a/src/core/views/api_views.py +++ b/src/core/views/api_views.py @@ -61,7 +61,7 @@ def post(self, request, *args, **kwargs): base64_encoded_result_str = base64_encoded_result_bytes.decode("ascii") # Create an HTML image tag with the base64 data - transformed_image = f"data:image/jpeg;base64,{base64_encoded_result_str}" + transformed_image = f"data:image/png;base64,{base64_encoded_result_str}" return HttpResponse( render( From a5172a3a7ced67b78da3e72792155443775fa92d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 03:41:03 +0000 Subject: [PATCH 09/14] Use non-capturing group for me_hotsite URL (#851) The regex (me|ME) captured the matched casing and forwarded it as a positional arg to me_hotsite, which absorbed it via an unused _. Switch to (?:me|ME) and drop the now-unneeded parameter so the view signature reflects what it actually receives. Closes #851 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/core/urls.py | 2 +- src/core/views/static_views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/urls.py b/src/core/urls.py index 6d8b9fc9..abc40c6d 100644 --- a/src/core/urls.py +++ b/src/core/urls.py @@ -63,7 +63,7 @@ path("sounds/upload/", sound_upload, name="sound-upload"), path("elements/", get_element, name="get-element"), re_path( - r"^(me|ME)\/?$", + r"^(?:me|ME)\/?$", me_hotsite, name="mitologia-extendida", ), diff --git a/src/core/views/static_views.py b/src/core/views/static_views.py index 22dff02a..63a58295 100644 --- a/src/core/views/static_views.py +++ b/src/core/views/static_views.py @@ -19,7 +19,7 @@ def health_check(_): return JsonResponse({"status": "ok"}, status=200) -def me_hotsite(request, _): +def me_hotsite(request): return render(request, "core/ME/hotsite.html", {}) From cbbee6b7aa032f3d55b1ffb16b59a4a6534937c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 03:41:46 +0000 Subject: [PATCH 10/14] Fix seeall context key typo in related_content (#850) Two of the three branches in related_content were writing "seeall:" (with a trailing colon) into the context dict, so the template's {% if seeall == False %} block fell through to the "not set" path. @pablodiegoss noted the default kept this from visibly breaking. Closes #850 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/core/views/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/views/views.py b/src/core/views/views.py index cf73ccdb..d915e0d2 100644 --- a/src/core/views/views.py +++ b/src/core/views/views.py @@ -660,7 +660,7 @@ def related_content(request): .distinct() ) - ctx = {"artworks": artworks, "exhibits": exhibits, "seeall:": False} + ctx = {"artworks": artworks, "exhibits": exhibits, "seeall": False} elif element_type == "artwork": element = Artwork.objects.prefetch_related( @@ -669,7 +669,7 @@ def related_content(request): exhibits = element.exhibits.all() - ctx = {"exhibits": exhibits, "seeall:": False} + ctx = {"exhibits": exhibits, "seeall": False} elif element_type == "sound": element = Sound.objects.prefetch_related( From 4da5326913f3972d3787b54bc2f45447c1da8fd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 03:42:21 +0000 Subject: [PATCH 11/14] Guard audio_description deletion with truthiness check (#849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calling .delete() on an empty FieldFile is already a no-op in Django, which is why this never crashed in practice, but the call is easier to read with an explicit guard — and it avoids invoking storage.delete / save on a missing file path. Closes #849 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/core/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/models.py b/src/core/models.py index a6ef3e74..cf988bdd 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -650,6 +650,7 @@ def remove_source_file(sender, instance, **kwargs): instance.source.delete(False) if isinstance(instance, Object): instance.source.delete(False) - instance.audio_description.delete(False) + if instance.audio_description: + instance.audio_description.delete(False) if isinstance(instance, Sound): instance.file.delete(False) From 4e845167a330516c788d5b08da13c3bab6fc459b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 03:42:52 +0000 Subject: [PATCH 12/14] Stop filtering Sound queryset by ObjectExtensions.GLB (#848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ArtworkForm.selected_sound was excluding Sound rows whose file_extension equals ObjectExtensions.GLB. Sound extensions are mp3/ogg/wav, so the filter matched nothing — but referencing the wrong enum in the wrong model is a waiting bug. Match the sibling UploadObjectForm.selected_sound and use Sound.objects.all(). Closes #848 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/core/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/forms.py b/src/core/forms.py index 230940f6..fa0310c4 100644 --- a/src/core/forms.py +++ b/src/core/forms.py @@ -184,7 +184,7 @@ class ArtworkForm(forms.ModelForm): widget=RangeInput(attrs={"class": "slider", "step": "0.1"}), ) selected_sound = forms.ModelChoiceField( - queryset=Sound.objects.exclude(file_extension=ObjectExtensions.GLB), + queryset=Sound.objects.all(), required=False, widget=forms.Select(attrs={"class": "form-control"}), ) From 27dcf9bfe77c67a0678e7b81a42529592a029dd5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 03:43:27 +0000 Subject: [PATCH 13/14] Return 404 instead of 500 on missing marker_preview (#847) marker_preview fetched Marker.objects.get(id=marker_id) directly, so an invalid or missing id raised DoesNotExist/ValueError through to a 500. Switch to the already-imported get_object_or_404. Closes #847 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/core/views/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/views/views.py b/src/core/views/views.py index d915e0d2..4c0c91d9 100644 --- a/src/core/views/views.py +++ b/src/core/views/views.py @@ -189,7 +189,7 @@ def object_upload(request): @require_http_methods(["GET"]) def marker_preview(request): marker_id = request.GET.get("id") - marker = Marker.objects.get(id=marker_id) + marker = get_object_or_404(Marker, id=marker_id) artwork = { "marker": marker, "augmented": marker, From 0e8dab71ae356fe179e97245d2997cbf779b1144 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 03:44:23 +0000 Subject: [PATCH 14/14] Use get_object_or_404 in edit_marker and edit_object (#846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both views called Model.objects.get(id=index) directly, so an invalid or missing id turned into a 500 (DoesNotExist / ValueError). Switch to get_object_or_404 — Django 6 handles both cases — and drop the now-redundant 'if not model' branch. The permission check still runs after the fetch, per @pablodiegoss's note that ownership lives on the row itself. Tighten request.user.profile access to match the rest of the file instead of re-fetching Profile. Closes #846 https://claude.ai/code/session_01XC1THLWgnGXGf5wgRhdyvB --- src/core/views/views.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/core/views/views.py b/src/core/views/views.py index 4c0c91d9..2bb1b2eb 100644 --- a/src/core/views/views.py +++ b/src/core/views/views.py @@ -226,7 +226,10 @@ def marker_upload(request): @login_required def edit_marker(request): index = request.GET.get("id", "-1") - model = Marker.objects.get(id=index) + model = get_object_or_404(Marker, id=index) + + if model.owner != request.user.profile: + raise Http404 model_data = { "source": model.source, @@ -236,9 +239,6 @@ def edit_marker(request): "title": model.title, } - if not model or model.owner != Profile.objects.get(user=request.user): - raise Http404 - if request.method == "POST": form = UploadMarkerForm(request.POST, request.FILES, instance=model) @@ -263,7 +263,10 @@ def edit_marker(request): @login_required def edit_object(request): index = request.GET.get("id", "-1") - model = Object.objects.get(id=index) + model = get_object_or_404(Object, id=index) + + if model.owner != request.user.profile: + raise Http404 model_data = { "source": model.source, @@ -272,8 +275,6 @@ def edit_object(request): "title": model.title, "thumbnail": model.thumbnail, } - if not model or model.owner != Profile.objects.get(user=request.user): - raise Http404 if request.method == "POST": form = UploadObjectForm(request.POST, request.FILES, instance=model)