Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/templates/accounts/profile_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
{% endblock title %}
{% block content %}
<div class="max-w-2xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">{% translate "Edit Profile" %}</h1>
<div class="flex items-center gap-4 mb-8">
{% include "partials/common/back_button.html" %}
<h1 class="text-4xl font-bold">{% translate "Edit Profile" %}</h1>
</div>
<!-- Profile Information -->
<div class="card bg-base-200 shadow-xl mb-8">
<div class="card-body">
Expand Down Expand Up @@ -140,8 +143,5 @@ <h2 class="card-title">{% translate "Change Password" %}</h2>
</form>
</div>
</div>
<div class="mt-8">
<a href="{% url 'home' %}" class="btn btn-ghost">← {% translate "Back to Home" %}</a>
</div>
</div>
{% endblock content %}
14 changes: 4 additions & 10 deletions src/templates/base/backup_manage.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
{% endblock title %}
{% block content %}
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-8">{% translate "Backup Management" %}</h1>
<div class="flex items-center gap-4 mb-8">
{% include "partials/common/back_button.html" %}
<h1 class="text-4xl font-bold">{% translate "Backup Management" %}</h1>
</div>
<!-- Messages -->
{% if messages %}
<div class="mb-6">
Expand Down Expand Up @@ -101,15 +104,6 @@ <h2 class="card-title text-warning">
</div>
</div>
</div>
<!-- Back Button -->
<div class="mt-6">
<button type="button"
onclick="history.length> 1 ? history.back() : window.location.href='{% url 'home' %}'"
class="btn btn-ghost">
{% lucide "arrow-left" %}
{% translate "Back to Home" %}
</button>
</div>
</div>
{# Confirmation modal for backup import #}
{% include "partials/common/confirm_modal.html" with modal_id="confirm-import-modal" title=_("⚠️ WARNING: Destructive Action") message=_("This action will DELETE ALL your current data and replace it with the backup data. Are you absolutely sure you want to continue?") confirm_text=_("Yes, import backup") is_danger=True form_id="import-backup-form" %}
Expand Down
5 changes: 1 addition & 4 deletions src/templates/base/media_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@
{# Header #}
<div class="flex justify-between items-start gap-4 mb-6">
<div class="flex-1">
<button type="button"
onclick="history.length > 1 ? history.back() : window.location.href='{% url 'home' %}'"
class="btn btn-ghost btn-sm btn-circle"
aria-label="{% translate "Back" %}">{% lucide "arrow-left" %}</button>
{% include "partials/common/back_button.html" %}
<h1 class="text-4xl font-bold">
{{ media.title }}
{% if media.pub_year %}<span class="text-2xl opacity-70 font-normal">({{ media.pub_year }})</span>{% endif %}
Expand Down
5 changes: 1 addition & 4 deletions src/templates/base/media_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@
{# Header #}
<div class="flex justify-between items-start gap-4 mb-6">
<div class="flex-1">
<button type="button"
onclick="history.length > 1 ? history.back() : window.location.href='{% url 'home' %}'"
class="btn btn-ghost btn-sm btn-circle"
aria-label="{% translate "Back" %}">{% lucide "arrow-left" %}</button>
{% include "partials/common/back_button.html" %}
<h1 class="text-4xl font-bold">
{% if media %}
{% translate "Edit" %} {{ media.title }}
Expand Down
8 changes: 5 additions & 3 deletions src/templates/base/media_import.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
<div class="max-w-2xl mx-auto">
{# Header #}
<div class="flex items-center gap-4 mb-6">
<a href="{% if media_id %}{% url 'media_edit' media_id %}{% else %}{% url 'media_add' %}{% endif %}"
class="btn btn-ghost btn-sm btn-circle"
aria-label="{% translate 'Back' %}">{% lucide "arrow-left" %}</a>
{% include "partials/common/back_button.html" %}
<h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
</div>
{# Source selector tabs #}
Expand All @@ -20,24 +18,28 @@ <h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
aria-label="{% translate 'Movies & TV' %}"
id="tab-tmdb"
checked
autocomplete="off"
hx-on:click="document.getElementById('source-tmdb').classList.remove('hidden'); document.getElementById('source-igdb').classList.add('hidden'); document.getElementById('source-openlibrary').classList.add('hidden'); document.getElementById('source-musicbrainz').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
<input type="radio"
name="import_source"
class="tab"
aria-label="{% translate 'Video games' %}"
id="tab-igdb"
autocomplete="off"
hx-on:click="document.getElementById('source-igdb').classList.remove('hidden'); document.getElementById('source-tmdb').classList.add('hidden'); document.getElementById('source-openlibrary').classList.add('hidden'); document.getElementById('source-musicbrainz').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
<input type="radio"
name="import_source"
class="tab"
aria-label="{% translate 'Books' %}"
id="tab-openlibrary"
autocomplete="off"
hx-on:click="document.getElementById('source-openlibrary').classList.remove('hidden'); document.getElementById('source-tmdb').classList.add('hidden'); document.getElementById('source-igdb').classList.add('hidden'); document.getElementById('source-musicbrainz').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
<input type="radio"
name="import_source"
class="tab"
aria-label="{% translate 'Music' %}"
id="tab-musicbrainz"
autocomplete="off"
hx-on:click="document.getElementById('source-musicbrainz').classList.remove('hidden'); document.getElementById('source-tmdb').classList.add('hidden'); document.getElementById('source-igdb').classList.add('hidden'); document.getElementById('source-openlibrary').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
</div>
{# TMDB Search (Movies & TV) #}
Expand Down
8 changes: 8 additions & 0 deletions src/templates/partials/common/back_button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% load i18n %}
{% load lucide %}
<button type="button"
onclick="history.length > 1 ? history.back() : window.location.href='{% url 'home' %}'"
class="btn btn-ghost btn-sm btn-circle"
aria-label="{% translate 'Back' %}">
{% lucide "arrow-left" %}
</button>
84 changes: 84 additions & 0 deletions src/tests/core/test_context_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
Tests for core.context_processors module.

These tests verify the behavior of context processors.
"""

from django.contrib.auth.models import AnonymousUser
from django.test import RequestFactory

from core.context_processors import saved_views
from core.models import SavedView


class TestSavedViewsContextProcessor:
"""Tests for the saved_views context processor."""

def test_authenticated_user_gets_their_saved_views(self, user, db):
"""Authenticated users receive their saved views in context."""
# Create saved views for the user
SavedView.objects.create(user=user, name="View 1")
SavedView.objects.create(user=user, name="View 2")

# Create a mock request with the authenticated user
factory = RequestFactory()
request = factory.get("/")
request.user = user

result = saved_views(request)

assert "saved_views" in result
assert result["saved_views"].count() == 2

def test_authenticated_user_only_gets_own_views(self, user, django_user_model, db):
"""Users only receive their own saved views, not others'."""
# Create another user with saved views
other_user = django_user_model.objects.create_user(
username="otheruser",
email="other@example.com",
password="testpass123",
)
SavedView.objects.create(user=other_user, name="Other View")
SavedView.objects.create(user=user, name="My View")

factory = RequestFactory()
request = factory.get("/")
request.user = user

result = saved_views(request)

assert result["saved_views"].count() == 1
assert result["saved_views"].first().name == "My View"

def test_anonymous_user_gets_empty_queryset(self, db):
"""Anonymous users receive an empty queryset."""

factory = RequestFactory()
request = factory.get("/")
request.user = AnonymousUser()

result = saved_views(request)

assert "saved_views" in result
assert result["saved_views"].count() == 0

def test_saved_views_available_on_all_pages(self, logged_in_client, user, db):
"""Saved views are available in context on various pages."""
from django.urls import reverse

SavedView.objects.create(user=user, name="Test View")

# Test home page
response = logged_in_client.get(reverse("home"))
assert "saved_views" in response.context
assert response.context["saved_views"].count() == 1

# Test import page
response = logged_in_client.get(reverse("media_import"))
assert "saved_views" in response.context
assert response.context["saved_views"].count() == 1

# Test backup manage page
response = logged_in_client.get(reverse("backup_manage"))
assert "saved_views" in response.context
assert response.context["saved_views"].count() == 1
86 changes: 86 additions & 0 deletions src/tests/core/test_musicbrainz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
Tests for MusicBrainz integration in views.

These tests verify the application behavior, not the external API.
"""

from unittest.mock import MagicMock, patch

import requests
from django.urls import reverse

from core.services.musicbrainz import MusicBrainzResult


class TestMusicBrainzSearchView:
"""Tests for the musicbrainz_search_htmx view."""

def test_returns_empty_for_short_query(self, logged_in_client):
"""Returns empty results for queries shorter than minimum length."""
response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "a"})

assert response.status_code == 200
assert "partials/musicbrainz/musicbrainz_suggestions.html" in [t.name for t in response.templates]
assert response.context["results"] == []

def test_returns_empty_for_empty_query(self, logged_in_client):
"""Returns empty results for empty query."""
response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": ""})

assert response.status_code == 200
assert response.context["results"] == []

@patch("core.views.get_musicbrainz_client")
def test_returns_search_results(self, mock_get_client, logged_in_client):
"""Returns search results from MusicBrainz client."""
mock_client = MagicMock()
mock_client.search_releases.return_value = [
MusicBrainzResult(
mbid="test-mbid-123",
title="Abbey Road",
artists=["The Beatles"],
year=1969,
country="GB",
label="Apple Records",
)
]
mock_get_client.return_value = mock_client

response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "abbey road"})

assert response.status_code == 200
assert len(response.context["results"]) == 1
assert response.context["results"][0].title == "Abbey Road"
mock_client.search_releases.assert_called_once()

@patch("core.views.get_musicbrainz_client")
def test_handles_api_error_gracefully(self, mock_get_client, logged_in_client):
"""Handles API errors gracefully and shows error message."""
mock_client = MagicMock()
mock_client.search_releases.side_effect = requests.RequestException("API Error")
mock_get_client.return_value = mock_client

response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "test query"})

assert response.status_code == 200
assert "error" in response.context
assert response.context["error"] == "Search failed"

def test_preserves_media_id_in_context(self, logged_in_client):
"""Preserves media_id in context for editing existing media."""
response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "", "media_id": "42"})

assert response.status_code == 200
assert response.context["media_id"] == "42"

@patch("core.views.get_musicbrainz_client")
def test_preserves_query_in_context(self, mock_get_client, logged_in_client):
"""Preserves search query in context."""
mock_client = MagicMock()
mock_client.search_releases.return_value = []
mock_get_client.return_value = mock_client

response = logged_in_client.get(reverse("musicbrainz_search_htmx"), {"q": "test"})

assert response.status_code == 200
assert response.context["query"] == "test"