From 36adb8b26d84fb53baebb7d0537cf0a832b4f44d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 12 May 2026 11:35:45 +0200 Subject: [PATCH 01/16] Missing link target --- src/annoy/templates/annoy/banner_seasonal_overlay.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/annoy/templates/annoy/banner_seasonal_overlay.html b/src/annoy/templates/annoy/banner_seasonal_overlay.html index 7ab86b2ff..62882e3f5 100644 --- a/src/annoy/templates/annoy/banner_seasonal_overlay.html +++ b/src/annoy/templates/annoy/banner_seasonal_overlay.html @@ -26,7 +26,7 @@ {% endif %}
{{ banner.get_text|safe }} - {{ banner.action_label }} + {{ banner.action_label }}
-- 2.20.1 From 0636415d54e414aa9ddff61bed92f72f9ee03a21 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 12 May 2026 11:49:45 +0200 Subject: [PATCH 02/16] Set password validators --- src/wolnelektury/settings/basic.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/wolnelektury/settings/basic.py b/src/wolnelektury/settings/basic.py index 869a78dfb..5c86787f8 100644 --- a/src/wolnelektury/settings/basic.py +++ b/src/wolnelektury/settings/basic.py @@ -15,6 +15,24 @@ CONTACT_EMAIL = 'fundacja@wolnelektury.pl' ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split() +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + "OPTIONS": { + "min_length": 6, + }, + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + CACHE_MIDDLEWARE_SECONDS = 3 * 60 CACHES = { -- 2.20.1 From 15c9704d44c349ced8dd40def225d3c6d3ce99ab Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 12 May 2026 11:54:14 +0200 Subject: [PATCH 03/16] fixes --- src/wolnelektury/templates/account/base.html | 2 +- src/wolnelektury/templates/socialaccount/connections.html | 2 +- src/wolnelektury/templates/socialaccount/login_cancelled.html | 2 +- src/wolnelektury/templates/user.html | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/wolnelektury/templates/account/base.html b/src/wolnelektury/templates/account/base.html index 40c1de9e7..093fcb2a6 100644 --- a/src/wolnelektury/templates/account/base.html +++ b/src/wolnelektury/templates/account/base.html @@ -1,7 +1,7 @@ {% extends "base_simple.html" %} {% block body %} -
+
{% block content %}{% endblock %}
{% endblock %} diff --git a/src/wolnelektury/templates/socialaccount/connections.html b/src/wolnelektury/templates/socialaccount/connections.html index dcee6ff87..fd4110389 100644 --- a/src/wolnelektury/templates/socialaccount/connections.html +++ b/src/wolnelektury/templates/socialaccount/connections.html @@ -5,7 +5,7 @@ {% block head_title %}{% trans "Połączone konta" %}{% endblock %} {% block body %} -
+

{% trans "Połączone konta" %}

diff --git a/src/wolnelektury/templates/socialaccount/login_cancelled.html b/src/wolnelektury/templates/socialaccount/login_cancelled.html index 3e95773c9..98da1308b 100644 --- a/src/wolnelektury/templates/socialaccount/login_cancelled.html +++ b/src/wolnelektury/templates/socialaccount/login_cancelled.html @@ -5,7 +5,7 @@ {% block head_title %}{% trans "Logowanie anulowane" %}{% endblock %} {% block body %} -
+

{% trans "Logowanie anulowane" %}

diff --git a/src/wolnelektury/templates/user.html b/src/wolnelektury/templates/user.html index e9a504781..42078df59 100644 --- a/src/wolnelektury/templates/user.html +++ b/src/wolnelektury/templates/user.html @@ -20,7 +20,6 @@ {% if request.user.membership %}

{% trans "Wspierasz Wolne Lektury, dziękujemy!" %}

{% endif %} -

{% trans "Zewnętrzne konta" %}

{% trans "Wyloguj się" %}

{% endblock %} -- 2.20.1 From fd86b5e2d53eb222653b5cbed22bc334238fd2fa Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 12 May 2026 12:00:31 +0200 Subject: [PATCH 04/16] cover size fix --- src/wolnelektury/static/2022/styles/layout/_books.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/wolnelektury/static/2022/styles/layout/_books.scss b/src/wolnelektury/static/2022/styles/layout/_books.scss index c983b3876..e14adbfac 100644 --- a/src/wolnelektury/static/2022/styles/layout/_books.scss +++ b/src/wolnelektury/static/2022/styles/layout/_books.scss @@ -108,9 +108,6 @@ img { width: 100%; - @include rwd($break3) { - width: 173px; - } } } -- 2.20.1 From 50995366c1728536907fbeded492f681c56c1b0d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 14 May 2026 11:38:24 +0200 Subject: [PATCH 05/16] Fuller docker config --- Dockerfile | 24 ++++++++++++++++++++++++ Makefile | 2 +- docker-compose.yml | 6 +++++- scripts/entrypoint.sh | 13 +++++++++++++ src/wolnelektury/settings/basic.py | 2 +- 5 files changed, 44 insertions(+), 3 deletions(-) create mode 100755 scripts/entrypoint.sh diff --git a/Dockerfile b/Dockerfile index ed113fe3b..b23530b9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,13 +37,37 @@ WORKDIR /app/src RUN mkdir /app/.ipython + + + FROM base AS dev #RUN pip install --no-cache-dir coverage + + FROM base AS prod +USER root + RUN pip install --no-cache-dir gunicorn +USER app + COPY src /app/src + + + + +FROM prod AS static-builder + +RUN python manage.py collectstatic --noinput + + + +FROM nginx:alpine AS nginx + +# COPY nginx.conf conf.d/... + +COPY --from=static-builder /app/var/static /app/var/static diff --git a/Makefile b/Makefile index 98a68987c..59e222af6 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ build: UID=$(UID) GID=$(GID) docker compose build shell: - UID=$(UID) GID=$(GID) docker compose run --rm web bash + UID=$(UID) GID=$(GID) docker compose exec web bash logs: docker compose logs -f diff --git a/docker-compose.yml b/docker-compose.yml index b6515098d..bda15bb40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,16 +7,21 @@ x-app-base: &app-base GID: ${GID:-1000} env_file: - .env + environment: + DEBUG: "True" volumes: - ./src:/app/src + - ./scripts:/app/scripts - ./var/media:/app/var/media - ./var/static:/app/var/static - ./var/counters/:/app/var/counters + - ${LIBRARIAN:-/nonexistent}:/${LIBRARIAN:+edita}ble/librarian - ipython:/app/.ipython depends_on: - db - redis - memcached + entrypoint: /app/scripts/entrypoint.sh services: web: @@ -31,7 +36,6 @@ services: db: image: postgres:18 - container_name: db env_file: - .env volumes: diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100755 index 000000000..7d809ef83 --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# For use in development only. +# Installs in editable mode any libs mounted under /editable/ +# This way, we can easily work on e.g. librarian. + +for lib in /editable/* +do + [ -d "$lib" ] || continue + pip install -e $lib +done + +exec "$@" diff --git a/src/wolnelektury/settings/basic.py b/src/wolnelektury/settings/basic.py index 5c86787f8..84503ed5f 100644 --- a/src/wolnelektury/settings/basic.py +++ b/src/wolnelektury/settings/basic.py @@ -3,7 +3,7 @@ # import os -DEBUG = True +DEBUG = os.environ.get('DEBUG', '') ADMINS = [ # ('Your Name', 'your_email@domain.com'), -- 2.20.1 From 999910c649979e70c5c73e1a4d853201dc6c0362 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 14 May 2026 11:38:41 +0200 Subject: [PATCH 06/16] Minor layout fixes --- src/wolnelektury/static/2022/styles/layout/_annoy.scss | 2 +- src/wolnelektury/static/2022/styles/layout/_checkout.scss | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wolnelektury/static/2022/styles/layout/_annoy.scss b/src/wolnelektury/static/2022/styles/layout/_annoy.scss index 18c8df847..d66cb165e 100644 --- a/src/wolnelektury/static/2022/styles/layout/_annoy.scss +++ b/src/wolnelektury/static/2022/styles/layout/_annoy.scss @@ -433,7 +433,7 @@ } p { - margin: 1em 0; + margin: 0 0 1em 0; } .l-button { display: inline-block; diff --git a/src/wolnelektury/static/2022/styles/layout/_checkout.scss b/src/wolnelektury/static/2022/styles/layout/_checkout.scss index 135c65310..a1ea7b4f3 100644 --- a/src/wolnelektury/static/2022/styles/layout/_checkout.scss +++ b/src/wolnelektury/static/2022/styles/layout/_checkout.scss @@ -111,20 +111,21 @@ letter-spacing: -0.02em; color: #92BD39; margin: 0; + margin-bottom: 23px; } p { font-weight: normal; font-size: 18px; line-height: 150%; - margin-top: 8px; + margin-top: 0; + margin-bottom: 8px; &:first-of-type { font-weight: 600; font-size: 21.5px; line-height: 140%; letter-spacing: -0.01em; - margin-top: 23px; } } } -- 2.20.1 From c8ae8e5cf35ed1e89bc10aae45d7c15431a33a7e Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 19 May 2026 14:42:04 +0200 Subject: [PATCH 07/16] minor form fixes --- src/club/templates/club/donation_step_base.html | 2 +- src/wolnelektury/static/2022/styles/layout/_annoy.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/club/templates/club/donation_step_base.html b/src/club/templates/club/donation_step_base.html index e2cec7ac2..546b399c9 100644 --- a/src/club/templates/club/donation_step_base.html +++ b/src/club/templates/club/donation_step_base.html @@ -38,7 +38,7 @@
{% endblock %}
-
+
{% if view.step > 1 and view.step != 4 %} diff --git a/src/wolnelektury/static/2022/styles/layout/_annoy.scss b/src/wolnelektury/static/2022/styles/layout/_annoy.scss index d66cb165e..5301cbafd 100644 --- a/src/wolnelektury/static/2022/styles/layout/_annoy.scss +++ b/src/wolnelektury/static/2022/styles/layout/_annoy.scss @@ -374,7 +374,7 @@ color: #083F4D; border-radius: 0 0 10px 10px; position: absolute; - z-index: 1000; + z-index: 14; left: 16px; right: 16px; padding: 15px 20px; -- 2.20.1 From 8604bb538bd8ddbd5a73cb24dfde2d05e2b84717 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 19 May 2026 14:44:08 +0200 Subject: [PATCH 08/16] up librarian --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index ad5eb4e64..d434c9688 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -42,7 +42,7 @@ mutagen==1.47 sorl-thumbnail==12.10.0 # home-brewed & dependencies -librarian==26.4.1 +librarian==26.5 # celery tasks celery[redis]==5.4.0 -- 2.20.1 From 55efada7f210cda246203bdef15bdd781ad26cfe Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 2 Jun 2026 10:47:35 +0200 Subject: [PATCH 09/16] API v4: change book representation when reading bookmarks to full. --- src/api/urls.py | 1 + src/bookmarks/api/views.py | 41 +++++++++++++++++++++++++++++++---- src/social/api/serializers.py | 21 ++++++++++++++++++ src/social/api/views.py | 17 ++++++++++++--- 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/api/urls.py b/src/api/urls.py index 867e1fd09..159781141 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -34,6 +34,7 @@ urlpatterns1 = [ urlpatterns = [ path('2/', include((urlpatterns1, 'api'), namespace="v2")), path('3/', include((urlpatterns1, 'api'), namespace="v3")), + path('4/', include((urlpatterns1, 'api'), namespace="v4")), path('1/', views.Unsupported.as_view()), path('oauth/request_token/', csrf_exempt(views.OAuth1RequestTokenView.as_view())), diff --git a/src/bookmarks/api/views.py b/src/bookmarks/api/views.py index b500a6654..82502e9e2 100644 --- a/src/bookmarks/api/views.py +++ b/src/bookmarks/api/views.py @@ -13,9 +13,11 @@ from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveUpda from rest_framework import serializers from rest_framework.permissions import SAFE_METHODS, IsAuthenticated, IsAuthenticatedOrReadOnly from api.fields import AbsoluteURLField +from catalogue.api.serializers import BookSerializer2 -class BookmarkSerializer(serializers.ModelSerializer): +class BookmarkSerializerV3(serializers.ModelSerializer): + """Replaced in API v4.""" book = serializers.SlugRelatedField( queryset=catalogue.models.Book.objects.all(), slug_field='slug', required=False @@ -30,12 +32,33 @@ class BookmarkSerializer(serializers.ModelSerializer): read_only_fields = ['uuid', 'mode'] +class BookmarkSerializer(serializers.ModelSerializer): + book_slug = serializers.SlugRelatedField( + queryset=catalogue.models.Book.objects.all(), slug_field='slug', + write_only=True, + required=False + ) + book = BookSerializer2(read_only=True) + href = AbsoluteURLField(view_name='api_bookmark', view_args=['uuid']) + timestamp = serializers.IntegerField(required=False) + location = serializers.CharField(required=False) + + class Meta: + model = models.Bookmark + fields = ['book_slug', 'book', 'anchor', 'audio_timestamp', 'mode', 'note', 'href', 'uuid', 'location', 'timestamp', 'deleted'] + read_only_fields = ['uuid', 'mode'] + @never_cache class BookmarksView(ListCreateAPIView): permission_classes = [IsAuthenticated] - serializer_class = BookmarkSerializer + def get_serializer_class(self): + if self.request.version < 'v4': + return BookmarkSerializerV3 + else: + return BookmarkSerializer + def get_queryset(self): return self.request.user.bookmark_set.all() @@ -46,9 +69,14 @@ class BookmarksView(ListCreateAPIView): @never_cache class BookBookmarksView(ListAPIView): permission_classes = [IsAuthenticated] - serializer_class = BookmarkSerializer pagination_class = None + def get_serializer_class(self): + if self.request.version < 'v4': + return BookmarkSerializerV3 + else: + return BookmarkSerializer + def get_queryset(self): return self.request.user.bookmark_set.filter(book__slug=self.kwargs['book']) @@ -56,9 +84,14 @@ class BookBookmarksView(ListAPIView): @never_cache class BookmarkView(RetrieveUpdateDestroyAPIView): permission_classes = [IsAuthenticatedOrReadOnly] - serializer_class = BookmarkSerializer lookup_field = 'uuid' + def get_serializer_class(self): + if self.request.version < 'v4': + return BookmarkSerializerV3 + else: + return BookmarkSerializer + def get_queryset(self): if self.request.method in SAFE_METHODS: q = Q(deleted=False) diff --git a/src/social/api/serializers.py b/src/social/api/serializers.py index cbaa3bb6a..155c0e621 100644 --- a/src/social/api/serializers.py +++ b/src/social/api/serializers.py @@ -130,6 +130,27 @@ class UserListItemSerializer(serializers.ModelSerializer): } +class UserListItemReadSerializerV3(UserListItemSerializer): + book = catalogue.api.serializers.BookSerializer2() + bookmark = bookmarks.api.views.BookmarkSerializerV3() + class Meta: + model = models.UserListItem + fields = [ + 'client_id', + 'uuid', + 'order', + 'list_slug', + 'timestamp', + 'favorites', + 'deleted', + + 'book', + 'fragment', + 'quote', + 'bookmark', + 'note', + ] + class UserListItemReadSerializer(UserListItemSerializer): book = catalogue.api.serializers.BookSerializer2() bookmark = bookmarks.api.views.BookmarkSerializer() diff --git a/src/social/api/views.py b/src/social/api/views.py index 2a57ad3d4..35af9a8ca 100644 --- a/src/social/api/views.py +++ b/src/social/api/views.py @@ -194,7 +194,10 @@ class ListItemListViewV3(ListCreateAPIView): def get_serializer_class(self): if self.request.method == 'GET': - return serializers.UserListItemReadSerializer + if self.request.version < 'v4': + return serializers.UserListItemReadSerializerV3 + else: + return serializers.UserListItemReadSerializer else: return serializers.UserListItemSerializer @@ -215,7 +218,12 @@ class ListItemListViewV3(ListCreateAPIView): @never_cache class ListItemsForBook(ListAPIView): permission_classes = [IsAuthenticated] - serializer_class = serializers.UserListItemReadSerializer + + def get_serializer_class(self): + if self.request.version < 'v4': + return serializers.UserListItemReadSerializerV3 + else: + return serializers.UserListItemReadSerializer def get_queryset(self): book = get_object_or_404(catalogue.models.Book, slug=self.kwargs['book']) @@ -250,7 +258,10 @@ class ListItemViewV3(RetrieveUpdateDestroyAPIView): def get_serializer_class(self): if self.request.method == 'GET': - return serializers.UserListItemReadSerializer + if self.request.version < 'v4': + return serializers.UserListItemReadSerializerV3 + else: + return serializers.UserListItemReadSerializer else: return serializers.UserListItemSerializer -- 2.20.1 From ce23e0cccbe0a08b22592fd589c83c9b80a4d026 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 16 Jun 2026 13:21:03 +0200 Subject: [PATCH 10/16] first streaming mp3 zip --- requirements/requirements.txt | 2 ++ src/catalogue/urls.py | 1 + src/catalogue/views.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index d434c9688..56887b9a2 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -41,6 +41,8 @@ Pillow==9.5.0 mutagen==1.47 sorl-thumbnail==12.10.0 +zipstream-ng==1.9.2 + # home-brewed & dependencies librarian==26.5 diff --git a/src/catalogue/urls.py b/src/catalogue/urls.py index 91c66a2fe..724a7831f 100644 --- a/src/catalogue/urls.py +++ b/src/catalogue/urls.py @@ -51,6 +51,7 @@ urlpatterns = [ path('zip/epub.zip', views.download_zip, {'file_format': 'epub', 'slug': None}, 'download_zip_epub'), path('zip/mobi.zip', views.download_zip, {'file_format': 'mobi', 'slug': None}, 'download_zip_mobi'), path('zip/mp3/.zip', views.download_zip, {'media_format': 'mp3'}, 'download_zip_mp3'), + path('zip/_mp3.zip', views.stream_zip, {'media_format': 'mp3'}), path('zip/ogg/.zip', views.download_zip, {'media_format': 'ogg'}, 'download_zip_ogg'), # Public interface. Do not change this URLs. diff --git a/src/catalogue/views.py b/src/catalogue/views.py index e754e05ca..487310663 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -5,11 +5,13 @@ from collections import OrderedDict import random import re from urllib.parse import quote_plus +from slugify import slugify +from zipstream import ZipStream from django.conf import settings from django.template.loader import render_to_string from django.shortcuts import get_object_or_404, render, redirect -from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect +from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect, StreamingHttpResponse from django.urls import reverse from django.db.models import Q, QuerySet from django.contrib.auth.decorators import login_required, user_passes_test @@ -518,6 +520,31 @@ def download_zip(request, file_format=None, media_format=None, slug=None): return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?=')) +def stream_zip(request, media_format=None, slug=None): + book = get_object_or_404(Book, slug=slug) + def iterate_audiobooks(book, names): + for bm in book.media.filter(type=media_format).order_by('index'): + yield ( + bm.file.path, + names + (slugify(bm.part_name),) if bm.part_name else names + ) + for child in book.get_children(): + yield from iterate_audiobooks(child, names + (slugify(child.title),)) + + zs = ZipStream() + + for i, (file_path, names) in enumerate(iterate_audiobooks(book, ())): + index = i + 1 + part_name = '_'.join(names) + ext = file_path.rsplit('.', 1)[-1] + zip_name = f'{book.slug}_{index:03d}_{part_name}'[:240] + '.' + ext + zs.add_path(file_path, zip_name) + + response = StreamingHttpResponse(zs, content_type='application/zip') + response['Content-Disposition'] = f'attachment; filename={slug}_{media_format}.zip' + return response + + class CustomPDFFormView(AjaxableFormView): form_class = forms.CustomPDFForm title = gettext_lazy('Stwórz własny PDF') -- 2.20.1 From e70d66e668627a4d9c65900856ca7024d8d87fe1 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 16 Jun 2026 13:22:04 +0200 Subject: [PATCH 11/16] fix for bookmarks --- src/bookmarks/api/views.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/bookmarks/api/views.py b/src/bookmarks/api/views.py index 82502e9e2..315624fab 100644 --- a/src/bookmarks/api/views.py +++ b/src/bookmarks/api/views.py @@ -48,6 +48,17 @@ class BookmarkSerializer(serializers.ModelSerializer): fields = ['book_slug', 'book', 'anchor', 'audio_timestamp', 'mode', 'note', 'href', 'uuid', 'location', 'timestamp', 'deleted'] read_only_fields = ['uuid', 'mode'] + def create(self, validated_data): + book = validated_data.pop('book_slug', None) + if book is not None: + validated_data['book'] = book + return super().create(validated_data) + + def update(self, instance, validated_data): + book = validated_data.pop('book_slug', None) + if book is not None: + validated_data['book'] = book + return super().update(instance, validated_data) @never_cache class BookmarksView(ListCreateAPIView): -- 2.20.1 From 9e7102a05937bf83ae2916b9206be011098e2aa1 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 16 Jun 2026 13:23:12 +0200 Subject: [PATCH 12/16] fixes for book page toc --- src/wolnelektury/static/2022/styles/layout/_article.scss | 5 +++-- src/wolnelektury/static/js/book_text/references.js | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wolnelektury/static/2022/styles/layout/_article.scss b/src/wolnelektury/static/2022/styles/layout/_article.scss index cb913a36e..c4b1024fe 100644 --- a/src/wolnelektury/static/2022/styles/layout/_article.scss +++ b/src/wolnelektury/static/2022/styles/layout/_article.scss @@ -30,10 +30,11 @@ list-style: none; margin-top: 0.25rem; margin-bottom: 0; - li { + margin-left: 1em; font-size: 18px; - line-height: 150%; font-weight: normal; + li { + line-height: 150%; } diff --git a/src/wolnelektury/static/js/book_text/references.js b/src/wolnelektury/static/js/book_text/references.js index ee7e2bd91..394ec3ffd 100644 --- a/src/wolnelektury/static/js/book_text/references.js +++ b/src/wolnelektury/static/js/book_text/references.js @@ -610,6 +610,7 @@ $('a.anchor').on('click', function(e) { // Workaround for bad TOC markers. if ($(this).closest('#toc').length) return; + if ($(this).closest('.toc').length) return; if ($(this).closest('#wltoc').length) return; e.preventDefault(); -- 2.20.1 From cba9f6f86615660a7dd4d63def4cbf9c6751e928 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 16 Jun 2026 13:35:34 +0200 Subject: [PATCH 13/16] Sync deleted bookmarks --- src/bookmarks/api/views.py | 6 ++++++ src/bookmarks/views.py | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/bookmarks/api/views.py b/src/bookmarks/api/views.py index 315624fab..6aeb82de8 100644 --- a/src/bookmarks/api/views.py +++ b/src/bookmarks/api/views.py @@ -3,6 +3,7 @@ from api.utils import never_cache from django.db.models import Q from django.http import Http404, JsonResponse from django.shortcuts import render, get_object_or_404 +from django.utils.timezone import now from django.views.decorators import cache import catalogue.models from wolnelektury.utils import is_ajax @@ -111,3 +112,8 @@ class BookmarkView(RetrieveUpdateDestroyAPIView): return models.Bookmark.objects.filter(q) else: return self.request.user.bookmark_set.all() + + def perform_destroy(self, instance): + instance.deleted = True + instance.updated_at = now() + instance.save() diff --git a/src/bookmarks/views.py b/src/bookmarks/views.py index 50390df16..c2b40e16b 100644 --- a/src/bookmarks/views.py +++ b/src/bookmarks/views.py @@ -1,5 +1,6 @@ from django.http import Http404, JsonResponse from django.shortcuts import render, get_object_or_404 +from django.utils.timezone import now from django.views.decorators import cache import catalogue.models from wolnelektury.utils import is_ajax @@ -41,6 +42,7 @@ def bookmarks(request): for bm in models.Bookmark.objects.filter( user=request.user, book=book, + deleted=False ) }) @@ -54,7 +56,10 @@ def bookmark(request, uuid): def bookmark_delete(request, uuid): - models.Bookmark.objects.filter(user=request.user, uuid=uuid).delete() + models.Bookmark.objects.filter(user=request.user, uuid=uuid).update( + deleted=True, + updated_at=now() + ) return JsonResponse({}) -- 2.20.1 From ba566bebbc9d89088ca6812360b29149f1e93314 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 16 Jun 2026 14:16:25 +0200 Subject: [PATCH 14/16] switch download audiobooks views --- .../catalogue/audiobook_zip_readme.txt | 21 ++++++++----------- src/catalogue/urls.py | 5 ++--- src/catalogue/views.py | 13 +++++++++++- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/catalogue/templates/catalogue/audiobook_zip_readme.txt b/src/catalogue/templates/catalogue/audiobook_zip_readme.txt index eaa63a536..a674f316c 100644 --- a/src/catalogue/templates/catalogue/audiobook_zip_readme.txt +++ b/src/catalogue/templates/catalogue/audiobook_zip_readme.txt @@ -1,7 +1,8 @@ -Ten audiobook pochodzi z darmowej biblioteki internetowej Wolne Lektury. Znajdziesz w niej tysiące ebooków i audiobooków. -Wolne Lektury to projekt prowadzony przez fundację Wolne Lektury. Jesteśmy organizacją pozarządową. Działamy dzięki wsparciu darczyńców takich jak Ty. -Wspólnie możemy udostępnić kolejne książki. Wspieraj Wolne Lektury stałą comiesięczną kwotą. Dzięki temu wcześniej uzyskasz dostęp do wyjątkowych publikacji! Możesz też wesprzeć nas jednorazowo. -Wejdź na https://wolnelektury.pl/pomagam/ i dorzuć się do nowych nagrań. +Ten audiobook pochodzi z darmowej biblioteki internetowej Wolne Lektury. Znajdziesz w niej tysiące książek i audiobooków. + +Wolne Lektury nie mają stałego finansowania. +Aby stabilnie działać, potrzebujemy wsparcia darczyńców takich jak Ty. +Wejdź na https://wolnelektury.pl/pomagam/ by wesprzeć naszą działalność. Audiobooki Wolnych Lektur są udostępniane na wolnych licencjach, co oznacza, że możesz je swobodnie wykorzystywać, udostępniać i publikować pod warunkiem zachowania warunków licencji i zgodnie z Zasadami wykorzystania Wolnych Lektur: https://wolnelektury.pl/info/zasady-wykorzystania/ @@ -9,9 +10,7 @@ Audiobooki Wolnych Lektur są udostępniane na wolnych licencjach, co oznacza, {% endif %}{% if meta.cover_by %}Okładka na podstawie: {{ meta.cover_by|safe }}{% if meta.cover_source %}, {{ meta.cover_source }}{% endif %}. -{% endif %}Przekaż 1,5%, by wszystkie dzieciaki w Polsce miały dostęp do darmowych książek KRS 0000070056: https://wolnelektury.pl/info/wesprzyj-nas/ - -Zostań Przyjacielem Wolnych Lektur: https://wolnelektury.pl/pomagam/ +{% endif %}Przekaż 1,5% na Wolne Lektury: KRS 0000070056, https://wolnelektury.pl/info/wesprzyj-nas/ Słuchaj nas na YouTube: https://wolnelektury.pl/re/WolneLekturyYT/ @@ -19,12 +18,10 @@ Wolne Lektury ul. Marszałkowska 84/92 lok. 125 00-514 Warszawa tel./fax: +48 22 621 30 17 -e-mail:fundacja@wolnelektury.pl -www.fundacja.wolnelektury.pl +e-mail: fundacja@wolnelektury.pl +https://fundacja.wolnelektury.pl Organizacja Pożytku Publicznego Sąd Rejestrowy dla M.St. Warszawy w Warszawie, XII Wydział Gospodarczy -KRS 0000070056, NIP 952-18-77-087 - -Zapraszamy do wysłuchania audiobooków Wolnych Lektur: https://wolnelektury.pl/katalog/audiobooki/ +KRS 0000070056, NIP 952-18-77-087 diff --git a/src/catalogue/urls.py b/src/catalogue/urls.py index 724a7831f..37b298dca 100644 --- a/src/catalogue/urls.py +++ b/src/catalogue/urls.py @@ -50,9 +50,8 @@ urlpatterns = [ path('zip/pdf.zip', views.download_zip, {'file_format': 'pdf', 'slug': None}, 'download_zip_pdf'), path('zip/epub.zip', views.download_zip, {'file_format': 'epub', 'slug': None}, 'download_zip_epub'), path('zip/mobi.zip', views.download_zip, {'file_format': 'mobi', 'slug': None}, 'download_zip_mobi'), - path('zip/mp3/.zip', views.download_zip, {'media_format': 'mp3'}, 'download_zip_mp3'), - path('zip/_mp3.zip', views.stream_zip, {'media_format': 'mp3'}), - path('zip/ogg/.zip', views.download_zip, {'media_format': 'ogg'}, 'download_zip_ogg'), + path('zip/_mp3.zip', views.stream_zip, {'media_format': 'mp3'}, 'download_zip_mp3'), + path('zip/_ogg.zip', views.stream_zip, {'media_format': 'ogg'}, 'download_zip_ogg'), # Public interface. Do not change this URLs. path('lektura/.html', views.book_text, name='book_text'), diff --git a/src/catalogue/views.py b/src/catalogue/views.py index 487310663..8bb86c282 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -526,6 +526,7 @@ def stream_zip(request, media_format=None, slug=None): for bm in book.media.filter(type=media_format).order_by('index'): yield ( bm.file.path, + bm.get_extra_info_json().get('license'), names + (slugify(bm.part_name),) if bm.part_name else names ) for child in book.get_children(): @@ -533,13 +534,23 @@ def stream_zip(request, media_format=None, slug=None): zs = ZipStream() - for i, (file_path, names) in enumerate(iterate_audiobooks(book, ())): + licenses = set() + for i, (file_path, lic, names) in enumerate(iterate_audiobooks(book, ())): index = i + 1 part_name = '_'.join(names) ext = file_path.rsplit('.', 1)[-1] zip_name = f'{book.slug}_{index:03d}_{part_name}'[:240] + '.' + ext zs.add_path(file_path, zip_name) + lic_name = constants.LICENSES.get(lic, {}).get('locative') + if lic_name: + licenses.add(lic_name) + + readme = render_to_string('catalogue/audiobook_zip_readme.txt', { + 'licenses': licenses, + }) + zs.add(readme, 'informacje.txt') + response = StreamingHttpResponse(zs, content_type='application/zip') response['Content-Disposition'] = f'attachment; filename={slug}_{media_format}.zip' return response -- 2.20.1 From 89881ffd8c2da92f115f23b002f48b7e626014d0 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 16 Jun 2026 14:45:10 +0200 Subject: [PATCH 15/16] Add audio bulk-download --- .../templates/catalogue/book_detail.html | 20 +++++++++---------- src/catalogue/views.py | 11 ++++++++-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/catalogue/templates/catalogue/book_detail.html b/src/catalogue/templates/catalogue/book_detail.html index 7074a81aa..69f81864b 100644 --- a/src/catalogue/templates/catalogue/book_detail.html +++ b/src/catalogue/templates/catalogue/book_detail.html @@ -263,7 +263,7 @@
- {% if book.has_mp3_file %} + {% if book.has_audio %}
- {% if book.has_ogg_file %} -
-
-

OggVorbis

-

{% trans "Otwarty format plików audio, oferujący wysokiej jakości nagranie." %}

-
-
- .ogg -
+
+
+

OggVorbis

+

{% trans "Otwarty format plików audio, oferujący wysokiej jakości nagranie." %}

- {% endif %} +
+ .ogg +
+
{% if book.has_daisy_file %}
diff --git a/src/catalogue/views.py b/src/catalogue/views.py index 8bb86c282..8d32e64ee 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -534,12 +534,19 @@ def stream_zip(request, media_format=None, slug=None): zs = ZipStream() + audiobook_list = list(iterate_audiobooks(book, ())) + licenses = set() - for i, (file_path, lic, names) in enumerate(iterate_audiobooks(book, ())): + for i, (file_path, lic, names) in enumerate(audiobook_list): index = i + 1 part_name = '_'.join(names) + if part_name: + part_name = '_' + part_name ext = file_path.rsplit('.', 1)[-1] - zip_name = f'{book.slug}_{index:03d}_{part_name}'[:240] + '.' + ext + if len(audiobook_list) > 1: + zip_name = f'{book.slug}_{index:03d}{part_name}'[:240] + '.' + ext + else: + zip_name = book.slug[:240] + '.' + ext zs.add_path(file_path, zip_name) lic_name = constants.LICENSES.get(lic, {}).get('locative') -- 2.20.1 From 3274152881e52cb7860c5610547c936fee934e0f Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 16 Jun 2026 14:48:47 +0200 Subject: [PATCH 16/16] enable audio dl button --- src/catalogue/templates/catalogue/book_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/catalogue/templates/catalogue/book_detail.html b/src/catalogue/templates/catalogue/book_detail.html index 69f81864b..4852c9a32 100644 --- a/src/catalogue/templates/catalogue/book_detail.html +++ b/src/catalogue/templates/catalogue/book_detail.html @@ -167,7 +167,7 @@
- {% if book.has_mp3_file %} + {% if book.has_audio %} {% endif %}
-- 2.20.1