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; \ 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/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..2d6dc22e 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) @@ -90,5 +104,9 @@ 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 = [ + models.Index(fields=["-created"]), + models.Index(fields=["-modified"]), + ] diff --git a/src/blog/views.py b/src/blog/views.py index 80665008..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 @@ -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(), @@ -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()} 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"}), ) 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..cf988bdd 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(): @@ -201,6 +202,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" @@ -247,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( @@ -284,6 +292,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" @@ -364,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: @@ -408,6 +423,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 +513,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): @@ -541,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( @@ -573,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", ) @@ -599,6 +633,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) @@ -608,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) 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/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( 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", {}) diff --git a/src/core/views/views.py b/src/core/views/views.py index cf73ccdb..2bb1b2eb 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, @@ -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) @@ -660,7 +661,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 +670,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( 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")