From 572ee91a188114e383712eac2426dab3bcef6c00 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 17 Feb 2026 15:34:41 +0100 Subject: [PATCH] Hide deleted lists; add some api fields: volume, last change; other small api-related changes. --- src/catalogue/api/serializers.py | 7 +++- src/catalogue/api/tojson.py | 40 ++++++++++++++++++- src/catalogue/api/urls2.py | 3 ++ src/catalogue/api/views.py | 11 ++++- .../0052_book_pages_book_read_time.py | 23 +++++++++++ src/catalogue/models/book.py | 22 +++++++++- src/catalogue/templatetags/catalogue_tags.py | 2 +- src/club/models.py | 2 + src/club/templates/club/receipt_email.txt | 6 +-- src/social/api/views.py | 16 ++++---- src/social/models.py | 12 +++++- src/wolnelektury/settings/contrib.py | 2 +- 12 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 src/catalogue/migrations/0052_book_pages_book_read_time.py diff --git a/src/catalogue/api/serializers.py b/src/catalogue/api/serializers.py index 3d6341ed8..9fa05f54f 100644 --- a/src/catalogue/api/serializers.py +++ b/src/catalogue/api/serializers.py @@ -155,6 +155,7 @@ class BookSerializer2(serializers.ModelSerializer): view_name='catalogue_api_book', lookup_field='slug' ) + children = serializers.SerializerMethodField() audiences = serializers.ListField(source='audiences_pl') class Meta: @@ -164,7 +165,7 @@ class BookSerializer2(serializers.ModelSerializer): 'href', 'url', 'language', 'authors', 'translators', 'epochs', 'genres', 'kinds', - #'children', + 'children', 'parent', 'preview', 'epub', 'mobi', 'pdf', 'html', 'txt', 'fb2', 'xml', 'cover_thumb', 'cover', @@ -172,8 +173,12 @@ class BookSerializer2(serializers.ModelSerializer): 'abstract', 'has_mp3_file', 'has_sync_file', 'elevenreader_link', 'content_warnings', 'audiences', + 'changed_at', 'read_time', 'pages', 'redakcja' ] + def get_children(self, obj): + return list(obj.get_children().values('slug', 'title')) + class BookSerializer11Labs(serializers.ModelSerializer): url = AbsoluteURLField() href = AbsoluteURLField(view_name='catalogue_api_book', view_args=['slug']) diff --git a/src/catalogue/api/tojson.py b/src/catalogue/api/tojson.py index 3ff257a60..b803e73da 100644 --- a/src/catalogue/api/tojson.py +++ b/src/catalogue/api/tojson.py @@ -54,7 +54,7 @@ tags = { 'osoba': ('em', True, {'class': 'osoba'}, None, None), 'didaskalia': ('div', True, {'class': 'didaskalia'}, None, None), 'kwestia': ('div', False, {'class': 'kwestia'}, None, None), - 'didask_tekst': ('em', False, {'class': 'didask_tekst'}, None, None), + 'didask_tekst': ('em', True, {'class': 'didask_tekst'}, None, None), 'naglowek_czesc': ('h2', True, None, None, None), 'naglowek_akt': ('h2', True, None, None, None), @@ -94,6 +94,38 @@ tags = { 'br': ('br', False, None, None, None), 'indeks_dolny': ('em', True, {'class': 'indeks_dolny'}, None, False), 'mat': ('span', True, {'class': 'mat'}, None, False), + + 'mfenced': ('math_mfenced', True, None, None, False), + 'mfrac': ('math_mfrac', True, None, None, False), + 'mrow': ('math_mrow', True, None, None, False), + 'mi': ('math_mi', True, None, None, False), + 'mn': ('math_mn', True, None, None, False), + 'mo': ('math_mo', True, None, None, False), + 'msup': ('math_msup', True, None, None, False), + + 'list': ('blockquote', False, {'class': 'list'}, None, None), + 'wywiad_pyt': ('blockquote', False, {'class': 'wywiad_pyt'}, None, None), + 'wywiad_odp': ('blockquote', False, {'class': 'wywiad_odp'}, None, None), + 'rownolegle': ('blockquote', False, {'class': 'rownolegle'}, None, None), + 'animacja': ('div', False, {'class': 'animacja'}, None, None), + 'data': ('div', True, {'class': 'data'}, None, None), + 'podpis': ('div', True, {'class': 'podpis'}, None, None), + 'naglowek_listu': ('div', True, {'class': 'naglowek_listu'}, None, None), + 'pozdrowienie': ('div', True, {'class': 'pozdrowienie'}, None, None), + 'adresat': ('div', True, {'class': 'adresat'}, None, None), + 'tytul_oryg': ('div', True, {'class': 'tytul_oryg'}, None, None), + 'miejsce_data': ('div', True, {'class': 'miejsce_data'}, None, None), + 'audio': ('_ignore', False, None, None, None), + 'www': ('a', True, {'class': 'www'}, {'href': '.text'}, False), + + 'tabela': ('table', False, None, None, None), + 'tabelka': ('table', False, None, None, None), + 'wiersz': ('tr', False, None, None, None), + 'kol': ('td', True, None, None, None), + + 'ilustr': ('img', False, None, {'src': 'src'}, False), + 'tab': ('span', False, {'class': 'tab'}, {'szer': 'szer'}, False), + } id_prefixes = { @@ -158,7 +190,11 @@ def toj(elem, S): if attr_map: output.setdefault('attr', {}) for k, v in attr_map.items(): - output['attr'][k] = elem.attrib[v] + if v == '.text': + val = elem.text + else: + val = elem.attrib[v] + output['attr'][k] = val output['contents'] = contents output = [output] if elem.tag == 'strofa': diff --git a/src/catalogue/api/urls2.py b/src/catalogue/api/urls2.py index 3353b25e5..b8885af07 100644 --- a/src/catalogue/api/urls2.py +++ b/src/catalogue/api/urls2.py @@ -23,6 +23,9 @@ urlpatterns = [ piwik_track_view(views.BookFragmentView.as_view()), name='catalogue_api_book_fragment' ), + path('books//children/', + views.BookChildrenView.as_view() + ), path('books//media//', views.BookMediaView.as_view() ), diff --git a/src/catalogue/api/views.py b/src/catalogue/api/views.py index 8cd2b6e80..e5005e2de 100644 --- a/src/catalogue/api/views.py +++ b/src/catalogue/api/views.py @@ -235,7 +235,7 @@ class BookRecommendationsView(ListAPIView): Book, slug=self.kwargs['slug'] ) - return book.recommended(limit=3) + return book.get_recommended(limit=3) class BookList11Labs(BookList2): @@ -556,6 +556,15 @@ class BookFragmentView(RetrieveAPIView): return book.choose_fragment() +class BookChildrenView(ListAPIView): + serializer_class = serializers.BookSerializer2 + pagination_class = None + + def get_queryset(self): + book = get_object_or_404(Book, slug=self.kwargs['slug']) + return book.get_children() + + class BookMediaView(ListAPIView): serializer_class = serializers.MediaSerializer2 pagination_class = None diff --git a/src/catalogue/migrations/0052_book_pages_book_read_time.py b/src/catalogue/migrations/0052_book_pages_book_read_time.py new file mode 100644 index 000000000..dd66d27a7 --- /dev/null +++ b/src/catalogue/migrations/0052_book_pages_book_read_time.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.8 on 2026-02-17 14:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0051_book_has_audio'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='pages', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='book', + name='read_time', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index eae021b18..6fcf181e0 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -98,7 +98,9 @@ class Book(models.Model): translators = models.ManyToManyField(Tag, blank=True) narrators = models.ManyToManyField(Tag, blank=True, related_name='narrated') has_audio = models.BooleanField(default=False) - + read_time = models.FloatField(blank=True, null=True) + pages = models.FloatField(blank=True, null=True) + html_built = django.dispatch.Signal() published = django.dispatch.Signal() @@ -187,6 +189,10 @@ class Book(models.Model): def isbn_mobi(self): return self.get_extra_info_json().get('isbn_mobi') + @property + def redakcja(self): + return self.get_extra_info_json().get('about') + def is_accessible_to(self, user): if not self.preview: return True @@ -737,6 +743,8 @@ class Book(models.Model): book.load_toc() book.save() + book.update_stats() + meta_tags = Tag.tags_from_info(book_info) just_tags = [t for (t, rel) in meta_tags if not rel] @@ -804,6 +812,16 @@ class Book(models.Model): cls.published.send(sender=cls, instance=book) return book + def update_stats(self): + stats = self.wldocument2().get_statistics()['total'] + self.pages = ( + stats['verses_with_fn'] / 30 + + stats['chars_out_verse_with_fn'] / 1800) + self.read_time = self.get_time() + self.save(update_fields=['pages', 'read_time']) + if self.parent is not None: + self.parent.update_stats() + def update_references(self): Entity = apps.get_model('references', 'Entity') doc = self.wldocument2() @@ -1001,7 +1019,7 @@ class Book(models.Model): elif isinstance(publisher, list): return ', '.join(publisher) - def recommended(self, limit=4): + def get_recommended(self, limit=4): books_qs = type(self).objects.filter(findable=True) books_qs = books_qs.exclude(common_slug=self.common_slug).exclude(ancestor=self) books = type(self).tagged.related_to(self, books_qs)[:limit] diff --git a/src/catalogue/templatetags/catalogue_tags.py b/src/catalogue/templatetags/catalogue_tags.py index 6c2368d21..e50ab5817 100644 --- a/src/catalogue/templatetags/catalogue_tags.py +++ b/src/catalogue/templatetags/catalogue_tags.py @@ -310,7 +310,7 @@ def plain_list(context, object_list, with_initials=True, by_author=False, choice @register.simple_tag def related_books(book, limit=4, taken=0): - return book.recommended(limit=limit - taken) + return book.get_recommended(limit=limit - taken) @register.simple_tag diff --git a/src/club/models.py b/src/club/models.py index 77154954b..de2d3c62b 100644 --- a/src/club/models.py +++ b/src/club/models.py @@ -514,6 +514,8 @@ class PayUOrder(payu_models.Order): receipt = cls.generate_receipt(email, year) if receipt: content, optout, payments = receipt + else: + return ctx = { "email": email, "year": year, diff --git a/src/club/templates/club/receipt_email.txt b/src/club/templates/club/receipt_email.txt index 3d9990266..9f3e8ae75 100644 --- a/src/club/templates/club/receipt_email.txt +++ b/src/club/templates/club/receipt_email.txt @@ -17,17 +17,17 @@ PS Poniżej znajdziesz email sprzed kilku tygodni, który być może Ci umknął czy wiesz, że możesz odliczyć od podstawy opodatkowania wszystkie darowizny przekazane na prowadzenie biblioteki Wolne Lektury i działalność Fundacji? -Łącznie w {{ year }} otrzymaliśmy od Ciebie {{ total }} zł na zapewnienie dostępu do książek wszystkim dzieciakom. +Łącznie w {{ year }} otrzymaliśmy od Ciebie {{ total }} zł na udostępnianie nowych ebooków i audiobooków. Zestawienie darowizn za rok {{ year }} znajdziesz w załączniku! Dane z załącznika wprowadź do formularza PIT. Pamiętaj, że w przypadku kontroli z urzędu skarbowego musisz mieć potwierdzenie wykonanych przelewów z Twojego banku. -Podczas wypełniania swojego PIT-u możesz także przekazać 1,5% swojego podatku na Wolne Lektury. Dzięki temu ufundujesz darmową e-książkę, która trafi do tysięcy dzieciaków. Wystarczy, że w odpowiednim polu wpiszesz nazwę organizacji „fundacja Wolne Lektury” oraz numer KRS 0000070056. Wspierasz Wolne Lektury w ten sposób już od dawna? W takim razie nic nie musisz zmieniać, bo system ponownie sam wprowadzi dane fundacji. +Podczas wypełniania swojego PIT-u możesz także przekazać 1,5% swojego podatku na Wolne Lektury. Dzięki temu ufundujesz pomożesz nam udostępniać kolejne ebooki i audiobooki w tym roku . Wystarczy, że w odpowiednim polu wpiszesz nazwę organizacji „fundacja Wolne Lektury” oraz numer KRS 0000070056. Wspierasz Wolne Lektury w ten sposób już od dawna? W takim razie nic nie musisz zmieniać, bo system ponownie sam wprowadzi dane fundacji. Serdecznie dziękujemy za Twoje wsparcie! -Paulina Choromańska i Jarosław Lipszyc +Paulina Choromańska i Radosław Czajka w imieniu całego zespołu Wolnych Lektur diff --git a/src/social/api/views.py b/src/social/api/views.py index 9d8fd4a59..5ce6f2797 100644 --- a/src/social/api/views.py +++ b/src/social/api/views.py @@ -139,15 +139,17 @@ class UserListSerializer(serializers.ModelSerializer): validated_data['name'], create=True ) - instance.userlistitem_set.all().delete() - for book in validated_data['books']: - instance.append(book) + if 'books' in validated_data: + instance.userlistitem_set.all().delete() + for book in validated_data['books']: + instance.append(book) return instance def update(self, instance, validated_data): - instance.userlistitem_set.all().delete() - for book in validated_data['books']: - instance.append(instance) + if 'books' in validated_data: + instance.userlistitem_set.all().delete() + for book in validated_data['books']: + instance.append(instance) return instance class UserListBooksSerializer(UserListSerializer): @@ -232,7 +234,7 @@ class ListView(RetrieveUpdateDestroyAPIView): ) else: return get_object_or_404( - models.UserList, + models.UserList.all_objects.all(), slug=self.kwargs['slug'], user=self.request.user) diff --git a/src/social/models.py b/src/social/models.py index 862db4cdd..89fa65cd8 100644 --- a/src/social/models.py +++ b/src/social/models.py @@ -277,6 +277,11 @@ class Progress(Syncable, models.Model): return super().save(*args, **kwargs) +class ActiveManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter(deleted=False) + + class UserList(Syncable, models.Model): slug = models.SlugField(unique=True) user = models.ForeignKey(User, models.CASCADE) @@ -289,7 +294,10 @@ class UserList(Syncable, models.Model): reported_timestamp = models.DateTimeField() syncable_fields = ['name', 'public', 'deleted'] - + + objects = ActiveManager() + all_objects = models.Manager() + def get_absolute_url(self): return reverse( 'tagged_object_list', @@ -351,7 +359,7 @@ class UserList(Syncable, models.Model): # merge? lists = list(cls.objects.filter(user=user, favorites=True)) for l in lists[1:]: - t.userlistitem_set.all().update( + l.userlistitem_set.all().update( list=lists[0] ) l.delete() diff --git a/src/wolnelektury/settings/contrib.py b/src/wolnelektury/settings/contrib.py index bd40547fc..465286bbb 100644 --- a/src/wolnelektury/settings/contrib.py +++ b/src/wolnelektury/settings/contrib.py @@ -33,7 +33,7 @@ REST_FRAMEWORK = { 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_PAGINATION_CLASS': 'api.pagination.WLLimitOffsetPagination', - 'PAGE_SIZE': 10, + 'PAGE_SIZE': 20, 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', } -- 2.20.1