Hide deleted lists; add some api fields: volume, last change; other small api-related...
authorRadek Czajka <rczajka@rczajka.pl>
Tue, 17 Feb 2026 14:34:41 +0000 (15:34 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Tue, 17 Feb 2026 14:34:41 +0000 (15:34 +0100)
12 files changed:
src/catalogue/api/serializers.py
src/catalogue/api/tojson.py
src/catalogue/api/urls2.py
src/catalogue/api/views.py
src/catalogue/migrations/0052_book_pages_book_read_time.py [new file with mode: 0644]
src/catalogue/models/book.py
src/catalogue/templatetags/catalogue_tags.py
src/club/models.py
src/club/templates/club/receipt_email.txt
src/social/api/views.py
src/social/models.py
src/wolnelektury/settings/contrib.py

index 3d6341e..9fa05f5 100644 (file)
@@ -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'])
index 3ff257a..b803e73 100644 (file)
@@ -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':
index 3353b25..b8885af 100644 (file)
@@ -23,6 +23,9 @@ urlpatterns = [
          piwik_track_view(views.BookFragmentView.as_view()),
          name='catalogue_api_book_fragment'
          ),
+    path('books/<slug:slug>/children/',
+         views.BookChildrenView.as_view()
+         ),
     path('books/<slug:slug>/media/<slug:type>/',
          views.BookMediaView.as_view()
          ),
index 8cd2b6e..e5005e2 100644 (file)
@@ -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 (file)
index 0000000..dd66d27
--- /dev/null
@@ -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),
+        ),
+    ]
index eae021b..6fcf181 100644 (file)
@@ -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]
index 6c2368d..e50ab58 100644 (file)
@@ -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
index 7715495..de2d3c6 100644 (file)
@@ -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,
index 3d99902..9f3e8ae 100644 (file)
@@ -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
 
 
index 9d8fd4a..5ce6f27 100644 (file)
@@ -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)
 
index 862db4c..89fa65c 100644 (file)
@@ -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()
index bd40547..465286b 100644 (file)
@@ -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',
 }