From 53a66f70e4a8f9585b3d1a459542fb2e44f310f2 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 5 Feb 2025 10:23:24 +0100 Subject: [PATCH 01/16] fix for paralells --- .../templates/catalogue/book_text.html | 45 +++++---- .../static/2022/styles/layout/_text.scss | 95 +++++++++++++++++++ src/wolnelektury/static/js/book_text/menu.js | 64 +++++++++++-- 3 files changed, 177 insertions(+), 27 deletions(-) diff --git a/src/catalogue/templates/catalogue/book_text.html b/src/catalogue/templates/catalogue/book_text.html index dbe92591a..18e4bcc43 100644 --- a/src/catalogue/templates/catalogue/book_text.html +++ b/src/catalogue/templates/catalogue/book_text.html @@ -176,7 +176,29 @@
- {% with next=book.get_next_text prev=book.get_prev_text %} + + +{% with next=book.get_next_text prev=book.get_prev_text %} {% if next %} {{ next.title }} → {% endif %} @@ -281,28 +303,9 @@
+ - {% if book.other_versions.exists %} -
-

{% trans "Inne wersje utworu" %}

- {% trans "Zamknij drugą wersję" %} - -
- {% endif %} -
{% annoy_banners 'book-text-intermission' %} diff --git a/src/wolnelektury/static/2022/styles/layout/_text.scss b/src/wolnelektury/static/2022/styles/layout/_text.scss index 65bc48aef..1fde2b98a 100644 --- a/src/wolnelektury/static/2022/styles/layout/_text.scss +++ b/src/wolnelektury/static/2022/styles/layout/_text.scss @@ -294,6 +294,7 @@ h2 { h3, .subtitle2 { font-size: 1.5em; margin: 1.5em 0 1em 0; + padding-right: 48px; font-weight: normal; line-height: 1.5em; } @@ -1044,3 +1045,97 @@ background: #fff; } } } + + + +#sidebar { + position: absolute; + left: 0; + top: 20px; + width: 200px; + + h2 { + font-size: 20px; + margin-bottom: 1em; + } + + .other-text-close { + display: none; + } + + #other { + ul { + list-style: none; + margin: 0; + padding: 0; + + .book-mini-box { + position: relative; + width: 137px; + .language { + position: absolute; + top: 163px; + left: 10px; + color: white; + background: black; + padding: 0 10px; + border-radius: 10px; + font-size: 14px; + line-height: 20px; + + } + .author, .title { + display: block; + font-size: 14px; + line-height: 18px; + } + } + } + } +} +.with-other-text { + #sidebar { + .other-text-close { + display: block; + background: red; + padding: 10px; + margin: 10px 0; + border-radius: 10px; + color: white; + text-decoration: none; + } + } + #main-text { + #book-text { + .other { + overflow: hidden; + margin: 10px 40px 10px 50px; + padding: 10px 20px 0 10px; + background: #eee; + border-left: 10px double #ddd; + + h3 { + margin: 0; + padding: 0; + } + + .paragraph { + padding: 0; + } + } + } + } +} + +:lang(pl), +:lang(en), +:lang(de), +:lang(fr), +:lang(lt), +:lang(uk) +{ + direction: ltr; +} +:lang(he) { + direction: rtl; +} diff --git a/src/wolnelektury/static/js/book_text/menu.js b/src/wolnelektury/static/js/book_text/menu.js index eff64a0c6..dde96edf9 100644 --- a/src/wolnelektury/static/js/book_text/menu.js +++ b/src/wolnelektury/static/js/book_text/menu.js @@ -94,33 +94,85 @@ $("#menu a").each(function() { $("#menu-other").show(); + function insertOtherText(text) { + let tree = $(text); + let lang = tree.attr('lang') || 'pl'; + + // toc? + // themes? + + let cursor = $(".main-text-body #book-text").children().first(); + // wstawiamy przed kursorem + lastTarget = ''; + tree.children().each((i, e) => { + let $e = $(e); + + if ($e.hasClass('anchor')) return; + if ($e.hasClass('numeracja')) return; + if ($e.attr('id') == 'toc') return; + if ($e.attr('id') == 'nota_red') return; + if ($e.attr('id') == 'themes') return; + if ($e.attr('name') && $e.attr('name').startsWith('sec')) return; + + if ($e.hasClass('target')) { + let target = $e.attr('name'); + + while (lastTarget != target) { + let nc = cursor.next(); + if (!nc.length) { + break; + } + cursor = nc; + lastTarget = cursor.attr('name'); + } + + while (true) { + let nc = cursor.next(); + if (!nc.length) { + break; + } + cursor = nc; + lastTarget = cursor.attr('name'); + if (lastTarget) break; + } + + } else { + let d = $('
'); + d.attr('lang', lang); + d.append(e); + d.insertBefore(cursor); + } + }); + } + /* Load other version of text. */ $(".display-other").click(function(e) { e.preventDefault(); release_menu(); - $("#other-text").show(); + $(".other").remove(); $("body").addClass('with-other-text'); $.ajax($(this).attr('data-other'), { success: function(text) { - $("#other-text-body").html(text); + insertOtherText(text); $("#other-text-waiter").hide(); - $("#other-text-body").show(); - loaded_text($("#other-text-body")); + loaded_text($(".other")); } }); _paq.push(['trackEvent', 'html', 'other-text']); }); + + + /* Remove other version of text. */ $(".other-text-close").click(function(e) { release_menu(); e.preventDefault(); - $("#other-text").hide(); + $(".other").remove(); $("body").removeClass('with-other-text'); - $("#other-text-body").html(""); _paq.push(['trackEvent', 'html', 'other-text-close']); }); -- 2.20.1 From 63336c27e7cbe3a8a5df35acaad7d08d9ee4dfaf Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 5 Feb 2025 11:45:27 +0100 Subject: [PATCH 02/16] lib --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f80322c62..fd78c46d1 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -39,7 +39,7 @@ mutagen==1.47 sorl-thumbnail==12.10.0 # home-brewed & dependencies -librarian==24.5.4 +librarian==24.5.5 # celery tasks celery[redis]==5.4.0 -- 2.20.1 From 17af7241ca7724df39d9d589245d818f689d97b5 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 5 Feb 2025 15:36:46 +0100 Subject: [PATCH 03/16] lib --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index fd78c46d1..c3934fea4 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -39,7 +39,7 @@ mutagen==1.47 sorl-thumbnail==12.10.0 # home-brewed & dependencies -librarian==24.5.5 +librarian==24.5.6 # celery tasks celery[redis]==5.4.0 -- 2.20.1 From 605af0880e55eebe3b5db50c97712c9e17448579 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 24 Feb 2025 16:41:05 +0100 Subject: [PATCH 04/16] registration --- .../migrations/0008_alter_token_token_type.py | 18 +++++++ src/api/serializers.py | 6 ++- src/api/views.py | 49 +++++++++++++++++-- .../migrations/0017_userconfirmation.py | 25 ++++++++++ src/social/models.py | 30 ++++++++++++ .../templates/social/user_confirmation.html | 12 +++++ src/social/urls.py | 1 + src/social/views.py | 11 ++++- 8 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 src/api/migrations/0008_alter_token_token_type.py create mode 100644 src/social/migrations/0017_userconfirmation.py create mode 100644 src/social/templates/social/user_confirmation.html diff --git a/src/api/migrations/0008_alter_token_token_type.py b/src/api/migrations/0008_alter_token_token_type.py new file mode 100644 index 000000000..b2ba7bab8 --- /dev/null +++ b/src/api/migrations/0008_alter_token_token_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.8 on 2025-02-24 15:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0007_alter_token_consumer'), + ] + + operations = [ + migrations.AlterField( + model_name='token', + name='token_type', + field=models.IntegerField(choices=[(1, 'Request'), (2, 'Access'), (3, 'Refresh')]), + ), + ] diff --git a/src/api/serializers.py b/src/api/serializers.py index 4ba660e26..8c008926d 100644 --- a/src/api/serializers.py +++ b/src/api/serializers.py @@ -37,8 +37,12 @@ class LoginSerializer(serializers.Serializer): class RegisterSerializer(serializers.Serializer): email = serializers.CharField() password = serializers.CharField(style={'input_type': 'password'}) - options = serializers.ListField(child=serializers.IntegerField()) + options = serializers.ListField(child=serializers.IntegerField(), required=False) class RefreshTokenSerializer(serializers.Serializer): refresh_token = serializers.CharField(style={'input_type': 'password'}) + + +class RequestConfirmSerializer(serializers.Serializer): + email = serializers.CharField() diff --git a/src/api/views.py b/src/api/views.py index 08d1650e7..0451c7760 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -4,6 +4,7 @@ from time import time from django.contrib.auth import authenticate from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User from django import forms from django.http import HttpResponse from django.http import Http404 @@ -18,6 +19,7 @@ from rest_framework.views import APIView from rest_framework.generics import GenericAPIView, RetrieveAPIView, get_object_or_404 from catalogue.models import Book from .models import BookUserData, KEY_SIZE, SECRET_SIZE, Token +from social.models import UserConfirmation from . import serializers from .request_validator import PistonRequestValidator from .utils import oauthlib_request, oauthlib_response, vary_on_auth @@ -227,7 +229,7 @@ class BlogView(APIView): -class RegisterView(APIView): +class RegisterView(GenericAPIView): serializer_class = serializers.RegisterSerializer def get(self, request): @@ -245,8 +247,30 @@ class RegisterView(APIView): }) def post(self, request): - pass - + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + d = serializer.validated_data + + user = User( + username=d['email'], + email=d['email'], + is_active=False + ) + user.set_password(d['password']) + + try: + user.save() + except: + return Response( + { + "detail": "Nie można utworzyć konta.", + }, + status=400 + ) + + UserConfirmation.request(user) + return Response({}) + class RefreshTokenView(APIView): serializer_class = serializers.RefreshTokenSerializer @@ -284,4 +308,21 @@ class RefreshTokenView(APIView): class RequestConfirmView(APIView): - pass + serializer_class = serializers.RequestConfirmSerializer + + def post(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + d = serializer.validated_data + + try: + user = User.objects.get( + username=d['email'], + is_active=False + ) + except User.DoesNotExist: + raise Http404 + + UserConfirmation.request(user) + return Response({}) + diff --git a/src/social/migrations/0017_userconfirmation.py b/src/social/migrations/0017_userconfirmation.py new file mode 100644 index 000000000..fe39336e1 --- /dev/null +++ b/src/social/migrations/0017_userconfirmation.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.8 on 2025-02-24 15:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('social', '0016_alter_bannergroup_options_alter_carousel_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='UserConfirmation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('key', models.CharField(max_length=128, unique=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/social/models.py b/src/social/models.py index cb1326b15..17fe7d07a 100644 --- a/src/social/models.py +++ b/src/social/models.py @@ -1,10 +1,13 @@ # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Wolne Lektury. See NOTICE for more information. # +from oauthlib.common import urlencode, generate_token from random import randint from django.db import models from django.conf import settings +from django.contrib.auth.models import User from django.core.exceptions import ValidationError +from django.core.mail import send_mail from django.urls import reverse from catalogue.models import Book from wolnelektury.utils import cached_render, clear_cached_renders @@ -170,3 +173,30 @@ class CarouselItem(models.Model): def get_banner(self): return self.banner or self.banner_group.get_banner() + + +class UserConfirmation(models.Model): + user = models.ForeignKey(User, models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + key = models.CharField(max_length=128, unique=True) + + def send(self): + send_mail( + 'Potwierdź konto w bibliotece Wolne Lektury', + f'https://beta.wolnelektury.pl/ludzie/potwierdz/{self.key}/', + settings.CONTACT_EMAIL, + [self.user.email] + ) + + def use(self): + user = self.user + user.is_active = True + user.save() + self.delete() + + @classmethod + def request(cls, user): + cls.objects.create( + user=user, + key=generate_token() + ).send() diff --git a/src/social/templates/social/user_confirmation.html b/src/social/templates/social/user_confirmation.html new file mode 100644 index 000000000..f56d2f259 --- /dev/null +++ b/src/social/templates/social/user_confirmation.html @@ -0,0 +1,12 @@ +{% extends "base_simple.html" %} +{% load i18n %} + + +{% block body %} +

{% trans "Konto potwierdzone" %}

+ +

+ {% blocktrans with user=user %}Konto {{ user }} zostało potwierdzone. Możesz się teraz zalogować.{% endblocktrans %} +

+{% endblock body %} + diff --git a/src/social/urls.py b/src/social/urls.py index 299f62058..10d9240d2 100644 --- a/src/social/urls.py +++ b/src/social/urls.py @@ -7,6 +7,7 @@ from . import views urlpatterns = [ + path('potwierdz//', views.confirm_user, name='social_confirm_user'), path('lektura//lubie/', views.like_book, name='social_like_book'), path('dodaj-tag/', views.AddSetView.as_view(), name='social_add_set_tag'), path('usun-tag/', views.RemoveSetView.as_view(), name='social_remove_set_tag'), diff --git a/src/social/views.py b/src/social/views.py index 989771a6c..3dfcd9e2b 100644 --- a/src/social/views.py +++ b/src/social/views.py @@ -10,7 +10,7 @@ from django.views.generic.edit import FormView from catalogue.models import Book, Tag import catalogue.models.tag -from social import forms +from social import forms, models from wolnelektury.utils import is_ajax @@ -125,3 +125,12 @@ def my_tags(request): t.name for t in tags ], safe=False ) + + +def confirm_user(request, key): + uc = get_object_or_404(models.UserConfirmation, key=key) + user = uc.user + uc.use() + return render(request, 'social/user_confirmation.html', { + 'user': user, + }) -- 2.20.1 From 27540194355a3118d8330262b6312b3f280f7b9a Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 26 Feb 2025 16:12:02 +0100 Subject: [PATCH 05/16] api for partner --- requirements/requirements.txt | 2 +- src/catalogue/api/serializers.py | 31 +++++++++++++++++++ src/catalogue/api/urls2.py | 4 +++ src/catalogue/api/views.py | 12 +++++++ src/catalogue/fields.py | 20 ++++++++++++ ...le_book_html_nonotes_file_etag_and_more.py | 30 ++++++++++++++++++ src/catalogue/models/book.py | 9 +++++- 7 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/catalogue/migrations/0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more.py diff --git a/requirements/requirements.txt b/requirements/requirements.txt index c3934fea4..898a609ae 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -39,7 +39,7 @@ mutagen==1.47 sorl-thumbnail==12.10.0 # home-brewed & dependencies -librarian==24.5.6 +librarian==24.5.7 # celery tasks celery[redis]==5.4.0 diff --git a/src/catalogue/api/serializers.py b/src/catalogue/api/serializers.py index 5f96e0270..3e3874d3f 100644 --- a/src/catalogue/api/serializers.py +++ b/src/catalogue/api/serializers.py @@ -158,6 +158,37 @@ class BookSerializer2(serializers.ModelSerializer): 'isbn_pdf', 'isbn_epub', 'isbn_mobi', ] +class BookSerializer11Labs(serializers.ModelSerializer): + url = AbsoluteURLField() + href = AbsoluteURLField(view_name='catalogue_api_book', view_args=['slug']) + html = EmbargoURLField(source='html_nonotes_url') + + authors = AuthorItemSerializer(many=True) + translators = AuthorItemSerializer(many=True) + epochs = EpochItemSerializer(many=True) + genres = GenreItemSerializer(many=True) + kinds = KindItemSerializer(many=True) + parent = serializers.HyperlinkedRelatedField( + read_only=True, + view_name='catalogue_api_book', + lookup_field='slug' + ) + + class Meta: + model = Book + fields = [ + 'slug', 'title', 'full_sort_key', + 'href', 'url', 'language', + 'authors', 'translators', + 'epochs', 'genres', 'kinds', + #'children', + 'parent', 'preview', + 'html', + 'cover_thumb', 'cover', + 'isbn_pdf', 'isbn_epub', 'isbn_mobi', + ] + + class BookSerializer(LegacyMixin, serializers.ModelSerializer): author = serializers.CharField(source='author_unicode') kind = serializers.CharField(source='kind_unicode') diff --git a/src/catalogue/api/urls2.py b/src/catalogue/api/urls2.py index 8748e2cf1..164cdca39 100644 --- a/src/catalogue/api/urls2.py +++ b/src/catalogue/api/urls2.py @@ -11,6 +11,10 @@ urlpatterns = [ piwik_track_view(views.BookList2.as_view()), name='catalogue_api_book_list' ), + path('11labs/books/', + piwik_track_view(views.BookList11Labs.as_view()), + name='catalogue_api_book_list' + ), path('books//', piwik_track_view(views.BookDetail2.as_view()), name='catalogue_api_book' diff --git a/src/catalogue/api/views.py b/src/catalogue/api/views.py index 9c8f4f177..b55b21451 100644 --- a/src/catalogue/api/views.py +++ b/src/catalogue/api/views.py @@ -196,6 +196,18 @@ class BookList2(ListAPIView): return books +class BookList11Labs(BookList2): + serializer_class = serializers.BookSerializer11Labs + + def get_queryset(self): + books = Book.objects.all() + books = books.filter(findable=True) + books = books.filter(license='') + books = order_books(books, True) + + return books + + @vary_on_auth # Because of 'liked'. class BookDetail(RetrieveAPIView): queryset = Book.objects.all() diff --git a/src/catalogue/fields.py b/src/catalogue/fields.py index 9c5696fa6..2d35357eb 100644 --- a/src/catalogue/fields.py +++ b/src/catalogue/fields.py @@ -398,6 +398,26 @@ class HtmlField(EbookField): return wldoc.as_html(gallery_path=gal_path, gallery_url=gal_url, base_url=absolute_url(gal_url)) +class HtmlNonotesField(EbookField): + ext = 'html' + for_parents = False + directory = 'html_nonotes' + + @staticmethod + def transform(wldoc, book): + # ugly, but we can't use wldoc.book_info here + from librarian import DCNS + url_elem = wldoc.edoc.getroot().find('.//' + DCNS('identifier.url')) + if url_elem is None: + gal_url = '' + gal_path = '' + else: + slug = url_elem.text.rstrip('/').rsplit('/', 1)[1] + gal_url = gallery_url(slug=slug) + gal_path = gallery_path(slug=slug) + return wldoc.as_html(gallery_path=gal_path, gallery_url=gal_url, base_url=absolute_url(gal_url), flags=['nonotes']) + + class CoverField(EbookField): ext = 'jpg' directory = 'cover' diff --git a/src/catalogue/migrations/0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more.py b/src/catalogue/migrations/0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more.py new file mode 100644 index 000000000..bd297d4c6 --- /dev/null +++ b/src/catalogue/migrations/0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.0.8 on 2025-02-26 14:46 + +import catalogue.fields +from django.db import migrations, models +import fnpdjango.storage + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0048_remove_collection_kind_remove_tag_for_books_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='html_nonotes_file', + field=catalogue.fields.HtmlNonotesField(etag_field_name='html_nonotes_file_etag', storage=fnpdjango.storage.BofhFileSystemStorage()), + ), + migrations.AddField( + model_name='book', + name='html_nonotes_file_etag', + field=models.CharField(db_index=True, default='', editable=False, max_length=255), + ), + migrations.AddField( + model_name='book', + name='license', + field=models.CharField(blank=True, db_index=True, max_length=255, verbose_name='licencja'), + ), + ] diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 29e375464..4080cc269 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -43,6 +43,7 @@ class Book(models.Model): common_slug = models.SlugField('wspólny slug', max_length=120, db_index=True) language = models.CharField('kod języka', max_length=3, db_index=True, default=app_settings.DEFAULT_LANGUAGE) description = models.TextField('opis', blank=True) + license = models.CharField('licencja', max_length=255, blank=True, db_index=True) abstract = models.TextField('abstrakt', blank=True) toc = models.TextField('spis treści', blank=True) created_at = models.DateTimeField('data utworzenia', auto_now_add=True, db_index=True) @@ -62,6 +63,7 @@ class Book(models.Model): # files generated during publication xml_file = fields.XmlField(storage=bofh_storage, with_etag=False) html_file = fields.HtmlField(storage=bofh_storage) + html_nonotes_file = fields.HtmlNonotesField(storage=bofh_storage) fb2_file = fields.Fb2Field(storage=bofh_storage) txt_file = fields.TxtField(storage=bofh_storage) epub_file = fields.EpubField(storage=bofh_storage) @@ -79,7 +81,7 @@ class Book(models.Model): 'okładka dla Ebookpoint') ebook_formats = constants.EBOOK_FORMATS - formats = ebook_formats + ['html', 'xml'] + formats = ebook_formats + ['html', 'xml', 'html_nonotes'] parent = models.ForeignKey('self', models.CASCADE, blank=True, null=True, related_name='children') ancestor = models.ManyToManyField('self', blank=True, editable=False, related_name='descendant', symmetrical=False) @@ -375,6 +377,9 @@ class Book(models.Model): def html_url(self): return self.media_url('html') + def html_nonotes_url(self): + return self.media_url('html_nonotes') + def pdf_url(self): return self.media_url('pdf') @@ -635,6 +640,7 @@ class Book(models.Model): book.findable = findable book.language = book_info.language book.title = book_info.title + book.license = book_info.license if book_info.variant_of: book.common_slug = book_info.variant_of.slug else: @@ -704,6 +710,7 @@ class Book(models.Model): for format_ in constants.EBOOK_FORMATS_WITH_CHILDREN: if format_ not in dont_build: getattr(book, '%s_file' % format_).build_delay() + book.html_nonotes_file.build_delay() if not settings.NO_SEARCH_INDEX and search_index and findable: tasks.index_book.delay(book.id) -- 2.20.1 From f9a95412f4923ce7b1b5b8844f314bed9a22142e Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 26 Feb 2025 16:15:18 +0100 Subject: [PATCH 06/16] feature switch for api register --- src/api/views.py | 8 ++++++++ src/wolnelektury/settings/custom.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/api/views.py b/src/api/views.py index 0451c7760..011161e52 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -2,6 +2,7 @@ # Copyright © Fundacja Wolne Lektury. See NOTICE for more information. # from time import time +from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User @@ -247,6 +248,13 @@ class RegisterView(GenericAPIView): }) def post(self, request): + if not settings.FEATURE_API_REGISTER: + return Response( + { + "detail": "Rejestracja aktualnie niedostępna." + }, + status=400 + ) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) d = serializer.validated_data diff --git a/src/wolnelektury/settings/custom.py b/src/wolnelektury/settings/custom.py index 16b5e0a4f..d67377268 100644 --- a/src/wolnelektury/settings/custom.py +++ b/src/wolnelektury/settings/custom.py @@ -76,3 +76,5 @@ SEARCH_CONFIG_SIMPLE = 'simple' SEARCH_USE_UNACCENT = False FEATURE_SYNCHRO = False + +FEATURE_API_REGISTER = False -- 2.20.1 From f9210448beac9bce1c9b971d38e8533fe7da1b82 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 3 Mar 2025 09:03:13 +0100 Subject: [PATCH 07/16] fix --- src/catalogue/models/book.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 4080cc269..c2b9eeddf 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -640,7 +640,7 @@ class Book(models.Model): book.findable = findable book.language = book_info.language book.title = book_info.title - book.license = book_info.license + book.license = book_info.license or '' if book_info.variant_of: book.common_slug = book_info.variant_of.slug else: -- 2.20.1 From e16f5c5d7985b8e501c6810abf5e540326e392f1 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 3 Mar 2025 14:01:58 +0100 Subject: [PATCH 08/16] downloading receipts --- src/club/models.py | 29 ++++++++++++++----- .../admin/club/schedule/change_list.html | 11 +++++++ src/club/urls.py | 2 ++ src/club/views.py | 22 +++++++++++++- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/club/models.py b/src/club/models.py index c09769a95..c40428def 100644 --- a/src/club/models.py +++ b/src/club/models.py @@ -417,7 +417,8 @@ class PayUOrder(payu_models.Order): ) @classmethod - def send_receipt(cls, email, year, resend=False): + def generate_receipt(cls, email, year): + # TODO: abstract out Contact = apps.get_model('messaging', 'Contact') Funding = apps.get_model('funding', 'Funding') BillingAgreement = apps.get_model('paypal', 'BillingAgreement') @@ -485,11 +486,8 @@ class PayUOrder(payu_models.Order): ctx = { "email": email, "year": year, - "next_year": year + 1, "total": sum(x['amount'] for x in payments), "payments": payments, - "optout": optout, - "resend": resend, } temp = tempfile.NamedTemporaryFile(prefix='receipt-', suffix='.pdf', delete=False) temp.close() @@ -497,15 +495,32 @@ class PayUOrder(payu_models.Order): "wl.eps": os.path.join(settings.STATIC_ROOT, "img/wl.eps"), }) + with open(temp.name, 'rb') as f: + content = f.read() + os.unlink(f.name) + return content, optout, payments + + @classmethod + def send_receipt(cls, email, year, resend=False): + receipt = cls.generate_receipt(email, year) + if receipt: + content, optout, payments = receipt + ctx = { + "email": email, + "year": year, + "next_year": year + 1, + "total": sum(x['amount'] for x in payments), + "payments": payments, + "optout": optout, + "resend": resend, + } message = EmailMessage( 'Odlicz darowiznę na Wolne Lektury od podatku', template.loader.render_to_string('club/receipt_email.txt', ctx), settings.CLUB_CONTACT_EMAIL, [email] ) - with open(temp.name, 'rb') as f: - message.attach('wolnelektury-darowizny.pdf', f.read(), 'application/pdf') + message.attach('wolnelektury-darowizny.pdf', content, 'application/pdf') message.send() - os.unlink(f.name) class PayUCardToken(payu_models.CardToken): diff --git a/src/club/templates/admin/club/schedule/change_list.html b/src/club/templates/admin/club/schedule/change_list.html index 1b6de0814..e47cd7436 100644 --- a/src/club/templates/admin/club/schedule/change_list.html +++ b/src/club/templates/admin/club/schedule/change_list.html @@ -2,6 +2,7 @@ {% load club %} {% block content %} +
@@ -19,5 +20,15 @@
Aktywne miesięczne wpłaty cykliczne:{% club_active_30day_sum %} zł.
+
+
+ {% csrf_token %} + Pobierz zestawienie roczne + + + +
+
+
{{ block.super }} {% endblock content %} diff --git a/src/club/urls.py b/src/club/urls.py index 026758070..87bfa5a90 100644 --- a/src/club/urls.py +++ b/src/club/urls.py @@ -28,4 +28,6 @@ urlpatterns = [ path('notify//', views.PayUNotifyView.as_view(), name='club_payu_notify'), path('weryfikacja/', views.member_verify, name='club_member_verify'), + + path('potwierdzenie/', views.receipt, name='club_receipt'), ] diff --git a/src/club/views.py b/src/club/views.py index c6136b67f..1ae16ce11 100644 --- a/src/club/views.py +++ b/src/club/views.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required, permission_required from django.db.models import Sum -from django.http import HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.utils.decorators import method_decorator @@ -249,3 +249,23 @@ def member_verify(request): 'result': rows } ) + + +@permission_required('club.schedule_view') +def receipt(request): + email = request.POST.get('email') + year = int(request.POST.get('year')) + + receipt = models.PayUOrder.generate_receipt(email, year) + if receipt: + content, optout, payments = receipt + if not content: + return HttpResponse('no content') + return HttpResponse( + content, + headers={ + "Content-Type": "application/pdf", + "Content-Disposition": f'attachment; filename="wolnelektury-{year}-{email}.pdf"', + } + ) + -- 2.20.1 From 7d079902b5d0e8507b17853b8aaa87cf6e2c646d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 4 Mar 2025 14:11:09 +0100 Subject: [PATCH 09/16] fix --- src/club/templates/admin/club/schedule/change_list.html | 4 ++-- src/club/views.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/club/templates/admin/club/schedule/change_list.html b/src/club/templates/admin/club/schedule/change_list.html index e47cd7436..3d9cd55eb 100644 --- a/src/club/templates/admin/club/schedule/change_list.html +++ b/src/club/templates/admin/club/schedule/change_list.html @@ -24,8 +24,8 @@
{% csrf_token %} Pobierz zestawienie roczne - - + +
diff --git a/src/club/views.py b/src/club/views.py index 1ae16ce11..221ee9ee6 100644 --- a/src/club/views.py +++ b/src/club/views.py @@ -254,7 +254,10 @@ def member_verify(request): @permission_required('club.schedule_view') def receipt(request): email = request.POST.get('email') - year = int(request.POST.get('year')) + try: + year = int(request.POST.get('year')) + except: + return HttpResponse('no content') receipt = models.PayUOrder.generate_receipt(email, year) if receipt: -- 2.20.1 From edd562d25bf18c4faf784cf67551af1eb062885b Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 4 Mar 2025 14:20:42 +0100 Subject: [PATCH 10/16] fix --- src/club/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/club/views.py b/src/club/views.py index 221ee9ee6..b2657e62f 100644 --- a/src/club/views.py +++ b/src/club/views.py @@ -262,7 +262,7 @@ def receipt(request): receipt = models.PayUOrder.generate_receipt(email, year) if receipt: content, optout, payments = receipt - if not content: + else: return HttpResponse('no content') return HttpResponse( content, -- 2.20.1 From ea53c59131b640ede6e5a00352b3158700a122c9 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 4 Mar 2025 15:31:39 +0100 Subject: [PATCH 11/16] book filtering --- requirements/requirements.txt | 1 + src/api/pagination.py | 3 +++ src/catalogue/api/views.py | 17 +++++++++++++++++ src/wolnelektury/settings/apps.py | 1 + 4 files changed, 22 insertions(+) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 898a609ae..10498cb2b 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -14,6 +14,7 @@ django-modeltranslation==0.18.12 django-allauth==0.51 django-extensions==3.2.3 djangorestframework==3.15.1 +django-filter==23.5 djangorestframework-xml==2.0.0 django-admin-ordering==0.18 django-countries==7.6.1 diff --git a/src/api/pagination.py b/src/api/pagination.py index 0c4ae09d4..04366220c 100644 --- a/src/api/pagination.py +++ b/src/api/pagination.py @@ -3,6 +3,9 @@ from rest_framework.response import Response class WLLimitOffsetPagination(LimitOffsetPagination): + def get_results(self, data): + return data['member'] + def get_paginated_response(self, data): return Response({ "member": data, diff --git a/src/catalogue/api/views.py b/src/catalogue/api/views.py index b55b21451..5afec171a 100644 --- a/src/catalogue/api/views.py +++ b/src/catalogue/api/views.py @@ -9,6 +9,7 @@ from django.core.files.base import ContentFile from django.http import Http404, HttpResponse from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache +from django_filters import rest_framework as filters from rest_framework.generics import (ListAPIView, RetrieveAPIView, RetrieveUpdateAPIView, get_object_or_404) from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly @@ -183,10 +184,26 @@ class BookList(LegacyListAPIView): return Response({}, status=status.HTTP_201_CREATED) +class BookFilter(filters.FilterSet): + sort = filters.OrderingFilter( + fields=( + ('sort_key_author', 'alpha'), + ('popularity', 'popularity'), + ) + ) + tag = filters.ModelMultipleChoiceFilter( + field_name='tag_relations__tag', + queryset=Tag.objects.filter(category__in=('author', 'epoch', 'genre', 'kind')), + conjoined=True, + ) + + class BookList2(ListAPIView): permission_classes = [DjangoModelPermissionsOrAnonReadOnly] queryset = Book.objects.none() # Required for DjangoModelPermissions serializer_class = serializers.BookSerializer2 + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = BookFilter def get_queryset(self): books = Book.objects.all() diff --git a/src/wolnelektury/settings/apps.py b/src/wolnelektury/settings/apps.py index d4faa8bfb..b9d507e86 100644 --- a/src/wolnelektury/settings/apps.py +++ b/src/wolnelektury/settings/apps.py @@ -59,6 +59,7 @@ INSTALLED_APPS_CONTRIB = [ 'django.contrib.postgres', 'admin_ordering', 'rest_framework', + 'django_filters', 'fnp_django_pagination', 'pipeline', 'sorl.thumbnail', -- 2.20.1 From 774d79294bdeab0d9d841b9775f696cdbb334c4d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 4 Mar 2025 15:53:59 +0100 Subject: [PATCH 12/16] book search --- src/catalogue/api/views.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/catalogue/api/views.py b/src/catalogue/api/views.py index 5afec171a..dac1968ad 100644 --- a/src/catalogue/api/views.py +++ b/src/catalogue/api/views.py @@ -9,7 +9,8 @@ from django.core.files.base import ContentFile from django.http import Http404, HttpResponse from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache -from django_filters import rest_framework as filters +from django_filters import rest_framework as dfilters +from rest_framework import filters from rest_framework.generics import (ListAPIView, RetrieveAPIView, RetrieveUpdateAPIView, get_object_or_404) from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly @@ -184,14 +185,14 @@ class BookList(LegacyListAPIView): return Response({}, status=status.HTTP_201_CREATED) -class BookFilter(filters.FilterSet): - sort = filters.OrderingFilter( +class BookFilter(dfilters.FilterSet): + sort = dfilters.OrderingFilter( fields=( ('sort_key_author', 'alpha'), ('popularity', 'popularity'), ) ) - tag = filters.ModelMultipleChoiceFilter( + tag = dfilters.ModelMultipleChoiceFilter( field_name='tag_relations__tag', queryset=Tag.objects.filter(category__in=('author', 'epoch', 'genre', 'kind')), conjoined=True, @@ -202,8 +203,14 @@ class BookList2(ListAPIView): permission_classes = [DjangoModelPermissionsOrAnonReadOnly] queryset = Book.objects.none() # Required for DjangoModelPermissions serializer_class = serializers.BookSerializer2 - filter_backends = (filters.DjangoFilterBackend,) + filter_backends = ( + dfilters.DjangoFilterBackend, + filters.SearchFilter, + ) filterset_class = BookFilter + search_fields = [ + 'title', + ] def get_queryset(self): books = Book.objects.all() -- 2.20.1 From adc204acad4f4e8eacf67faa0ca9b3d916cd3c91 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 7 Mar 2025 12:11:36 +0100 Subject: [PATCH 13/16] Remove stale links. --- src/pdcounter/templates/pdcounter/author_detail.html | 8 ++++---- src/pdcounter/templates/pdcounter/book_detail.html | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pdcounter/templates/pdcounter/author_detail.html b/src/pdcounter/templates/pdcounter/author_detail.html index 8e3153a2d..2e3adb86e 100644 --- a/src/pdcounter/templates/pdcounter/author_detail.html +++ b/src/pdcounter/templates/pdcounter/author_detail.html @@ -25,8 +25,8 @@ {% if author.alive %}

{% trans "Dzieła tego autora objęte są prawem autorskim." %} - {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %} - Dowiedz się, dlaczego biblioteki internetowe nie mogą + {% blocktrans trimmed %} + Biblioteki internetowe nie mogą udostępniać dzieł tego autora. {% endblocktrans %}

@@ -46,8 +46,8 @@

- {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %} - Dowiedz się, dlaczego biblioteki internetowe nie mogą + {% blocktrans trimmed %} + Biblioteki internetowe nie mogą udostępniać dzieł tego autora. {% endblocktrans %}

diff --git a/src/pdcounter/templates/pdcounter/book_detail.html b/src/pdcounter/templates/pdcounter/book_detail.html index 655d7f045..02a099da5 100644 --- a/src/pdcounter/templates/pdcounter/book_detail.html +++ b/src/pdcounter/templates/pdcounter/book_detail.html @@ -40,16 +40,16 @@ {% endblocktrans %}

- {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %} - Dowiedz się, dlaczego biblioteki internetowe nie mogą + {% blocktrans trimmed %} + Biblioteki internetowe nie mogą udostępniać tego utworu. {% endblocktrans %}

{% else %}

{% trans "Ten utwór objęty jest prawem autorskim." %} - {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %} - Dowiedz się, dlaczego biblioteki internetowe nie mogą + {% blocktrans trimmed %} + Biblioteki internetowe nie mogą udostępniać tego utworu. {% endblocktrans %}

-- 2.20.1 From ab9b52b00d9c995e2b2036f32270841bfebb11b2 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 10 Mar 2025 09:43:43 +0100 Subject: [PATCH 14/16] resize banner on small screens --- .../static/2022/styles/layout/_annoy.scss | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/wolnelektury/static/2022/styles/layout/_annoy.scss b/src/wolnelektury/static/2022/styles/layout/_annoy.scss index e91d58535..70704f34d 100644 --- a/src/wolnelektury/static/2022/styles/layout/_annoy.scss +++ b/src/wolnelektury/static/2022/styles/layout/_annoy.scss @@ -51,7 +51,7 @@ .annoy-banner-off { font-size: 1.2rem !important; } - + @include rwd($break-flow) { .annoy-banner-inner { padding: 0; @@ -69,7 +69,7 @@ } .annoy-banner-off { - + } } } @@ -89,10 +89,14 @@ &.annoy-banner_top-container { padding: 16px 0; + + @media screen and (max-height: 700px) { + padding: 5px 0; + } } &.annoy-banner_crisis-container { height: 160px; - + @media screen and (min-height: 480px) { height: 33vh; top: calc(-33vh + 160px); @@ -146,11 +150,29 @@ } } + @media screen and (max-height: 700px) { + flex-direction: row; + justify-content: space-between; + p { + font-size: .9em; + } + } + + .text { background: #edc016; padding: 1em; border: 3px solid black; } + + @media screen and (max-height: 400px) { + align-items: center; + .text { + height: 1.2em; + overflow: hidden; + } + } + a { color: #c32721; } @@ -193,6 +215,13 @@ display: block; transition: background-color .2s; + @media screen and (max-height: 700px) { + font-size: .9em; + } + @media screen and (max-height: 400px) { + white-space: nowrap; + } + &:hover { background: #ffd430; text-decoration: none; -- 2.20.1 From 1501fe79a5abe6fc214309ade4ceb85f6bfd0328 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 10 Mar 2025 15:16:17 +0100 Subject: [PATCH 15/16] filters api --- src/catalogue/api/serializers.py | 6 ++++++ src/catalogue/api/urls2.py | 5 +++++ src/catalogue/api/views.py | 11 +++++++++++ 3 files changed, 22 insertions(+) diff --git a/src/catalogue/api/serializers.py b/src/catalogue/api/serializers.py index 3e3874d3f..60e52a052 100644 --- a/src/catalogue/api/serializers.py +++ b/src/catalogue/api/serializers.py @@ -350,3 +350,9 @@ class FragmentDetailSerializer(serializers.ModelSerializer): class Meta: model = Fragment fields = ['book', 'anchor', 'text', 'url', 'themes'] + + +class FilterTagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = ['id', 'category', 'name'] diff --git a/src/catalogue/api/urls2.py b/src/catalogue/api/urls2.py index 164cdca39..b16af6651 100644 --- a/src/catalogue/api/urls2.py +++ b/src/catalogue/api/urls2.py @@ -20,6 +20,11 @@ urlpatterns = [ name='catalogue_api_book' ), + path('suggested-tags/', + piwik_track_view(views.SuggestedTags.as_view()), + name='catalogue_api_suggested_tags' + ), + path('authors/', piwik_track_view(views.AuthorList.as_view()), name="catalogue_api_author_list"), diff --git a/src/catalogue/api/views.py b/src/catalogue/api/views.py index dac1968ad..0e758b15e 100644 --- a/src/catalogue/api/views.py +++ b/src/catalogue/api/views.py @@ -19,6 +19,7 @@ from rest_framework import status from api.handlers import read_tags from api.utils import vary_on_auth from catalogue.forms import BookImportForm +from catalogue.helpers import get_top_level_related_tags from catalogue.models import Book, Collection, Tag, Fragment, BookMedia from catalogue.models.tag import prefetch_relations from club.models import Membership @@ -493,3 +494,13 @@ class FragmentView(RetrieveAPIView): book__slug=self.kwargs['book'], anchor=self.kwargs['anchor'] ) + + +class SuggestedTags(ListAPIView): + serializer_class = serializers.FilterTagSerializer + + def get_queryset(self): + tag_ids = self.request.GET.getlist('tag', []) + tags = [get_object_or_404(Tag, id=tid) for tid in tag_ids] + related_tags = list(t.id for t in get_top_level_related_tags(tags)) + return Tag.objects.filter(id__in=related_tags) -- 2.20.1 From 88a41b1939e8d8f3adef9d2100d43654dfb5e238 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 14 Mar 2025 09:05:15 +0100 Subject: [PATCH 16/16] update --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 10498cb2b..084f0da8d 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -40,7 +40,7 @@ mutagen==1.47 sorl-thumbnail==12.10.0 # home-brewed & dependencies -librarian==24.5.7 +librarian==24.5.8 # celery tasks celery[redis]==5.4.0 -- 2.20.1