Add the option to filter lists by status#928
Conversation
- Introduced `list_detail_status` in user preferences with validations and choices. - Updated templates to include UI for status filtering. - Enhanced backend to filter items based on the selected status. - Applied database constraints for valid `list_detail_status` values.
…iciency - Removed redundant default status assignment. - Consolidated media filtering logic to avoid duplication. - Ensured distinct media type retrieval for better query optimization. - Streamlined media object mapping to improve code maintainability.
…ons and add `status` filter test logic
|
This is a huge, HUGE feature for most people and I feel like it's being overshadowed by other requests. Allow me to quickly explain my use case, and (other than the high fees now), why I'm leaving Trakt: I have a custom list of things me and my partner love watching. We filter this list by "unwatched", and boom! All the new things every week pop right up. Additionally, it would be nice to set a custom order for shows. Bob's Burger's is one of our favorites, it would be so cool to see that as #1. Then, #2 (if there's a new episode) might be something like Hazbin Hotel, another favorite. I don't see any way to do this on Yamtrack... which is a bummer, because I absolutely love this software. Trakt removed this from their app recently, which was a huge step backwards. If it can be added here, that would be so darn helpful, practical, and COOL. Please consider, and thank you so much for sharing this wonderful software with us!! Edit: I'm actually going to likely pay Trakt more money at this point purely because I can't seem to find this feature here, or on SIMKL (unless I pay them almost as much as Trakt wants). If there's a way to put a bounty on certain features, I will absolutely fork over cash for a feature like this! |
There was a problem hiding this comment.
Pull request overview
This pull request adds status-based filtering functionality to custom list detail views, allowing users to filter list items by their completion status (e.g., Completed, In Progress, Planning, etc.). The implementation includes backend changes to persist user preferences, frontend UI enhancements with a new filter dropdown, and comprehensive test coverage.
Changes:
- Added a
list_detail_statusfield to the User model to persist status filter preferences across sessions - Implemented status filtering logic in the
list_detailview to filter items based on user's status across different media types - Enhanced the list detail template with a new status filter dropdown UI using Alpine.js and HTMX
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/users/models.py |
Added list_detail_status field with MediaStatusChoices and database constraint |
src/users/migrations/0038_user_list_detail_status_alter_user_home_sort_and_more.py |
Migration to add the new field and constraints |
src/lists/views.py |
Implemented status filtering logic and updated context to include status choices |
src/templates/lists/list_detail.html |
Added status filter dropdown UI and integrated with Alpine.js state management |
src/lists/tests/test_views.py |
Refactored test setup and added test coverage for status filtering |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Get distinct media types for filtering by status | ||
| media_types = items.values_list("media_type", flat=True).distinct() | ||
| media_by_item_id = {} | ||
| media_manager = MediaManager() | ||
|
|
||
| # Filter by status if specified | ||
| if params["status_filter"] != MediaStatusChoices.ALL: | ||
| # Get the list of item IDs that have the specified status for the current user | ||
| # We need to check across all media types since items can be of different types | ||
| item_ids_with_status = set() | ||
|
|
||
| for media_type in media_types: | ||
| model = apps.get_model("app", media_type) | ||
|
|
||
| if media_type == MediaTypes.EPISODE.value: | ||
| # Episodes are linked through seasons | ||
| filter_kwargs = { | ||
| "related_season__user": request.user, | ||
| "related_season__status": params["status_filter"], | ||
| "item__in": [item.id for item in items], | ||
| } | ||
| else: | ||
| filter_kwargs = { | ||
| "user": request.user, | ||
| "status": params["status_filter"], | ||
| "item__in": [item.id for item in items], | ||
| } | ||
| queryset = model.objects.filter(**filter_kwargs).select_related("item") | ||
| queryset = media_manager._apply_prefetch_related(queryset, media_type) | ||
| media_manager.annotate_max_progress(queryset, media_type) | ||
|
|
||
| # Map media objects by item_id | ||
| for entry in queryset: | ||
| media_by_item_id.setdefault(entry.item_id, entry) | ||
| item_ids_with_status.add(entry.item_id) | ||
|
|
||
| # Filter items to only those with the specified status | ||
| items = items.filter(id__in=item_ids_with_status) | ||
|
|
There was a problem hiding this comment.
When status_filter is not set to ALL, the media objects are fetched and stored in media_by_item_id during the status filtering logic. However, when status_filter is ALL, no media objects are fetched, which means items_page items won't have their media attribute set. This creates inconsistent behavior where items have media objects only when filtering by status but not when showing all items. The media fetching logic that was removed should still execute for items that weren't already processed during status filtering.
| filter_kwargs = { | ||
| "related_season__user": request.user, | ||
| "related_season__status": params["status_filter"], | ||
| "item__in": [item.id for item in items], |
There was a problem hiding this comment.
This implementation creates an N+1 query pattern. The code iterates over all items in the queryset with [item.id for item in items], which forces evaluation of the queryset. This happens before any actual filtering, pagination, or processing occurs. For large lists, this could be inefficient. Consider using items.values_list('id', flat=True) instead to avoid loading full Item objects when you only need IDs.
| "item__in": [item.id for item in items], | ||
| } | ||
| else: | ||
| filter_kwargs = { | ||
| "user": request.user, | ||
| "status": params["status_filter"], | ||
| "item__in": [item.id for item in items], |
There was a problem hiding this comment.
This implementation creates an N+1 query pattern. The code iterates over all items in the queryset with [item.id for item in items], which forces evaluation of the queryset. This happens before any actual filtering, pagination, or processing occurs. For large lists, this could be inefficient. Consider using items.values_list('id', flat=True) instead to avoid loading full Item objects when you only need IDs.
| "item__in": [item.id for item in items], | |
| } | |
| else: | |
| filter_kwargs = { | |
| "user": request.user, | |
| "status": params["status_filter"], | |
| "item__in": [item.id for item in items], | |
| "item__in": items.values_list("id", flat=True), | |
| } | |
| else: | |
| filter_kwargs = { | |
| "user": request.user, | |
| "status": params["status_filter"], | |
| "item__in": items.values_list("id", flat=True), |
| """Test the list_detail view with status filter.""" | ||
| mock_update_preference.return_value = "date_added" | ||
| mock_user_can_view.return_value = True |
There was a problem hiding this comment.
The mock for update_preference only returns "date_added", but the view calls update_preference twice: once for "list_detail_sort" and once for "list_detail_status". The mock should either use side_effect to return different values for each call, or the test should verify that both calls are being made correctly. Currently, the status filter would get "date_added" as its value which is invalid for MediaStatusChoices, but the update_preference implementation should handle this by returning the current value. This test might not be catching potential bugs in the update_preference logic.
| mock_update_preference.return_value = "date_added" | ||
| mock_user_can_view.return_value = True | ||
|
|
||
| # Test the view with media type filter |
There was a problem hiding this comment.
The comment says "Test the view with media type filter" but this test is actually testing status filtering, not media type filtering. The comment should be updated to accurately reflect what the test does.
| # Test the view with media type filter | |
| # Test the view with status filter |
| </div> | ||
|
|
||
| <div x-data="{ sort: '{{ current_sort }}', type: '{{ request.GET.type|default:'all' }}', query: '{{ request.GET.q|default:''|escapejs }}', mediaTypeLabels: { | ||
| <div x-data="{ sort: '{{ current_sort }}', type: '{{ request.GET.type|default:'all' }}', status: '{{ current_status }}', query: '{{ request.GET.q|default:'' }}', mediaTypeLabels: { |
There was a problem hiding this comment.
The escapejs filter was removed from the query parameter initialization in the Alpine.js data object. This could lead to JavaScript errors or potential XSS vulnerabilities if the query parameter contains quotes or other special characters. The escapejs filter should be restored to properly escape JavaScript strings.
| <div x-data="{ sort: '{{ current_sort }}', type: '{{ request.GET.type|default:'all' }}', status: '{{ current_status }}', query: '{{ request.GET.q|default:'' }}', mediaTypeLabels: { | |
| <div x-data="{ sort: '{{ current_sort|escapejs }}', type: '{{ request.GET.type|default:'all'|escapejs }}', status: '{{ current_status|escapejs }}', query: '{{ request.GET.q|default:''|escapejs }}', mediaTypeLabels: { |
|
Hey, thanks for the PR! I appreciate your work on this. GitHub Copilot auto-review got triggered somehow (not sure why that happened), and it flagged a few minor issues. The suggestions look valid, but since this PR has been open for a while and the core functionality is solid, I'm going to merge it now and handle those fixes in a follow-up commit. Thanks again for the contribution! |
|
@tbmedia could you create a new issue for that feature request? |
Without it this currently results in an error at startup on a fresh
checkout, e.g. when just doing `docker compose up`:
yamtrack | CommandError: Conflicting migrations detected; multiple leaf nodes in the migration graph: (0038_user_list_detail_status_alter_user_home_sort_and_more, 0039_user_clickable_media_cards in users).
yamtrack | To fix them run 'python manage.py makemigrations --merge'
yamtrack exited with code 1 (restarting)
`0038_user_list_detail_status_alter_user_home_sort_and_more` was added in FuzzyGrim#928,
but by the time it was merged another migration had already been added as well.
|
Added in |
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [FuzzyGrim/Yamtrack](https://github.com/FuzzyGrim/Yamtrack) | minor | `0.24.11` → `0.25.0` | | [ghcr.io/fuzzygrim/yamtrack](https://github.com/FuzzyGrim/Yamtrack) | minor | `0.24.11` → `0.25.0` | --- ### Release Notes <details> <summary>FuzzyGrim/Yamtrack (FuzzyGrim/Yamtrack)</summary> ### [`v0.25.0`](https://github.com/FuzzyGrim/Yamtrack/releases/tag/v0.25.0) [Compare Source](FuzzyGrim/Yamtrack@v0.24.11...v0.25.0) ##### Features - Added support for the official Jellyfin Webhook plugin [@​Oridjinn1980](https://github.com/Oridjinn1980) in [#​907](FuzzyGrim/Yamtrack#907) - Added the option to filter lists by status by [@​doluk](https://github.com/doluk) in [#​928](FuzzyGrim/Yamtrack#928) - Added external links (imdb, tvdb, wikidata) to media details page [#​937](FuzzyGrim/Yamtrack#937) ([`38673ca`](FuzzyGrim/Yamtrack@38673ca)) - Added new option to select default date when bulk completing tv shows/seasons [#​802](FuzzyGrim/Yamtrack#802) ([`d2df3cd`](FuzzyGrim/Yamtrack@d2df3cd)) - Added user-customizable date and time display formats across the application [#​624](FuzzyGrim/Yamtrack#624) ([`070cfc8`](FuzzyGrim/Yamtrack@070cfc8)) - Added Board Game tracking with BoardGameGeek integration by [@​zskemp](https://github.com/zskemp) in [#​979](FuzzyGrim/Yamtrack#979) - Allow importing TV seasons/episodes by title only from Yamtrack CSV format by [@​dpantel](https://github.com/dpantel) in [#​968](FuzzyGrim/Yamtrack#968) - Display movie collections on media detail page by [@​andrebk](https://github.com/andrebk) in [#​1003](FuzzyGrim/Yamtrack#1003) - Added `CELERY_REDIS_URL` to allow configuring celery redis url independently from django [#​1123](FuzzyGrim/Yamtrack#1123) ([`ed20461`](FuzzyGrim/Yamtrack@ed20461)) - Added progress bar on media cards by [@​busliggabor](https://github.com/busliggabor) in [#​1130](FuzzyGrim/Yamtrack#1130) ##### Fixes - Fixed comic events not showing issue number ([`9f71132`](FuzzyGrim/Yamtrack@9f71132)) - Fixed some log entries getting incorrectly labeled as error log [#​1056](FuzzyGrim/Yamtrack#1056) ([`a47bf3d`](FuzzyGrim/Yamtrack@a47bf3d)) - Fixed season episodes notifications not being sent when tv is enabled but season disabled [#​1057](FuzzyGrim/Yamtrack#1057) ([`9947cbe`](FuzzyGrim/Yamtrack@9947cbe)) - Fixed docker secrets file parsing [#​789](FuzzyGrim/Yamtrack#789) ([`495de72`](FuzzyGrim/Yamtrack@495de72)) - Fixed rating style by [@​busliggabor](https://github.com/busliggabor) in [#​1086](FuzzyGrim/Yamtrack#1086) - Improved release dates metadata for Hardcover books [#​966](FuzzyGrim/Yamtrack#966) [`bb083ef`](FuzzyGrim/Yamtrack@bb083ef) - Fixed end date gets auto filled to current datetime when setting progress to maximum [#​1091](FuzzyGrim/Yamtrack#1091) ([`9765be7`](FuzzyGrim/Yamtrack@9765be7)) - Fixed wrong total anime episodes when AniList episode data is wrong compared to MyAnimeList [#​1096](FuzzyGrim/Yamtrack#1096) ([`c43d712`](FuzzyGrim/Yamtrack@c43d712)) - Fixed incorrect upcoming episode time on Home Page depending on time [#​1100](FuzzyGrim/Yamtrack#1100) ([`974d711`](FuzzyGrim/Yamtrack@974d711)) - Fixed can't create users with admin page [#​1147](FuzzyGrim/Yamtrack#1147) ([`11d9649`](FuzzyGrim/Yamtrack@11d9649)) ##### Maintenance - build(deps-dev): bump coverage from 7.13.0 to 7.13.1 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1064](FuzzyGrim/Yamtrack#1064) - build(deps): bump django-widget-tweaks from 1.5.0 to 1.5.1 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1075](FuzzyGrim/Yamtrack#1075) - build(deps): bump aiohttp from 3.13.2 to 3.13.3 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1076](FuzzyGrim/Yamtrack#1076) - build(deps): bump celery from 5.6.0 to 5.6.2 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1077](FuzzyGrim/Yamtrack#1077) - build(deps): bump pillow from 12.0.0 to 12.1.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1078](FuzzyGrim/Yamtrack#1078) - build(deps): bump requests-ratelimiter from 0.7.0 to 0.8.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1079](FuzzyGrim/Yamtrack#1079) - build(deps): bump django-select2 from 8.4.7 to 8.4.8 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1085](FuzzyGrim/Yamtrack#1085) - build(deps-dev): bump ruff from 0.14.10 to 0.14.13 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1107](FuzzyGrim/Yamtrack#1107) - build(deps): bump django-allauth\[socialaccount] from 65.13.1 to 65.14.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1116](FuzzyGrim/Yamtrack#1116) - build(deps): bump django from 5.2.9 to 5.2.11 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1153](FuzzyGrim/Yamtrack#1153) - build(deps-dev): bump fakeredis from 2.32.1 to 2.33.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1156](FuzzyGrim/Yamtrack#1156) - build(deps): bump apprise from 1.9.6 to 1.9.7 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1157](FuzzyGrim/Yamtrack#1157) - build(deps-dev): bump coverage from 7.13.1 to 7.13.3 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1159](FuzzyGrim/Yamtrack#1159) - build(deps): bump gunicorn from 23.0.0 to 25.0.1 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1160](FuzzyGrim/Yamtrack#1160) - build(deps): bump django-debug-toolbar from 6.1.0 to 6.2.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1161](FuzzyGrim/Yamtrack#1161) - build(deps): bump django-health-check from 3.20.8 to 3.23.3 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1162](FuzzyGrim/Yamtrack#1162) ##### New Contributors - [@​Oridjinn1980](https://github.com/Oridjinn1980) made their first contribution in [#​907](FuzzyGrim/Yamtrack#907) - [@​doluk](https://github.com/doluk) made their first contribution in [#​928](FuzzyGrim/Yamtrack#928) - [@​zskemp](https://github.com/zskemp) made their first contribution in [#​979](FuzzyGrim/Yamtrack#979) - [@​dpantel](https://github.com/dpantel) made their first contribution in [#​968](FuzzyGrim/Yamtrack#968) **Full Changelog**: <FuzzyGrim/Yamtrack@v0.24.11...v0.25.0> </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4zLjYiLCJ1cGRhdGVkSW5WZXIiOiI0My4zLjYiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbImltYWdlIl19--> Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/3812 Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net> Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Thanks for the amazing project. I really enjoy to use it. In the lack of genre labels (like sci-fi for example covering multiple media types) I created a sci-fi list and just added my items to it. But then I run into the issue, that I would only want to see planned items on my sci-fi list....
This pr aims to add filtering by status to the users list similar to how the medialist has it as an option. This is my first time working with Django, so any feedback is appreciated and please excuse any beginner mistakes I made.
I added also a test for it. While working on the tests, I moved the duplicated Movie, etc creation into the setUp function. I just noticed, that I accidentally commited my change to the source of the test items. While someone could argue, that is better to test without the metadata api requests, I am happy to revert the change (from a clean branch, so it isn't in the commit history).
Copilot summary
This pull request introduces a new feature that allows users to filter items in a custom list by their status (e.g., Completed, In Progress, Planning, etc.) in the list detail view. The changes span backend logic, frontend UI, and database schema to support status-based filtering and user preference persistence.
Key changes include:
Backend: Status Filtering and Persistence
list_detail_statusfield to theUsermodel and database, with choices for all relevant statuses and a migration to enforce data integrity. This allows each user to persist their preferred status filter for list detail views. [1] [2] [3]list_detailview inlists/views.pyto accept astatusparameter, apply filtering by status across all media types, and pass the current status and available choices to the template context. [1] [2] [3] [4]Frontend: UI for Status Filtering
list_detail.html) to add a status filter dropdown alongside the media type filter, update the Alpine.js state, and ensure the status filter is included in form submissions and HTMX requests. [1] [2] [3]Testing: Test Refactoring and Coverage
test_views.pyto create media instances insetUpinstead of each test, and added/updated tests to verify status filtering logic and UI. [1] [2] [3] [4] [5] [6]These changes together provide a robust, user-friendly way to filter list items by status, and ensure the feature is well-tested and persists user preferences.
Backend: Status Filtering and Persistence
list_detail_statusfield to theUsermodel with choices and a migration, along with a database constraint to ensure valid values. [1] [2] [3]list_detailview to filter items by status, persist the user's choice, and pass status-related context to the template. [1] [2] [3] [4]Frontend: UI for Status Filtering
list_detail.html, updated Alpine.js state, and ensured filter state is included in form and HTMX requests. [1] [2] [3]Testing: Test Refactoring and Coverage