Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 4 additions & 2 deletions src/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ def get_home_status(
user,
status,
sort_by,
items_limit,
items_limit=None,
specific_media_type=None,
):
"""Get a home media list for a specific status grouped by media type."""
Expand Down Expand Up @@ -442,7 +442,9 @@ def get_home_status(

# Apply pagination
total_count = len(sorted_list)
if specific_media_type:
if items_limit is None:
paginated_list = sorted_list
elif specific_media_type:
paginated_list = sorted_list[items_limit:]
else:
paginated_list = sorted_list[:items_limit]
Expand Down
77 changes: 77 additions & 0 deletions src/app/tests/views/test_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Sources,
Status,
)
from events.models import Event
from users.models import HomeSortChoices


Expand Down Expand Up @@ -142,6 +143,16 @@ def mock_get_media_metadata(
status=Status.IN_PROGRESS.value,
progress=10,
)
Event.objects.create(
item=anime_item,
content_number=16,
datetime=timezone.now() - timezone.timedelta(days=1),
)
Event.objects.create(
item=anime_item,
content_number=17,
datetime=timezone.now() + timezone.timedelta(days=1),
)

movie_item = Item.objects.create(
media_id="10",
Expand All @@ -155,6 +166,11 @@ def mock_get_media_metadata(
user=self.user,
status=Status.PLANNING.value,
)
Event.objects.create(
item=season_item,
content_number=6,
datetime=timezone.now() + timezone.timedelta(days=1),
)

def test_home_view(self):
"""Test the home view displays in-progress and planning media."""
Expand Down Expand Up @@ -201,6 +217,67 @@ def test_home_view_with_sort(self):
self.user.refresh_from_db()
self.assertEqual(self.user.home_sort, "completion")

def test_home_view_separates_incoming_when_enabled(self):
"""Test home view splits incoming media from in-progress."""
self.user.home_separate_incoming = True
self.user.save(update_fields=["home_separate_incoming"])

response = self.client.get(reverse("home"))

self.assertEqual(response.status_code, 200)

sections_by_key = {
section["key"]: section for section in response.context["home_sections"]
}
self.assertIn("incoming", sections_by_key)
self.assertIn(Status.IN_PROGRESS.value, sections_by_key)

incoming_section = sections_by_key["incoming"]
in_progress_section = sections_by_key[Status.IN_PROGRESS.value]

self.assertEqual(incoming_section["count"], 2)
self.assertEqual(in_progress_section["count"], 1)
self.assertIn(MediaTypes.SEASON.value, incoming_section["media_types"])
self.assertIn(MediaTypes.ANIME.value, incoming_section["media_types"])
self.assertIn(MediaTypes.ANIME.value, in_progress_section["media_types"])

def test_home_view_htmx_load_more_for_incoming(self):
"""Test HTMX load more for incoming section."""
self.user.home_separate_incoming = True
self.user.save(update_fields=["home_separate_incoming"])

for i in range(6, 20):
anime_item = Item.objects.create(
media_id=f"incoming-{i}",
source=Sources.MAL.value,
media_type=MediaTypes.ANIME.value,
title=f"Incoming Anime {i}",
image="http://example.com/image.jpg",
)
Anime.objects.create(
item=anime_item,
user=self.user,
status=Status.IN_PROGRESS.value,
progress=1,
)
Event.objects.create(
item=anime_item,
content_number=2,
datetime=timezone.now() + timezone.timedelta(days=1),
)

response = self.client.get(
reverse("home")
+ f"?load_status=incoming&load_media_type={MediaTypes.ANIME.value}",
headers={"hx-request": "true"},
)

self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "app/components/home_grid.html")
self.assertEqual(response.context["home_status"], "incoming")
self.assertEqual(len(response.context["media_list"]["items"]), 1)
self.assertEqual(response.context["media_list"]["total"], 15)

@patch("app.providers.services.get_media_metadata")
def test_home_view_htmx_load_more(self, mock_get_media_metadata):
"""Test the HTMX load more functionality."""
Expand Down
187 changes: 159 additions & 28 deletions src/app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,49 +36,180 @@

logger = logging.getLogger(__name__)

HOME_SECTION_INCOMING = "incoming"


def _build_home_section(key, media_types):
"""Build home section payload."""
return {
"key": key,
"id": slugify(key),
"media_types": media_types,
"count": sum(media_list["total"] for media_list in media_types.values()),
}


def _filter_home_media_types(media_types, predicate):
"""Filter home media entries by predicate."""
filtered_media_types = {}
for media_type, media_list in media_types.items():
filtered_items = [media for media in media_list["items"] if predicate(media)]
if filtered_items:
filtered_media_types[media_type] = {
"items": filtered_items,
"total": len(filtered_items),
}
return filtered_media_types


def _paginate_home_media_types(media_types, items_limit, page_start=0):
"""Paginate already-grouped home media entries."""
paginated_media_types = {}
for media_type, media_list in media_types.items():
items = media_list["items"][page_start : page_start + items_limit]
Comment thread
thespbgamer marked this conversation as resolved.
Outdated

if items:
paginated_media_types[media_type] = {
"items": items,
"total": media_list["total"],
}
return paginated_media_types


def _is_incoming_media(media):
"""Return True when media has a real upcoming release."""
return bool(media.next_event and not media.next_event.is_max_datetime)


def _is_active_in_progress_media(media):
"""Return True when media still has released backlog."""
if not _is_incoming_media(media):
return True

return media.max_progress is not None and media.progress < media.max_progress


def _get_split_in_progress_media_types(request, sort_by, items_limit, page_start=0):
"""Return incoming and in-progress media types split by upcoming event."""
all_in_progress_media_types = BasicMedia.objects.get_home_status(
user=request.user,
status=Status.IN_PROGRESS.value,
sort_by=sort_by,
items_limit=None,
)
incoming_media_types = _filter_home_media_types(
all_in_progress_media_types,
_is_incoming_media,
)
active_media_types = _filter_home_media_types(
all_in_progress_media_types,
_is_active_in_progress_media,
)

return {
HOME_SECTION_INCOMING: _paginate_home_media_types(
incoming_media_types,
items_limit,
page_start=page_start,
),
Status.IN_PROGRESS.value: _paginate_home_media_types(
active_media_types,
items_limit,
page_start=page_start,
),
}


def _uses_split_home_sections(user, section_key):
"""Return True when section uses split in-progress home data."""
return user.home_separate_incoming and section_key in (
HOME_SECTION_INCOMING,
Status.IN_PROGRESS.value,
)


def _get_home_section_media_types(request, sort_by, section_key, items_limit):
"""Return media types for home section, including virtual incoming section."""
if _uses_split_home_sections(request.user, section_key):
return _get_split_in_progress_media_types(request, sort_by, items_limit)[
section_key
]

return BasicMedia.objects.get_home_status(
user=request.user,
status=section_key,
sort_by=sort_by,
items_limit=items_limit,
)


def _get_home_load_more_media_types(
request,
sort_by,
section_key,
items_limit,
media_type_to_load,
):
"""Return load-more payload for a specific home section/media type."""
if _uses_split_home_sections(request.user, section_key):
return _get_split_in_progress_media_types(
request,
sort_by,
items_limit,
page_start=items_limit,
)[section_key]
Comment thread
thespbgamer marked this conversation as resolved.

return BasicMedia.objects.get_home_status(
user=request.user,
status=section_key,
sort_by=sort_by,
items_limit=items_limit,
specific_media_type=media_type_to_load,
)


def _get_home_section_keys(user):
"""Return ordered home section keys for current user."""
if user.home_separate_incoming:
return [Status.IN_PROGRESS.value, HOME_SECTION_INCOMING, Status.PLANNING.value]

return [Status.IN_PROGRESS.value, Status.PLANNING.value]


@require_GET
def home(request):
"""Home page with media items in progress and planning."""
sort_by = request.user.update_preference("home_sort", request.GET.get("sort"))
media_type_to_load = request.GET.get("load_media_type")
status_to_load = request.GET.get("load_status", Status.IN_PROGRESS.value)
section_to_load = request.GET.get("load_status", Status.IN_PROGRESS.value)
items_limit = 14

# If this is an HTMX request to load more items for a specific media type
if request.headers.get("HX-Request") and media_type_to_load:
list_by_type = BasicMedia.objects.get_home_status(
user=request.user,
status=status_to_load,
sort_by=sort_by,
items_limit=items_limit,
specific_media_type=media_type_to_load,
list_by_type = _get_home_load_more_media_types(
request,
sort_by,
section_to_load,
items_limit,
media_type_to_load,
)
context = {
"media_list": list_by_type.get(media_type_to_load, []),
"home_status": status_to_load,
}
return render(request, "app/components/home_grid.html", context)

home_sections = []
for status in (Status.IN_PROGRESS.value, Status.PLANNING.value):
media_types = BasicMedia.objects.get_home_status(
user=request.user,
status=status,
sort_by=sort_by,
items_limit=items_limit,
)
home_sections.append(
return render(
request,
"app/components/home_grid.html",
{
"key": status,
"id": slugify(status),
"media_types": media_types,
"count": sum(
media_list["total"] for media_list in media_types.values()
),
"media_list": list_by_type.get(media_type_to_load, []),
"home_status": section_to_load,
},
)

home_sections = [
_build_home_section(
section_key,
_get_home_section_media_types(request, sort_by, section_key, items_limit),
)
for section_key in _get_home_section_keys(request.user)
]
Comment thread
thespbgamer marked this conversation as resolved.

context = {
"home_sections": home_sections,
"current_sort": sort_by,
Expand Down
Loading