Skip to content

Book percentage progress#1245

Open
connorjburton wants to merge 6 commits intoFuzzyGrim:devfrom
connorjburton:book-percentage-progress
Open

Book percentage progress#1245
connorjburton wants to merge 6 commits intoFuzzyGrim:devfrom
connorjburton:book-percentage-progress

Conversation

@connorjburton
Copy link
Copy Markdown
Contributor

@connorjburton connorjburton commented Mar 10, 2026

Full disclosure, Gemini wrote all the code. I carefully planned and reviewed the work, making many changes to make it more idiomatic, fix bugs and keep it in-line with the existing code. I am not an expert in Python and I know there are some areas that I am not 100% happy with that I will highlight with inline comments.

This feature solves #1110 and #768

It adds two primary features

  1. A user preference to choose the default book progress unit
  2. The ability to save a book's progress in either unit, overriding the default

The reason I chose to do it this way is that if a user reads a mix of physical and digital book their preference will change. We shouldn't expect users to have to convert their page number to % every time they want to update their progress, so it's important to give them that flexibility.

By default the preference is pages, so no change to behaviour. Two migrations are added, one to add the two units, defaulting to pages, and the other two set all existing books to NULL as their progress unit (as the user has not specified what unit they want).

I chose to have an overloaded method on the book model to return the progress unit, and a media model method that is pretty much a no-op.

For the UI, I chose to put the toggle in the label line, but I can change this if you prefer something else, I looked over the app and could not find an existing pattern for this. Another nice feature here is that it will auto-convert the %/pages to the other unit when toggling, so that it's easy to swap between them for existing books. Progress bar and other places where progress is used like the details box all handle the % correctly.

See below screenshots/videos.

Screenshot_20260310_012517 Screenshot_20260310_012656 Screenshot_20260310_012827
Screencast_20260310_012729.webm

@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 book tracking feature by allowing users to choose how they record their reading progress, either by page count or by percentage completion. This change addresses the diverse ways users consume books, offering a personalized and intuitive experience. The update includes both backend model and form modifications, new database fields, and dynamic frontend elements to facilitate seamless unit switching and accurate progress representation.

Highlights

  • Flexible Book Progress Tracking: Introduced the ability for users to track book progress using either page numbers or percentages, providing greater flexibility based on reading habits.
  • User Preference for Default Unit: Added a user preference setting to choose the default progress unit (pages or percentage) for new books, with 'pages' as the default to maintain existing behavior.
  • Dynamic UI Toggle and Conversion: Implemented a UI toggle within the book tracking form that allows users to switch between page and percentage units, automatically converting the progress value between units.
  • Database Migrations: Included necessary database migrations to add progress_unit fields to both the Book and User models, ensuring data persistence for the new functionality.
  • Updated Progress Logic and Display: Adjusted the backend logic for processing and displaying book progress to correctly handle both page-based and percentage-based units across various parts of the application, including progress bars and detail views.
Changelog
  • src/app/forms.py
    • Imported the users module.
    • Added a can_toggle_unit class attribute to MediaForm and BookForm.
    • Modified MediaForm's __init__ method to accept and store a user argument.
    • Added a progress_unit ChoiceField to BookForm.
    • Updated BookForm's Meta class to include progress_unit in its fields.
    • Modified BookForm's __init__ to set the initial progress_unit based on the book instance or user preference, and dynamically update the progress field's label and max attribute.
  • src/app/migrations/0056_book_progress_unit_historicalbook_progress_unit.py
    • Added a new migration file.
    • Added progress_unit CharField to the Book model.
    • Added progress_unit CharField to the HistoricalBook model.
  • src/app/models.py
    • Added a get_progress_unit method to the Media model to retrieve the progress unit.
    • Refactored process_progress to handle percentage-based progress, capping it at 100% and marking as completed.
    • Updated process_status to set progress to 100% when completed if the unit is percentage, otherwise using max_progress.
    • Modified formatted_progress property in Media to conditionally include max_progress.
    • Added a progress_unit CharField to the Book model.
    • Overrode the formatted_progress property in Book to display percentage if applicable.
    • Overrode the get_progress_unit method in Book to fall back to user preference if no specific unit is set for the book.
  • src/app/tests/models/test_book.py
    • Added a new test file for the Book model.
    • Implemented test_get_progress_unit_fallback to verify unit fallback logic.
    • Implemented test_process_progress_percentage to test progress processing for percentage units.
    • Implemented test_formatted_progress_percentage to test formatted output for percentage units.
    • Implemented test_formatted_progress_pages to test formatted output for page units.
  • src/app/views.py
    • Updated track_modal to use get_media_prefetch and filter_media_prefetch for media retrieval.
    • Modified track_modal to pass the user object and max_progress to the form initialization and context.
    • Modified media_save to pass the user object to the form initialization.
  • src/static/js/mediaStatusDateHandler.js
    • Added new JavaScript variables to access progressField, progressUnitField, and mediaTypeField.
    • Implemented toggleProgressUnit function to switch between 'pages' and 'percentage' units.
    • Added logic within toggleProgressUnit to auto-convert progress values and update the input field's max attribute and the label text.
  • src/templates/app/components/fill_track.html
    • Added data-max-progress attribute to the form element.
    • Included form.progress_unit field in the template.
    • Modified the progress field's label to include a 'Toggle % / Pages' button, visible only for books with can_toggle_unit and max_progress.
  • src/templates/app/components/media_card.html
    • Removed the explicit display of / {{ media.max_progress }} as it is now handled within formatted_progress.
  • src/templates/app/components/media_table_items.html
    • Removed the explicit display of / {{ media.max_progress }} as it is now handled within formatted_progress.
  • src/templates/app/components/progress_bar.html
    • Updated the progress bar logic to render based on percentage if get_progress_unit returns 'percentage', otherwise using the width ratio based on max_progress.
  • src/templates/app/media_details.html
    • Removed explicit display of / {{ current_instance.max_progress }} and unit pluralization, relying on formatted_progress.
  • src/templates/users/preferences.html
    • Added a new section in user preferences for 'Default Book Progress Unit'.
    • Included a select element to allow users to choose between 'Pages' and 'Percentage' as their default book progress unit.
  • src/users/migrations/0051_user_book_progress_unit_and_more.py
    • Added a new migration file.
    • Added book_progress_unit CharField to the User model with choices for 'pages' and 'percentage' and a default of 'pages'.
    • Added a CheckConstraint named book_progress_unit_valid to ensure valid choices for book_progress_unit.
  • src/users/models.py
    • Defined a new ProgressUnit TextChoices class with 'PAGES' and 'PERCENTAGE' options.
    • Added a book_progress_unit CharField to the User model, using ProgressUnit choices and defaulting to 'PAGES'.
    • Added a CheckConstraint to the User model's Meta class to validate book_progress_unit values.
  • src/users/views.py
    • Imported ProgressUnit from users.models.
    • Modified the preferences view to pass ProgressUnit.choices to the template context.
    • Updated the preferences view to save the book_progress_unit from POST data to the user's preferences.
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.

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 new feature allowing users to track book progress using either pages or percentages, with a configurable default unit in user preferences. The changes include adding progress_unit fields to Book and User models, updating MediaForm and BookForm to manage this new unit, and modifying Media model methods to correctly calculate and display progress. Frontend updates provide a dynamic 'Toggle % / Pages' button in the tracking form and adjust progress bar rendering. Additionally, performance was improved by using prefetch methods for media retrieval. The review suggests refactoring duplicated completion logic in the process_progress method for better maintainability and extracting the hardcoded 'Pages' unit name in the JavaScript to allow for future reusability with other media types.

Comment on lines 849 to 871
if self.get_progress_unit() == users.models.ProgressUnit.PERCENTAGE:
self.progress = min(self.progress, 100)
if self.progress == 100:
self.status = Status.COMPLETED.value
now = timezone.now().replace(second=0, microsecond=0)
self.end_date = now
return

max_progress = providers.services.get_media_metadata(
self.item.media_type,
self.item.media_id,
self.item.source,
)["max_progress"]

if self.progress == max_progress:
self.status = Status.COMPLETED.value
if max_progress:
self.progress = min(self.progress, max_progress)

now = timezone.now().replace(second=0, microsecond=0)
self.end_date = now
if self.progress == max_progress:
self.status = Status.COMPLETED.value

now = timezone.now().replace(second=0, microsecond=0)
self.end_date = now

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

This method contains duplicated logic for marking an item as completed. To improve maintainability and adhere to the DRY principle, you could extract the completion logic into a local function.

        def mark_completed():
            self.status = Status.COMPLETED.value
            now = timezone.now().replace(second=0, microsecond=0)
            self.end_date = now

        if self.get_progress_unit() == users.models.ProgressUnit.PERCENTAGE:
            self.progress = min(self.progress, 100)
            if self.progress == 100:
                mark_completed()
            return

        max_progress = providers.services.get_media_metadata(
            self.item.media_type,
            self.item.media_id,
            self.item.source,
        )["max_progress"]

        if max_progress:
            self.progress = min(self.progress, max_progress)
            if self.progress == max_progress:
                mark_completed()

// Update label suffix via custom event or direct DOM manipulation
const label = this.$el.querySelector(`label[for="${progressField.id}"]`);
if (label) {
label.textContent = newUnit === 'percentage' ? 'Progress (%)' : `Progress (Pages)`;
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 unit name 'Pages' is hardcoded here. While this works for books, it would be more maintainable and reusable for other media types in the future if this was not hardcoded. You could pass the unit name from the template to the javascript via a data- attribute on the form element.

For example, in fill_track.html:

<form ... data-unit-name-plural="{{ media_type|long_unit }}s">

Then in the Javascript:

const unitNamePlural = this.$el.dataset.unitNamePlural || 'Pages';
label.textContent = newUnit === 'percentage' ? 'Progress (%)' : `Progress (${unitNamePlural})`;

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 10, 2026

Codecov Report

❌ Patch coverage is 92.50000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.39%. Comparing base (d1eed95) to head (3abfb88).

Files with missing lines Patch % Lines
src/app/forms.py 76.19% 5 Missing ⚠️
src/app/views.py 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #1245      +/-   ##
==========================================
+ Coverage   81.30%   81.39%   +0.09%     
==========================================
  Files          70       70              
  Lines        7327     7386      +59     
==========================================
+ Hits         5957     6012      +55     
- Misses       1370     1374       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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