Skip to content

Add support for bulk adding items to a list#1227

Open
olioby wants to merge 6 commits intoFuzzyGrim:devfrom
olioby:bulk-list-edit
Open

Add support for bulk adding items to a list#1227
olioby wants to merge 6 commits intoFuzzyGrim:devfrom
olioby:bulk-list-edit

Conversation

@olioby
Copy link
Copy Markdown

@olioby olioby commented Mar 1, 2026

Hey, I took a stab at adding support for adding multiple media items to a custom list in one go. This was a big pain point for me and something that makes it very hard to migrate from other services like imdb or goodreads.

Let me know what you think of the approach - I'm not very well versed in Django/htmx so I'd appreciate a critical look.

Thanks @FuzzyGrim for all your work on this project!


Screenshots

image
image
image
image

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the user experience by introducing a bulk selection and addition mechanism for media items to custom lists. This feature addresses a common pain point for users migrating data from other services, allowing them to efficiently organize large collections of media. The changes involve both frontend interactivity using Alpine.js and HTMX, and new backend logic to process bulk operations securely and efficiently.

Highlights

  • Bulk Item Selection: Introduced the ability to select multiple media items from the media list view using a new 'Select' toggle button. Selected items are visually highlighted, and checkboxes appear on each media card/table row for individual selection.
  • Bulk Add to List Functionality: Implemented a new feature allowing users to add all currently selected media items to an existing custom list in a single action. This is facilitated by a bulk action bar that appears when items are selected, leading to a modal for list selection.
  • New Backend Endpoints: Added two new Django URL routes and corresponding view functions (bulk_lists_modal and bulk_list_add) to handle fetching available lists for bulk adding and processing the bulk addition request, respectively.
  • Alpine.js State Management: Utilized Alpine.js to manage the frontend state for bulk selection, including tracking active bulk mode, selected item IDs, and providing functions to toggle selection, select all visible items, and clear selections.
  • User Interface Enhancements: Updated media card and table item templates to include visual indicators and interactive elements for bulk selection. A sticky bulk action bar and a modal for choosing target lists were also added to the media list page.
Changelog
  • src/lists/urls.py
    • Added new URL pattern bulk_lists_modal for displaying the modal to select lists for bulk adding.
    • Added new URL pattern bulk_list_add for processing the bulk addition of items to a selected list.
  • src/lists/views.py
    • Implemented bulk_lists_modal view function to retrieve and render a list of custom lists accessible by the user for bulk operations.
    • Implemented bulk_list_add view function to handle POST requests for adding multiple item_ids to a specified custom_list_id, including permission checks and conflict handling.
  • src/templates/app/components/media_card.html
    • Added data-bulk-id attribute to the media card div for Alpine.js integration.
    • Applied dynamic CSS class binding to visually indicate selected items in bulk mode.
    • Introduced a bulk select overlay with a checkbox that appears when bulk mode is active, allowing users to select/deselect items.
  • src/templates/app/components/media_table_items.html
    • Added data-bulk-id attribute to table rows for Alpine.js integration.
    • Applied dynamic CSS class binding to visually indicate selected table rows in bulk mode.
    • Added a new table data cell (<td>) containing a checkbox for bulk selection, visible only when bulk mode is active.
  • src/templates/app/media_list.html
    • Initialized an Alpine.js store named bulk to manage the state of bulk selection (active status, selected IDs, toggle, select all, clear functions).
    • Added a 'Select' toggle button to activate/deactivate bulk selection mode, clearing selections when deactivated.
    • Included a hidden form (bulk-ids-form) to dynamically hold selected item IDs and the CSRF token for HTMX requests.
    • Implemented a sticky bulk action bar that displays the count of selected items and provides 'Select all visible', 'Add to list', and 'Cancel' buttons.
    • Created a modal structure (bulkListsOpen) to display the list selection interface for bulk adding, triggered by the 'Add to list' button.
  • src/templates/lists/components/bulk_fill_lists.html
    • Added a new template for the bulk lists modal, displaying a list of custom lists with 'Add' buttons.
    • Integrated HTMX attributes for posting selected item IDs to the bulk_list_add endpoint.
  • src/templates/lists/components/bulk_fill_lists_success.html
    • Added a new template to display a success message after items are bulk added to a list, automatically closing the modal and clearing selections after a short delay.
Activity
  • The pull request was opened as a draft by olioby, indicating ongoing work and a request for early feedback.
  • The author explicitly asked for a critical look at the Django/HTMX approach, acknowledging their less familiarity with these technologies.
  • The author thanked @FuzzyGrim for their work on the project, suggesting a collaborative environment.
  • The pull request includes multiple image attachments, providing visual demonstrations of the new bulk selection and add-to-list functionality.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@olioby olioby changed the title [Draft] Add support for bulk adding items to a list [Draft] Add support for bulk adding items to a list, and deleting items from a list Mar 1, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a feature to bulk-add media items to a custom list, utilizing Alpine.js for frontend state management and htmx for server communication. However, a reflected Cross-Site Scripting (XSS) vulnerability was identified in the media_list.html template where URL parameters are rendered directly into an Alpine.js attribute without proper JavaScript escaping. Additionally, suggestions have been made to improve the security and efficiency of the backend view handling item additions, and to optimize a frontend JavaScript function for better performance.

Comment thread src/lists/views.py Outdated
Comment on lines +342 to +356
custom_list_id = request.POST["custom_list_id"]

custom_list = get_object_or_404(CustomList, id=custom_list_id)

if custom_list.user_can_edit(request.user):
CustomListItem.objects.bulk_create(
[CustomListItem(custom_list=custom_list, item_id=i) for i in item_ids],
ignore_conflicts=True,
)
logger.info("%d items bulk added to %s.", len(item_ids), custom_list)

return render(request, "lists/components/bulk_fill_lists_success.html")

messages.error(request, "You do not have permission to edit this list.")
return helpers.redirect_back(request)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation for fetching the CustomList and checking for edit permissions can be improved for security and efficiency.

  1. Accessing request.POST["custom_list_id"] directly will raise a MultiValueDictKeyError if the key is missing. It's safer to use request.POST.get() and handle the case where it's not provided.
  2. Fetching the object and then checking for permissions separately can leak information about the existence of a list a user doesn't have access to. It's better to incorporate the permission check into the get_object_or_404 query. This also makes the view consistent with list_item_toggle.

Here's a suggested refactoring that addresses both points and simplifies the code by removing the if/else block for permissions.

Suggested change
custom_list_id = request.POST["custom_list_id"]
custom_list = get_object_or_404(CustomList, id=custom_list_id)
if custom_list.user_can_edit(request.user):
CustomListItem.objects.bulk_create(
[CustomListItem(custom_list=custom_list, item_id=i) for i in item_ids],
ignore_conflicts=True,
)
logger.info("%d items bulk added to %s.", len(item_ids), custom_list)
return render(request, "lists/components/bulk_fill_lists_success.html")
messages.error(request, "You do not have permission to edit this list.")
return helpers.redirect_back(request)
custom_list_id = request.POST.get("custom_list_id")
if not custom_list_id:
messages.error(request, "No list was selected.")
return helpers.redirect_back(request)
custom_list = get_object_or_404(
CustomList.objects.filter(
Q(owner=request.user) | Q(collaborators=request.user), id=custom_list_id
).distinct()
)
CustomListItem.objects.bulk_create(
[CustomListItem(custom_list=custom_list, item_id=i) for i in item_ids],
ignore_conflicts=True,
)
logger.info("%d items bulk added to %s.", len(item_ids), custom_list)
return render(request, "lists/components/bulk_fill_lists_success.html")

});
</script>

<div x-data="{ sort: '{{ current_sort }}', status: '{{ current_status }}', layout: '{{ current_layout }}', search: '{{ request.GET.search|default:'' }}', bulkListsOpen: false, statusLabels: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The search parameter from the URL, as well as current_sort, current_status, and current_layout, are rendered directly into an Alpine.js x-data attribute. Because these values are enclosed in single quotes and not properly escaped for a JavaScript context, an attacker can provide a malicious value (e.g., in the search query parameter) containing single quotes to break out of the string and execute arbitrary JavaScript (Reflected XSS).

Comment on lines +21 to +26
selectAll() {
document.querySelectorAll('[data-bulk-id]').forEach(function(el) {
const id = parseInt(el.dataset.bulkId);
if (Alpine.store('bulk').ids.indexOf(id) === -1) Alpine.store('bulk').ids.push(id);
});
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The selectAll function can be implemented more efficiently. The current implementation iterates over all visible items and checks for existence in the ids array one by one, which can be slow if there are many items on the page or many items are already selected.

A more performant approach would be to get all visible IDs, merge them with the already selected IDs using a Set to handle duplicates, and then update the ids array.

        selectAll() {
          const allVisibleIds = Array.from(document.querySelectorAll('[data-bulk-id]'), el => parseInt(el.dataset.bulkId));
          this.ids = Array.from(new Set([...this.ids, ...allVisibleIds]));
        },

@olioby olioby changed the title [Draft] Add support for bulk adding items to a list, and deleting items from a list [Draft] Add support for bulk adding items to a list Mar 1, 2026
@olioby olioby changed the title [Draft] Add support for bulk adding items to a list Add support for bulk adding items to a list Mar 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant