From 97f61693efc5f1a119bd26d3f64873f090967b94 Mon Sep 17 00:00:00 2001 From: Connor Burton Date: Tue, 10 Mar 2026 01:06:13 +0000 Subject: [PATCH 1/6] add migration for default book progress and set existing books progress_unit to null --- ...gress_unit_historicalbook_progress_unit.py | 23 ++++++++++++++++++ .../0051_user_book_progress_unit_and_more.py | 24 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/app/migrations/0056_book_progress_unit_historicalbook_progress_unit.py create mode 100644 src/users/migrations/0051_user_book_progress_unit_and_more.py diff --git a/src/app/migrations/0056_book_progress_unit_historicalbook_progress_unit.py b/src/app/migrations/0056_book_progress_unit_historicalbook_progress_unit.py new file mode 100644 index 000000000..9eb5e38bc --- /dev/null +++ b/src/app/migrations/0056_book_progress_unit_historicalbook_progress_unit.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.11 on 2026-03-10 00:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0055_alter_item_media_type'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='progress_unit', + field=models.CharField(blank=True, default=None, max_length=20, null=True), + ), + migrations.AddField( + model_name='historicalbook', + name='progress_unit', + field=models.CharField(blank=True, default=None, max_length=20, null=True), + ), + ] diff --git a/src/users/migrations/0051_user_book_progress_unit_and_more.py b/src/users/migrations/0051_user_book_progress_unit_and_more.py new file mode 100644 index 000000000..d48f18a29 --- /dev/null +++ b/src/users/migrations/0051_user_book_progress_unit_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.11 on 2026-03-10 00:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0056_book_progress_unit_historicalbook_progress_unit'), + ('auth', '0012_alter_user_first_name_max_length'), + ('users', '0050_user_watch_provider_region'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='book_progress_unit', + field=models.CharField(choices=[('pages', 'Pages'), ('percentage', 'Percentage')], default='pages', max_length=20), + ), + migrations.AddConstraint( + model_name='user', + constraint=models.CheckConstraint(condition=models.Q(('book_progress_unit__in', ['pages', 'percentage'])), name='book_progress_unit_valid'), + ), + ] From 4f78873400db1013f24e5bf588b4afdd8e4510e9 Mon Sep 17 00:00:00 2001 From: Connor Burton Date: Tue, 10 Mar 2026 01:09:08 +0000 Subject: [PATCH 2/6] preferences option for default book progress --- src/templates/users/preferences.html | 22 ++++++++++++++++++++++ src/users/models.py | 16 ++++++++++++++++ src/users/views.py | 12 +++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/templates/users/preferences.html b/src/templates/users/preferences.html index 82fe1ded6..65bf13ecf 100644 --- a/src/templates/users/preferences.html +++ b/src/templates/users/preferences.html @@ -191,6 +191,28 @@

Preferences

+ {# Default Book Progress Unit #} +
+
+
+
+ {% include "app/icons/book-open.svg" with classes="w-5 h-5 mr-2" %} + Default Book Progress Unit +
+

+ Choose the default unit for tracking book progress. +

+
+ +
+
+
{# Media Types Settings #} diff --git a/src/users/models.py b/src/users/models.py index cd18e007b..32e6e6414 100644 --- a/src/users/models.py +++ b/src/users/models.py @@ -90,6 +90,13 @@ class QuickWatchDateChoices(models.TextChoices): NO_DATE = "no_date", "No Date" +class ProgressUnit(models.TextChoices): + """Choices for progress measurement units.""" + + PAGES = "pages", "Pages" + PERCENTAGE = "percentage", "Percentage" + + class DateFormatChoices(models.TextChoices): """Choices for date format display.""" @@ -248,6 +255,11 @@ class User(AbstractUser): default=MediaStatusChoices.ALL, choices=MediaStatusChoices, ) + book_progress_unit = models.CharField( + max_length=20, + default=ProgressUnit.PAGES, + choices=ProgressUnit, + ) # Media type preferences: Comics comic_enabled = models.BooleanField(default=True) @@ -462,6 +474,10 @@ class Meta: name="book_sort_valid", condition=models.Q(book_sort__in=MediaSortChoices.values), ), + models.CheckConstraint( + name="book_progress_unit_valid", + condition=models.Q(book_progress_unit__in=ProgressUnit.values), + ), models.CheckConstraint( name="calendar_layout_valid", condition=models.Q(calendar_layout__in=CalendarLayoutChoices.values), diff --git a/src/users/views.py b/src/users/views.py index c3e45805b..55f2040f2 100644 --- a/src/users/views.py +++ b/src/users/views.py @@ -15,7 +15,12 @@ from app.models import Item, MediaTypes from app.providers import tmdb from users.forms import NotificationSettingsForm, PasswordChangeForm, UserUpdateForm -from users.models import DateFormatChoices, QuickWatchDateChoices, TimeFormatChoices +from users.models import ( + DateFormatChoices, + ProgressUnit, + QuickWatchDateChoices, + TimeFormatChoices, +) logger = logging.getLogger(__name__) @@ -226,6 +231,7 @@ def preferences(request): "date_format_choices": DateFormatChoices.choices, "time_format_choices": TimeFormatChoices.choices, "watch_provider_choices": watch_provider_regions, + "progress_unit_choices": ProgressUnit.choices, }, ) @@ -253,6 +259,10 @@ def preferences(request): "time_format", TimeFormatChoices.HOUR_24, ) + request.user.book_progress_unit = request.POST.get( + "book_progress_unit", + ProgressUnit.PAGES, + ) media_types_checked = request.POST.getlist("media_types_checkboxes") provider_region = request.POST.get("watch_provider_region", "") From 6f37cfb97af8a4cde5f48ccc88470064b491ea66 Mon Sep 17 00:00:00 2001 From: Connor Burton Date: Tue, 10 Mar 2026 01:16:45 +0000 Subject: [PATCH 3/6] add support for percentage on media modals, including converting between pages/% --- src/app/forms.py | 37 +++++++++ src/app/models.py | 84 +++++++++++++++----- src/app/views.py | 23 ++++-- src/static/js/mediaStatusDateHandler.js | 38 +++++++++ src/templates/app/components/fill_track.html | 18 ++++- 5 files changed, 169 insertions(+), 31 deletions(-) diff --git a/src/app/forms.py b/src/app/forms.py index 77ef4f06f..bcb70ba28 100644 --- a/src/app/forms.py +++ b/src/app/forms.py @@ -1,6 +1,7 @@ from django import forms from django.conf import settings +import users from app import config from app.models import ( TV, @@ -194,11 +195,17 @@ def save(self, commit=True): # noqa: FBT002 class MediaForm(forms.ModelForm): """Base form for all media types.""" + can_toggle_unit = False instance_id = forms.CharField(widget=forms.HiddenInput(), required=False) media_type = forms.CharField(widget=forms.HiddenInput(), required=True) source = forms.CharField(widget=forms.HiddenInput(), required=True) media_id = forms.CharField(widget=forms.HiddenInput(), required=True) + def __init__(self, *args, **kwargs): + """Initialize the form.""" + self.user = kwargs.pop("user", None) + super().__init__(*args, **kwargs) + class Meta: """Define fields and input types.""" @@ -284,16 +291,46 @@ class Meta(MediaForm.Meta): class BookForm(MediaForm): """Form for books.""" + can_toggle_unit = True + progress_unit = forms.ChoiceField( + choices=users.models.ProgressUnit.choices, + widget=forms.HiddenInput(), + required=False, + ) + class Meta(MediaForm.Meta): """Bind form to model.""" model = Book + fields = MediaForm.Meta.fields + ["progress_unit"] labels = { "progress": ( f"Progress ({config.get_unit(MediaTypes.BOOK.value, short=False)}s)" ), } + def __init__(self, *args, **kwargs): + """Initialize the form and set progress unit.""" + super().__init__(*args, **kwargs) + + # Set initial progress unit + if self.instance and self.instance.pk: + unit = self.instance.get_progress_unit() + self.initial["progress_unit"] = unit + else: + # For new items, use user preference if available + user = getattr(self, "user", None) + if user: + self.initial["progress_unit"] = user.book_progress_unit + else: + self.initial["progress_unit"] = users.models.ProgressUnit.PAGES + + # Update label based on unit + current_unit = self.initial.get("progress_unit") + if current_unit == users.models.ProgressUnit.PERCENTAGE: + self.fields["progress"].label = "Progress (%)" + self.fields["progress"].widget.attrs["max"] = 100 + class ComicForm(MediaForm): """Form for comics.""" diff --git a/src/app/models.py b/src/app/models.py index 444e228e0..21f6922fd 100644 --- a/src/app/models.py +++ b/src/app/models.py @@ -823,6 +823,11 @@ def __str__(self): """Return the title of the media.""" return self.item.__str__() + def get_progress_unit(self): + """Return the progress unit for the media.""" + # Check if instance has a specific progress_unit field (like Book) + return getattr(self, "progress_unit", None) + def save(self, *args, **kwargs): """Save the media instance.""" if self.tracker.has_changed("progress"): @@ -837,33 +842,47 @@ def process_progress(self): """Update fields depending on the progress of the media.""" if self.progress < 0: self.progress = 0 - elif self.status == Status.IN_PROGRESS.value: - 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.status != Status.IN_PROGRESS.value: + return + + 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 def process_status(self): """Update fields depending on the status of the media.""" if self.status == Status.COMPLETED.value: - max_progress = providers.services.get_media_metadata( - self.item.media_type, - self.item.media_id, - self.item.source, - )["max_progress"] + if self.get_progress_unit() == users.models.ProgressUnit.PERCENTAGE: + self.progress = 100 + else: + 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 = max_progress + if max_progress: + self.progress = max_progress self.item.fetch_releases(delay=True) @@ -881,7 +900,11 @@ def formatted_score(self): @property def formatted_progress(self): """Return the progress of the media in a formatted string.""" - return str(self.progress) + display = str(self.progress) + max_progress = getattr(self, "max_progress", None) + if max_progress and self.item.media_type != MediaTypes.MOVIE.value: + display += f" / {max_progress}" + return display def increase_progress(self): """Increase the progress of the media by one.""" @@ -1613,6 +1636,27 @@ class Book(Media): tracker = FieldTracker() + progress_unit = models.CharField( + max_length=20, + null=True, + blank=True, + default=None, + ) + + @property + def formatted_progress(self): + """Return the progress of the media in a formatted string.""" + if self.get_progress_unit() == users.models.ProgressUnit.PERCENTAGE: + return f"{self.progress}%" + return super().formatted_progress + + def get_progress_unit(self): + """Return the progress unit for the book, falling back to user preference.""" + unit = super().get_progress_unit() + if unit: + return unit + return self.user.book_progress_unit + class Comic(Media): """Model for comics.""" diff --git a/src/app/views.py b/src/app/views.py index 6a3779de1..3416253ef 100644 --- a/src/app/views.py +++ b/src/app/views.py @@ -438,7 +438,7 @@ def track_modal( """Return the tracking form for a media item.""" instance_id = request.GET.get("instance_id") if instance_id: - media = BasicMedia.objects.get_media( + media = BasicMedia.objects.get_media_prefetch( request.user, media_type, instance_id, @@ -447,14 +447,14 @@ def track_modal( media = None else: # no specific instance, try to find the first one - user_medias = BasicMedia.objects.filter_media( + user_medias = BasicMedia.objects.filter_media_prefetch( request.user, media_id, media_type, source, season_number=season_number, ) - media = user_medias.first() + media = user_medias[0] if user_medias else None if media: instance_id = media.id @@ -470,17 +470,24 @@ def track_modal( title = media.item if media_type == MediaTypes.GAME.value: initial_data["progress"] = helpers.minutes_to_hhmm(media.progress) + max_progress = getattr(media, "max_progress", None) else: - title = services.get_media_metadata( + metadata = services.get_media_metadata( media_type, media_id, source, [season_number], - )["title"] + ) + title = metadata["title"] + max_progress = metadata.get("max_progress") if media_type == MediaTypes.SEASON.value: title += f" S{season_number}" - form = get_form_class(media_type)(instance=media, initial=initial_data) + form = get_form_class(media_type)( + instance=media, + initial=initial_data, + user=request.user, + ) return render( request, @@ -489,6 +496,8 @@ def track_modal( "title": title, "form": form, "media": media, + "media_type": media_type, + "max_progress": max_progress, "return_url": request.GET["return_url"], }, ) @@ -531,7 +540,7 @@ def media_save(request): # Validate the form and save the instance if it's valid form_class = get_form_class(media_type) - form = form_class(request.POST, instance=instance) + form = form_class(request.POST, instance=instance, user=request.user) if form.is_valid(): form.save() logger.info("%s saved successfully.", form.instance) diff --git a/src/static/js/mediaStatusDateHandler.js b/src/static/js/mediaStatusDateHandler.js index a316adf98..31edccd2d 100644 --- a/src/static/js/mediaStatusDateHandler.js +++ b/src/static/js/mediaStatusDateHandler.js @@ -16,6 +16,9 @@ document.addEventListener("alpine:init", () => { const endDateField = this.$el.querySelector('[name="end_date"]'); const startDateField = this.$el.querySelector('[name="start_date"]'); const instanceIdField = this.$el.querySelector('[name="instance_id"]'); + const progressField = this.$el.querySelector('[name="progress"]'); + const progressUnitField = this.$el.querySelector('[name="progress_unit"]'); + const mediaTypeField = this.$el.querySelector('[name="media_type"]'); // Check if this is a new form (no instance_id) vs editing existing record const isNewForm = !instanceIdField || !instanceIdField.value; @@ -27,6 +30,41 @@ document.addEventListener("alpine:init", () => { this.original.end_date = endDateField?.value || null; } + // Progress unit toggle logic + if (progressUnitField && progressField) { + this.progress_unit = progressUnitField.value; + const maxProgress = parseInt(this.$el.dataset.maxProgress) || 0; + + this.toggleProgressUnit = () => { + const oldUnit = this.progress_unit; + const newUnit = oldUnit === 'pages' ? 'percentage' : 'pages'; + const currentValue = parseInt(progressField.value) || 0; + + if (maxProgress > 0) { + let newValue; + if (newUnit === 'percentage') { + // pages -> percentage + newValue = Math.round((currentValue / maxProgress) * 100); + progressField.max = 100; + } else { + // percentage -> pages + newValue = Math.round((currentValue / 100) * maxProgress); + progressField.max = maxProgress; + } + progressField.value = Number.isNaN(newValue) ? 0 : newValue; + } + + this.progress_unit = newUnit; + progressUnitField.value = newUnit; + + // 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)`; + } + }; + } + // Get the current time in correct format based on input type const now = this.getCurrentDateTime(endDateField); diff --git a/src/templates/app/components/fill_track.html b/src/templates/app/components/fill_track.html index f8360bedf..d2162f322 100644 --- a/src/templates/app/components/fill_track.html +++ b/src/templates/app/components/fill_track.html @@ -7,22 +7,32 @@

{{ title }}

@click="trackOpen = false">{% include "app/icons/x.svg" with classes="w-6 h-6" %} -
+ {% csrf_token %} {{ form.instance_id }} {{ form.media_type }} {{ form.source }} {{ form.media_id }} {{ form.season_number }} + {% if form.progress_unit %}{{ form.progress_unit }}{% endif %} {% with total_fields=form.fields|length %} {% if total_fields > 8 %}
{% endif %} {% for field in form %} - {% if field.name != 'notes' and field.name != 'instance_id' and field.name != 'media_type' and field.name != 'source' and field.name != 'media_id' and field.name != 'season_number' %} + {% if field.name != 'notes' and field.name != 'instance_id' and field.name != 'media_type' and field.name != 'source' and field.name != 'media_id' and field.name != 'season_number' and field.name != 'progress_unit' %}
- +
+ + {% if field.name == 'progress' and form.can_toggle_unit and max_progress %} + + {% endif %} +
{% if field.name == "status" %} {{ field|add_class:"w-full p-2 bg-[#39404b] rounded-md text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-[#4a9eff] appearance-none" }} From 48183414fcef4e2de0f9d475144cba4397a00007 Mon Sep 17 00:00:00 2001 From: Connor Burton Date: Tue, 10 Mar 2026 01:22:28 +0000 Subject: [PATCH 4/6] support displaying % or pages throughout the different views of data --- src/templates/app/components/media_card.html | 1 - src/templates/app/components/media_table_items.html | 1 - src/templates/app/components/progress_bar.html | 5 ++++- src/templates/app/media_details.html | 4 ---- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/templates/app/components/media_card.html b/src/templates/app/components/media_card.html index 2062dc606..d6e272384 100644 --- a/src/templates/app/components/media_card.html +++ b/src/templates/app/components/media_card.html @@ -57,7 +57,6 @@
{{ media.formatted_progress }} - {% if media.max_progress %}/ {{ media.max_progress }}{% endif %}
{% endif %} diff --git a/src/templates/app/components/media_table_items.html b/src/templates/app/components/media_table_items.html index 021f9a1b3..cc01ddabf 100644 --- a/src/templates/app/components/media_table_items.html +++ b/src/templates/app/components/media_table_items.html @@ -58,7 +58,6 @@ {% if media_type != MediaTypes.MOVIE.value %} {{ media.formatted_progress }} - {% if media.max_progress %}/ {{ media.max_progress }}{% endif %} {% if media_type == MediaTypes.TV.value %}{{ media.last_watched }}{% endif %} {% endif %} diff --git a/src/templates/app/components/progress_bar.html b/src/templates/app/components/progress_bar.html index 107763cbc..4b9c04b55 100644 --- a/src/templates/app/components/progress_bar.html +++ b/src/templates/app/components/progress_bar.html @@ -3,7 +3,10 @@ {% if media.status and user.progress_bar %}
{% if media.status == 'In progress' %} - {% if media.max_progress and media.max_progress > 0 %} + {% if media.get_progress_unit == 'percentage' %} +
+ {% elif media.max_progress and media.max_progress > 0 %} {% widthratio media.progress media.max_progress 100 as progress_width %}
diff --git a/src/templates/app/media_details.html b/src/templates/app/media_details.html index 9fbc92e32..50ffe0111 100644 --- a/src/templates/app/media_details.html +++ b/src/templates/app/media_details.html @@ -272,7 +272,6 @@

Your History

Progress: {{ current_instance.formatted_progress }} - {% if current_instance.max_progress %}/ {{ current_instance.max_progress }}{% endif %}
{% endif %} @@ -332,9 +331,6 @@

Your History

{% if media.media_type != MediaTypes.MOVIE.value %}
Progress: {{ user_media.formatted_progress }} - {% if media.media_type != MediaTypes.GAME.value %} - {{ media_type|long_unit|lower }}{{ user_media.progress|pluralize }} - {% endif %}
{% endif %} From d8dcb44743c6419c02d4bdabc2149a21f3ffa238 Mon Sep 17 00:00:00 2001 From: Connor Burton Date: Tue, 10 Mar 2026 01:26:34 +0000 Subject: [PATCH 5/6] add test for pages/% feature for books model --- src/app/tests/models/test_book.py | 106 ++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/app/tests/models/test_book.py diff --git a/src/app/tests/models/test_book.py b/src/app/tests/models/test_book.py new file mode 100644 index 000000000..77dd1afa5 --- /dev/null +++ b/src/app/tests/models/test_book.py @@ -0,0 +1,106 @@ +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.test import TestCase + +from app.models import ( + Book, + Item, + MediaTypes, + Sources, + Status, +) +from users.models import ProgressUnit + + +@patch("app.providers.services.get_media_metadata") +class BookModelTests(TestCase): + """Test case for the Book model methods.""" + + def setUp(self): + """Set up test data for Book model tests.""" + self.credentials = {"username": "test", "password": "12345"} + self.user = get_user_model().objects.create_user(**self.credentials) + + self.item = Item.objects.create( + media_id="book123", + source=Sources.OPENLIBRARY.value, + media_type=MediaTypes.BOOK.value, + title="Test Book", + ) + + def test_get_progress_unit_fallback(self, mock_metadata): + """Test that get_progress_unit falls back to user preference.""" + mock_metadata.return_value = {"max_progress": 200} + book = Book.objects.create( + item=self.item, + user=self.user, + status=Status.PLANNING.value, + ) + + # Default user preference is PAGES + self.assertEqual(book.get_progress_unit(), ProgressUnit.PAGES) + + # Update user preference to PERCENTAGE + self.user.book_progress_unit = ProgressUnit.PERCENTAGE + self.user.save() + self.assertEqual(book.get_progress_unit(), ProgressUnit.PERCENTAGE) + + # Override on book specifically + book.progress_unit = ProgressUnit.PAGES + book.save() + self.assertEqual(book.get_progress_unit(), ProgressUnit.PAGES) + + def test_process_progress_percentage(self, mock_metadata): + """Test progress processing when using percentage unit.""" + mock_metadata.return_value = {"max_progress": 200} + + book = Book.objects.create( + item=self.item, + user=self.user, + status=Status.IN_PROGRESS.value, + progress_unit=ProgressUnit.PERCENTAGE, + ) + + # Set progress to 50% + book.progress = 50 + book.save() + self.assertEqual(book.status, Status.IN_PROGRESS.value) + + # Set progress to 100% + book.progress = 100 + book.save() + self.assertEqual(book.status, Status.COMPLETED.value) + self.assertIsNotNone(book.end_date) + + # Set progress > 100% should be capped + book.status = Status.IN_PROGRESS.value + book.progress = 150 + book.save() + self.assertEqual(book.progress, 100) + + def test_formatted_progress_percentage(self, mock_metadata): + """Test formatting of progress when using percentage unit.""" + mock_metadata.return_value = {"max_progress": 200} + book = Book.objects.create( + item=self.item, + user=self.user, + status=Status.IN_PROGRESS.value, + progress=45, + progress_unit=ProgressUnit.PERCENTAGE, + ) + self.assertEqual(book.formatted_progress, "45%") + + def test_formatted_progress_pages(self, mock_metadata): + """Test formatting of progress when using pages unit.""" + mock_metadata.return_value = {"max_progress": 200} + book = Book.objects.create( + item=self.item, + user=self.user, + status=Status.IN_PROGRESS.value, + progress=150, + progress_unit=ProgressUnit.PAGES, + ) + # Mock max_progress annotation which usually comes from MediaManager + book.max_progress = 300 + self.assertEqual(book.formatted_progress, "150 / 300") From 3abfb883d6bff8bf7c63ce433a95aa15c7437877 Mon Sep 17 00:00:00 2001 From: Connor Burton Date: Tue, 10 Mar 2026 01:42:19 +0000 Subject: [PATCH 6/6] ruff changes --- src/app/forms.py | 2 +- ...gress_unit_historicalbook_progress_unit.py | 6 ++-- src/app/models.py | 32 ++++++++++--------- .../0051_user_book_progress_unit_and_more.py | 1 - 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/app/forms.py b/src/app/forms.py index bcb70ba28..5bc5084e4 100644 --- a/src/app/forms.py +++ b/src/app/forms.py @@ -302,7 +302,7 @@ class Meta(MediaForm.Meta): """Bind form to model.""" model = Book - fields = MediaForm.Meta.fields + ["progress_unit"] + fields = [*MediaForm.Meta.fields, "progress_unit"] labels = { "progress": ( f"Progress ({config.get_unit(MediaTypes.BOOK.value, short=False)}s)" diff --git a/src/app/migrations/0056_book_progress_unit_historicalbook_progress_unit.py b/src/app/migrations/0056_book_progress_unit_historicalbook_progress_unit.py index 9eb5e38bc..3c1010f5c 100644 --- a/src/app/migrations/0056_book_progress_unit_historicalbook_progress_unit.py +++ b/src/app/migrations/0056_book_progress_unit_historicalbook_progress_unit.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.11 on 2026-03-10 00:34 +# Generated by Django 5.2.11 on 2026-03-10 01:39 from django.db import migrations, models @@ -13,11 +13,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='book', name='progress_unit', - field=models.CharField(blank=True, default=None, max_length=20, null=True), + field=models.CharField(blank=True, default='', max_length=20), ), migrations.AddField( model_name='historicalbook', name='progress_unit', - field=models.CharField(blank=True, default=None, max_length=20, null=True), + field=models.CharField(blank=True, default='', max_length=20), ), ] diff --git a/src/app/models.py b/src/app/models.py index 21f6922fd..0acfa315c 100644 --- a/src/app/models.py +++ b/src/app/models.py @@ -823,11 +823,6 @@ def __str__(self): """Return the title of the media.""" return self.item.__str__() - def get_progress_unit(self): - """Return the progress unit for the media.""" - # Check if instance has a specific progress_unit field (like Book) - return getattr(self, "progress_unit", None) - def save(self, *args, **kwargs): """Save the media instance.""" if self.tracker.has_changed("progress"): @@ -838,17 +833,23 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) + def get_progress_unit(self): + """Return the progress unit for the media.""" + # Check if instance has a specific progress_unit field (like Book) + return getattr(self, "progress_unit", None) + def process_progress(self): """Update fields depending on the progress of the media.""" - if self.progress < 0: - self.progress = 0 + self.progress = max(self.progress, 0) if self.status != Status.IN_PROGRESS.value: return - if self.get_progress_unit() == users.models.ProgressUnit.PERCENTAGE: - self.progress = min(self.progress, 100) - if self.progress == 100: + percentage_unit = users.models.ProgressUnit.PERCENTAGE + if self.get_progress_unit() == percentage_unit: + max_percentage = 100 + self.progress = min(self.progress, max_percentage) + if self.progress == max_percentage: self.status = Status.COMPLETED.value now = timezone.now().replace(second=0, microsecond=0) self.end_date = now @@ -872,8 +873,10 @@ def process_progress(self): def process_status(self): """Update fields depending on the status of the media.""" if self.status == Status.COMPLETED.value: - if self.get_progress_unit() == users.models.ProgressUnit.PERCENTAGE: - self.progress = 100 + percentage_unit = users.models.ProgressUnit.PERCENTAGE + if self.get_progress_unit() == percentage_unit: + max_percentage = 100 + self.progress = max_percentage else: max_progress = providers.services.get_media_metadata( self.item.media_type, @@ -1638,9 +1641,8 @@ class Book(Media): progress_unit = models.CharField( max_length=20, - null=True, blank=True, - default=None, + default="", ) @property @@ -1653,7 +1655,7 @@ def formatted_progress(self): def get_progress_unit(self): """Return the progress unit for the book, falling back to user preference.""" unit = super().get_progress_unit() - if unit: + if unit and unit != "": return unit return self.user.book_progress_unit diff --git a/src/users/migrations/0051_user_book_progress_unit_and_more.py b/src/users/migrations/0051_user_book_progress_unit_and_more.py index d48f18a29..8be4794c5 100644 --- a/src/users/migrations/0051_user_book_progress_unit_and_more.py +++ b/src/users/migrations/0051_user_book_progress_unit_and_more.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): dependencies = [ - ('app', '0056_book_progress_unit_historicalbook_progress_unit'), ('auth', '0012_alter_user_first_name_max_length'), ('users', '0050_user_watch_provider_region'), ]