filters api master
authorRadek Czajka <rczajka@rczajka.pl>
Mon, 10 Mar 2025 14:16:17 +0000 (15:16 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Mon, 10 Mar 2025 14:16:17 +0000 (15:16 +0100)
31 files changed:
requirements/requirements.txt
src/annoy/templates/annoy/banner_top.html [new file with mode: 0644]
src/annoy/templatetags/annoy.py
src/api/migrations/0008_alter_token_token_type.py [new file with mode: 0644]
src/api/pagination.py
src/api/serializers.py
src/api/views.py
src/catalogue/api/serializers.py
src/catalogue/api/urls2.py
src/catalogue/api/views.py
src/catalogue/fields.py
src/catalogue/migrations/0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more.py [new file with mode: 0644]
src/catalogue/models/book.py
src/catalogue/templates/catalogue/book_text.html
src/club/models.py
src/club/templates/admin/club/schedule/change_list.html
src/club/urls.py
src/club/views.py
src/pdcounter/templates/pdcounter/author_detail.html
src/pdcounter/templates/pdcounter/book_detail.html
src/social/migrations/0017_userconfirmation.py [new file with mode: 0644]
src/social/models.py
src/social/templates/social/user_confirmation.html [new file with mode: 0644]
src/social/urls.py
src/social/views.py
src/wolnelektury/settings/apps.py
src/wolnelektury/settings/custom.py
src/wolnelektury/static/2022/styles/layout/_annoy.scss
src/wolnelektury/static/2022/styles/layout/_text.scss
src/wolnelektury/static/js/book_text/menu.js
src/wolnelektury/templates/header.html

index 6b650e3..10498cb 100644 (file)
@@ -14,6 +14,7 @@ django-modeltranslation==0.18.12
 django-allauth==0.51
 django-extensions==3.2.3
 djangorestframework==3.15.1
 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
 djangorestframework-xml==2.0.0
 django-admin-ordering==0.18
 django-countries==7.6.1
@@ -34,12 +35,12 @@ python-fb==0.2
 
 Feedparser==6.0.11
 
 
 Feedparser==6.0.11
 
-Pillow==10.4
+Pillow==9.5.0
 mutagen==1.47
 sorl-thumbnail==12.10.0
 
 # home-brewed & dependencies
 mutagen==1.47
 sorl-thumbnail==12.10.0
 
 # home-brewed & dependencies
-librarian==24.5.4
+librarian==24.5.7
 
 # celery tasks
 celery[redis]==5.4.0
 
 # celery tasks
 celery[redis]==5.4.0
diff --git a/src/annoy/templates/annoy/banner_top.html b/src/annoy/templates/annoy/banner_top.html
new file mode 100644 (file)
index 0000000..2a62d11
--- /dev/null
@@ -0,0 +1,51 @@
+{% load l10n %}
+
+{% if banner %}
+<div class="
+           annoy-banner_{{ banner.place }}-container
+            annoy-banner-style_{{ banner.style }}
+           ">
+  <div class="
+              annoy-banner
+              annoy-banner_{{ banner.place }}
+              {% if banner.image %}with-image{% endif %}
+              {% if banner.smallfont %}banner-smallfont{% endif %}
+              "
+        id="annoy-banner-{{ banner.id }}"
+       style="
+           {% if banner.text_color %}color: {{ banner.text_color }};{% endif %}
+           {% if banner.background_color %}background-color: {{ banner.background_color }};{% endif %}
+
+        ">
+    <div class="annoy-banner-inner">
+
+      <div class="image-box">
+       {% if banner.image %}
+       <img src="{{ banner.image.url }}">
+       {% endif %}
+      </div>
+
+      <div class="text-box">
+       <div class="text">
+          {{ banner.get_text|safe|linebreaks }}
+       </div>
+
+       <div class="state-box">
+         <div class="action-box">
+           {% if banner.action_label %}
+            <a class="action" href="{{ banner.url }}">
+              {{ banner.action_label }}
+            </a>
+           {% endif %}
+         </div>
+       </div>
+      </div>
+
+
+
+    </div>
+  </div>
+  </div>
+
+{% endif %}
+
index 40f7511..25293f1 100644 (file)
@@ -22,6 +22,13 @@ def annoy_banner_blackout(context):
         'closable': True,
     }
 
         'closable': True,
     }
 
+@register.inclusion_tag('annoy/banner_top.html', takes_context=True)
+def annoy_banner_top(context):
+    banners = Banner.choice('top', request=context['request'])
+    return {
+        'banner': banners.first(),
+        'closable': True,
+    }
 
 @register.inclusion_tag('annoy/banners.html', takes_context=True)
 def annoy_banners(context, place):
 
 @register.inclusion_tag('annoy/banners.html', takes_context=True)
 def annoy_banners(context, place):
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 (file)
index 0000000..b2ba7ba
--- /dev/null
@@ -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')]),
+        ),
+    ]
index 0c4ae09..0436622 100644 (file)
@@ -3,6 +3,9 @@ from rest_framework.response import Response
 
 
 class WLLimitOffsetPagination(LimitOffsetPagination):
 
 
 class WLLimitOffsetPagination(LimitOffsetPagination):
+    def get_results(self, data):
+        return data['member']
+
     def get_paginated_response(self, data):
         return Response({
             "member": data,
     def get_paginated_response(self, data):
         return Response({
             "member": data,
index 4ba660e..8c00892 100644 (file)
@@ -37,8 +37,12 @@ class LoginSerializer(serializers.Serializer):
 class RegisterSerializer(serializers.Serializer):
     email = serializers.CharField()
     password = serializers.CharField(style={'input_type': 'password'})
 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 RefreshTokenSerializer(serializers.Serializer):
     refresh_token = serializers.CharField(style={'input_type': 'password'})
+
+
+class RequestConfirmSerializer(serializers.Serializer):
+    email = serializers.CharField()
index 08d1650..011161e 100644 (file)
@@ -2,8 +2,10 @@
 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
 #
 from time import time
 # 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 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
 from django import forms
 from django.http import HttpResponse
 from django.http import Http404
@@ -18,6 +20,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 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
 from . import serializers
 from .request_validator import PistonRequestValidator
 from .utils import oauthlib_request, oauthlib_response, vary_on_auth
@@ -227,7 +230,7 @@ class BlogView(APIView):
 
 
 
 
 
 
-class RegisterView(APIView):
+class RegisterView(GenericAPIView):
     serializer_class = serializers.RegisterSerializer
 
     def get(self, request):
     serializer_class = serializers.RegisterSerializer
 
     def get(self, request):
@@ -245,8 +248,37 @@ class RegisterView(APIView):
         })
 
     def post(self, request):
         })
 
     def post(self, request):
-        pass
-    
+        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
+
+        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
 
 class RefreshTokenView(APIView):
     serializer_class = serializers.RefreshTokenSerializer
@@ -284,4 +316,21 @@ class RefreshTokenView(APIView):
 
 
 class RequestConfirmView(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({})
+
index ee3c506..60e52a0 100644 (file)
@@ -40,6 +40,84 @@ class TagDetailSerializer(serializers.ModelSerializer):
         ]
 
 
         ]
 
 
+class AuthorItemSerializer(serializers.ModelSerializer):
+    url = AbsoluteURLField()
+    href = AbsoluteURLField(
+        view_name='catalogue_api_author',
+        view_args=('slug',)
+    )
+
+    class Meta:
+        model = Tag
+        fields = [
+            'url', 'href', 'name'
+        ]
+
+class AuthorSerializer(AuthorItemSerializer):
+    photo_thumb = ThumbnailField('139x193', source='photo')
+
+    class Meta:
+        model = Tag
+        fields = [
+            'url', 'href', 'name', 'slug', 'sort_key', 'description',
+            'genitive', 'photo', 'photo_thumb', 'photo_attribution',
+        ]
+
+class EpochItemSerializer(serializers.ModelSerializer):
+    url = AbsoluteURLField()
+    href = AbsoluteURLField(
+        view_name='catalogue_api_epoch',
+        view_args=('slug',)
+    )
+    class Meta:
+        model = Tag
+        fields = ['url', 'href', 'name']
+
+class EpochSerializer(EpochItemSerializer):
+    class Meta:
+        model = Tag
+        fields = [
+            'url', 'href', 'name', 'slug', 'sort_key', 'description',
+            'adjective_feminine_singular', 'adjective_nonmasculine_plural',
+        ]
+
+class GenreItemSerializer(serializers.ModelSerializer):
+    url = AbsoluteURLField()
+    href = AbsoluteURLField(
+        view_name='catalogue_api_genre',
+        view_args=('slug',)
+    )
+    class Meta:
+        model = Tag
+        fields = ['url', 'href', 'name']
+
+class GenreSerializer(GenreItemSerializer):
+    class Meta:
+        model = Tag
+        fields = [
+            'url', 'href', 'name', 'slug', 'sort_key', 'description',
+            'plural', 'genre_epoch_specific',
+        ]
+
+class KindItemSerializer(serializers.ModelSerializer):
+    url = AbsoluteURLField()
+    href = AbsoluteURLField(
+        view_name='catalogue_api_kind',
+        view_args=('slug',)
+    )
+    class Meta:
+        model = Tag
+        fields = ['url', 'href', 'name']
+
+class KindSerializer(KindItemSerializer):
+    class Meta:
+        model = Tag
+        fields = [
+            'url', 'href', 'name', 'slug', 'sort_key', 'description',
+            'collective_noun',
+        ]
+
+
 class TranslatorSerializer(serializers.Serializer):
     name = serializers.CharField(source='*')
 
 class TranslatorSerializer(serializers.Serializer):
     name = serializers.CharField(source='*')
 
@@ -55,12 +133,24 @@ class BookSerializer2(serializers.ModelSerializer):
     mobi = EmbargoURLField(source='mobi_url')
     pdf = EmbargoURLField(source='pdf_url')
 
     mobi = EmbargoURLField(source='mobi_url')
     pdf = EmbargoURLField(source='pdf_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 = [
     class Meta:
         model = Book
         fields = [
-            'full_sort_key', 'title',
+            'slug', 'title', 'full_sort_key',
             'href', 'url', 'language',
             'href', 'url', 'language',
-            #'epochs', 'genres', 'kinds', 'authors', 'translators',
+            'authors', 'translators',
+            'epochs', 'genres', 'kinds',
             #'children',
             'parent', 'preview',
             'epub', 'mobi', 'pdf', 'html', 'txt', 'fb2', 'xml',
             #'children',
             'parent', 'preview',
             'epub', 'mobi', 'pdf', 'html', 'txt', 'fb2', 'xml',
@@ -68,6 +158,37 @@ class BookSerializer2(serializers.ModelSerializer):
             'isbn_pdf', 'isbn_epub', 'isbn_mobi',
         ]
 
             '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')
 class BookSerializer(LegacyMixin, serializers.ModelSerializer):
     author = serializers.CharField(source='author_unicode')
     kind = serializers.CharField(source='kind_unicode')
@@ -229,3 +350,9 @@ class FragmentDetailSerializer(serializers.ModelSerializer):
     class Meta:
         model = Fragment
         fields = ['book', 'anchor', 'text', 'url', 'themes']
     class Meta:
         model = Fragment
         fields = ['book', 'anchor', 'text', 'url', 'themes']
+
+
+class FilterTagSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Tag
+        fields = ['id', 'category', 'name']
index 3fd0023..b16af66 100644 (file)
@@ -11,8 +11,42 @@ urlpatterns = [
          piwik_track_view(views.BookList2.as_view()),
          name='catalogue_api_book_list'
          ),
          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/<slug:slug>/',
          piwik_track_view(views.BookDetail2.as_view()),
          name='catalogue_api_book'
          ),
     path('books/<slug:slug>/',
          piwik_track_view(views.BookDetail2.as_view()),
          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"),
+    path('authors/<slug:slug>/',
+         piwik_track_view(views.AuthorView.as_view()),
+         name='catalogue_api_author'),
+    path('epochs/',
+         piwik_track_view(views.EpochList.as_view()),
+         name="catalogue_api_epoch_list"),
+    path('epochs/<slug:slug>/',
+         piwik_track_view(views.EpochView.as_view()),
+         name='catalogue_api_epoch'),
+    path('kinds/',
+         piwik_track_view(views.KindList.as_view()),
+         name="catalogue_api_kind_list"),
+    path('kinds/<slug:slug>/',
+         piwik_track_view(views.KindView.as_view()),
+         name='catalogue_api_kind'),
+    path('genres/',
+         piwik_track_view(views.GenreList.as_view()),
+         name="catalogue_api_genre_list"),
+    path('genres/<slug:slug>/',
+         piwik_track_view(views.GenreView.as_view()),
+         name='catalogue_api_genre'),
 ]
 ]
index abe8064..0e758b1 100644 (file)
@@ -9,6 +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.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 dfilters
+from rest_framework import filters
 from rest_framework.generics import (ListAPIView, RetrieveAPIView,
                                      RetrieveUpdateAPIView, get_object_or_404)
 from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
 from rest_framework.generics import (ListAPIView, RetrieveAPIView,
                                      RetrieveUpdateAPIView, get_object_or_404)
 from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
@@ -17,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 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
 from catalogue.models import Book, Collection, Tag, Fragment, BookMedia
 from catalogue.models.tag import prefetch_relations
 from club.models import Membership
@@ -183,14 +186,48 @@ class BookList(LegacyListAPIView):
         return Response({}, status=status.HTTP_201_CREATED)
 
 
         return Response({}, status=status.HTTP_201_CREATED)
 
 
+class BookFilter(dfilters.FilterSet):
+    sort = dfilters.OrderingFilter(
+        fields=(
+            ('sort_key_author', 'alpha'),
+            ('popularity', 'popularity'),
+        )
+    )
+    tag = dfilters.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
 class BookList2(ListAPIView):
     permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
     queryset = Book.objects.none()  # Required for DjangoModelPermissions
     serializer_class = serializers.BookSerializer2
+    filter_backends = (
+        dfilters.DjangoFilterBackend,
+        filters.SearchFilter,
+    )
+    filterset_class = BookFilter
+    search_fields = [
+        'title',
+    ]
+
+    def get_queryset(self):
+        books = Book.objects.all()
+        books = books.filter(findable=True)
+        books = order_books(books, True)
+
+        return books
+
+
+class BookList11Labs(BookList2):
+    serializer_class = serializers.BookSerializer11Labs
 
     def get_queryset(self):
         books = Book.objects.all()
         books = books.filter(findable=True)
 
     def get_queryset(self):
         books = Book.objects.all()
         books = books.filter(findable=True)
+        books = books.filter(license='')
         books = order_books(books, True)
 
         return books
         books = order_books(books, True)
 
         return books
@@ -328,6 +365,42 @@ class TagCategoryView(LegacyListAPIView):
 
         return tags
 
 
         return tags
 
+class AuthorList(ListAPIView):
+    serializer_class = serializers.AuthorSerializer
+    queryset = Tag.objects.filter(category='author')
+
+class AuthorView(RetrieveAPIView):
+    serializer_class = serializers.AuthorSerializer
+    queryset = Tag.objects.filter(category='author')
+    lookup_field = 'slug'
+
+class EpochList(ListAPIView):
+    serializer_class = serializers.EpochSerializer
+    queryset = Tag.objects.filter(category='epoch')
+
+class EpochView(RetrieveAPIView):
+    serializer_class = serializers.EpochSerializer
+    queryset = Tag.objects.filter(category='epoch')
+    lookup_field = 'slug'
+
+class GenreList(ListAPIView):
+    serializer_class = serializers.GenreSerializer
+    queryset = Tag.objects.filter(category='genre')
+
+class GenreView(RetrieveAPIView):
+    serializer_class = serializers.GenreSerializer
+    queryset = Tag.objects.filter(category='genre')
+    lookup_field = 'slug'
+
+class KindList(ListAPIView):
+    serializer_class = serializers.KindSerializer
+    queryset = Tag.objects.filter(category='kind')
+
+class KindView(RetrieveAPIView):
+    serializer_class = serializers.KindSerializer
+    queryset = Tag.objects.filter(category='kind')
+    lookup_field = 'slug'
+
 
 class TagView(RetrieveAPIView):
     permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
 
 class TagView(RetrieveAPIView):
     permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
@@ -421,3 +494,13 @@ class FragmentView(RetrieveAPIView):
             book__slug=self.kwargs['book'],
             anchor=self.kwargs['anchor']
         )
             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)
index 9c5696f..2d35357 100644 (file)
@@ -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))
 
 
         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'
 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 (file)
index 0000000..bd297d4
--- /dev/null
@@ -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'),
+        ),
+    ]
index 29e3754..c2b9eed 100644 (file)
@@ -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)
     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)
     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)
     # 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)
     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
         '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)
 
     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_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')
 
     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.findable = findable
         book.language = book_info.language
         book.title = book_info.title
+        book.license = book_info.license or ''
         if book_info.variant_of:
             book.common_slug = book_info.variant_of.slug
         else:
         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()
         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)
 
         if not settings.NO_SEARCH_INDEX and search_index and findable:
             tasks.index_book.delay(book.id)
index dbe9259..18e4bcc 100644 (file)
 
 
   <article id="main-text" {% if book.has_sync_file %}class="has-sync"{% endif %}>
 
 
   <article id="main-text" {% if book.has_sync_file %}class="has-sync"{% endif %}>
-    {% with next=book.get_next_text prev=book.get_prev_text %}
+<div id="sidebar">
+      {% if book.other_versions.exists %}
+        <div class="box" id="other">
+          <h2>{% trans "Inne wersje tekstu" %}</h2>
+          <a class="other-text-close" href="#">{% trans "Zamknij drugą wersję" %}</a>
+          <ul>
+            {% spaceless %}
+              {% for other_version in book.other_versions %}
+                <li>
+                  <a class="display-other"
+                     data-other="{{ other_version.html_url }}"
+                     href="{% url 'book_text' other_version.slug %}">
+                    {{ other_version.mini_box_nolink }}
+                  </a>
+                </li>
+              {% endfor %}
+            {% endspaceless %}
+          </ul>
+        </div>
+      {% endif %}
+</div>
+
+{% with next=book.get_next_text prev=book.get_prev_text %}
       {% if next %}
         <a class="text_next-book" href="{% url 'book_text' next.slug %}">{{ next.title }}&nbsp;&rarr;</a>
       {% endif %}
       {% if next %}
         <a class="text_next-book" href="{% url 'book_text' next.slug %}">{{ next.title }}&nbsp;&rarr;</a>
       {% endif %}
             <div class="pointer pointer-top"></div>
         </div>
       </div>
             <div class="pointer pointer-top"></div>
         </div>
       </div>
+</div>
 
 
 
 
-      {% if book.other_versions.exists %}
-        <div class="box" id="other">
-          <h2>{% trans "Inne wersje utworu" %}</h2>
-          <a class="other-text-close" href="#">{% trans "Zamknij drugą wersję" %}</a>
-          <ul>
-            {% spaceless %}
-              {% for other_version in book.other_versions %}
-                <li>
-                  <a class="display-other"
-                     data-other="{{ other_version.html_url }}"
-                     href="{% url 'book_text' other_version.slug %}">
-                    {{ other_version.mini_box_nolink }}
-                  </a>
-                </li>
-              {% endfor %}
-            {% endspaceless %}
-          </ul>
-        </div>
-      {% endif %}
-
       <div id="annoy-stubs">
         {% annoy_banners 'book-text-intermission' %}
 
       <div id="annoy-stubs">
         {% annoy_banners 'book-text-intermission' %}
 
index c09769a..c40428d 100644 (file)
@@ -417,7 +417,8 @@ class PayUOrder(payu_models.Order):
         )            
 
     @classmethod
         )            
 
     @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')
         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,
         ctx = {
             "email": email,
             "year": year,
-            "next_year": year + 1,
             "total": sum(x['amount'] for x in payments),
             "payments": payments,
             "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()
         }
         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"),
             })
 
             "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]
             )
         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()
         message.send()
-        os.unlink(f.name)
 
 
 class PayUCardToken(payu_models.CardToken):
 
 
 class PayUCardToken(payu_models.CardToken):
index 1b6de08..3d9cd55 100644 (file)
@@ -2,6 +2,7 @@
 {% load club %}
 
 {% block content %}
 {% load club %}
 
 {% block content %}
+<div style="display: flex; gap:50px;">
   <table class="table">
     <tr>
       <td>Aktywne miesięczne wpłaty cykliczne:</td>
   <table class="table">
     <tr>
       <td>Aktywne miesięczne wpłaty cykliczne:</td>
       <td>{% club_active_30day_sum %} zł.</td>
     </tr>
   </table>
       <td>{% club_active_30day_sum %} zł.</td>
     </tr>
   </table>
+  <div>
+    <form method="post" action="{% url 'club_receipt' %}" style="display: flex; flex-direction: column;">
+      {% csrf_token %}
+      <span>Pobierz zestawienie roczne</span>
+      <input name="email" placeholder="email" required></input>
+      <input name="year" type="number" min="2013" max="2024" value="2024" placeholder="rok" required></input>
+      <button>pobierz zestawienie</button>
+    </form>
+  </div>
+  </div>
   {{ block.super }}
 {% endblock content %}
   {{ block.super }}
 {% endblock content %}
index 0267580..87bfa5a 100644 (file)
@@ -28,4 +28,6 @@ urlpatterns = [
     path('notify/<int:pk>/', views.PayUNotifyView.as_view(), name='club_payu_notify'),
 
     path('weryfikacja/', views.member_verify, name='club_member_verify'),
     path('notify/<int:pk>/', views.PayUNotifyView.as_view(), name='club_payu_notify'),
 
     path('weryfikacja/', views.member_verify, name='club_member_verify'),
+
+    path('potwierdzenie/', views.receipt, name='club_receipt'),
 ]
 ]
index c6136b6..b2657e6 100644 (file)
@@ -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.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
 from django.shortcuts import get_object_or_404, render
 from django.urls import reverse
 from django.utils.decorators import method_decorator
@@ -249,3 +249,26 @@ def member_verify(request):
             'result': rows
         }
     )
             'result': rows
         }
     )
+
+
+@permission_required('club.schedule_view')
+def receipt(request):
+    email = request.POST.get('email')
+    try:
+        year = int(request.POST.get('year'))
+    except:
+        return HttpResponse('no content')
+
+    receipt = models.PayUOrder.generate_receipt(email, year)
+    if receipt:
+        content, optout, payments = receipt
+    else:
+        return HttpResponse('no content')
+    return HttpResponse(
+        content,
+        headers={
+            "Content-Type": "application/pdf",
+            "Content-Disposition": f'attachment; filename="wolnelektury-{year}-{email}.pdf"',
+        }
+    )
+
index 8e3153a..2e3adb8 100644 (file)
@@ -25,8 +25,8 @@
     {% if author.alive %}
       <p>
         {% trans "Dzieła tego autora objęte są prawem autorskim." %}
     {% if author.alive %}
       <p>
         {% trans "Dzieła tego autora objęte są prawem autorskim." %}
-        {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %}
-          <a href="{{ url }}">Dowiedz się</a>, dlaczego biblioteki internetowe nie mogą
+        {% blocktrans trimmed %}
+          Biblioteki internetowe nie mogą
           udostępniać dzieł tego autora.
         {% endblocktrans %}
       </p>
           udostępniać dzieł tego autora.
         {% endblocktrans %}
       </p>
@@ -46,8 +46,8 @@
           </p>
           <div class='countdown' data-until='{{ pd_counter|date_to_utc|utc_for_js }}'></div>
           <p>
           </p>
           <div class='countdown' data-until='{{ pd_counter|date_to_utc|utc_for_js }}'></div>
           <p>
-            {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %}
-              <a href="{{ url }}">Dowiedz się</a>, dlaczego biblioteki internetowe nie mogą
+            {% blocktrans trimmed %}
+              Biblioteki internetowe nie mogą
               udostępniać dzieł tego autora.
             {% endblocktrans %}
           </p>
               udostępniać dzieł tego autora.
             {% endblocktrans %}
           </p>
index 655d7f0..02a099d 100644 (file)
               {% endblocktrans %}</p>
               <div class='countdown' data-until='{{ pd_counter|date_to_utc|utc_for_js }}'></div>
               <p>
               {% endblocktrans %}</p>
               <div class='countdown' data-until='{{ pd_counter|date_to_utc|utc_for_js }}'></div>
               <p>
-                {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %}
-                  <a href="{{ url }}">Dowiedz się</a>, dlaczego biblioteki internetowe nie mogą
+                {% blocktrans trimmed %}
+                  Biblioteki internetowe nie mogą
                   udostępniać tego utworu.
                 {% endblocktrans %}
               </p>
             {% else %}
               <p>
                 {% trans "Ten utwór objęty jest prawem autorskim." %}
                   udostępniać tego utworu.
                 {% endblocktrans %}
               </p>
             {% else %}
               <p>
                 {% trans "Ten utwór objęty jest prawem autorskim." %}
-                {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %}
-                  <a href="{{ url }}">Dowiedz się</a>, dlaczego biblioteki internetowe nie mogą
+                {% blocktrans trimmed %}
+                  Biblioteki internetowe nie mogą
                   udostępniać tego utworu.
                 {% endblocktrans %}
               </p>
                   udostępniać tego utworu.
                 {% endblocktrans %}
               </p>
diff --git a/src/social/migrations/0017_userconfirmation.py b/src/social/migrations/0017_userconfirmation.py
new file mode 100644 (file)
index 0000000..fe39336
--- /dev/null
@@ -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)),
+            ],
+        ),
+    ]
index cb1326b..17fe7d0 100644 (file)
@@ -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.
 #
 # 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 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.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
 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()
 
     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 (file)
index 0000000..f56d2f2
--- /dev/null
@@ -0,0 +1,12 @@
+{% extends "base_simple.html" %}
+{% load i18n %}
+
+
+{% block body %}
+  <h1>{% trans "Konto potwierdzone" %}</h1>
+
+  <p class="normal-text">
+    {% blocktrans with user=user %}Konto <strong>{{ user }}</strong> zostało potwierdzone. Możesz się teraz zalogować.{% endblocktrans %}
+  </p>
+{% endblock body %}
+
index 299f620..10d9240 100644 (file)
@@ -7,6 +7,7 @@ from . import views
 
 
 urlpatterns = [
 
 
 urlpatterns = [
+    path('potwierdz/<str:key>/', views.confirm_user, name='social_confirm_user'),
     path('lektura/<slug:slug>/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'),
     path('lektura/<slug:slug>/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'),
index 989771a..3dfcd9e 100644 (file)
@@ -10,7 +10,7 @@ from django.views.generic.edit import FormView
 
 from catalogue.models import Book, Tag
 import catalogue.models.tag
 
 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
 
 
 from wolnelektury.utils import is_ajax
 
 
@@ -125,3 +125,12 @@ def my_tags(request):
             t.name for t in tags
         ], safe=False
     )
             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,
+    })
index d4faa8b..b9d507e 100644 (file)
@@ -59,6 +59,7 @@ INSTALLED_APPS_CONTRIB = [
     'django.contrib.postgres',
     'admin_ordering',
     'rest_framework',
     'django.contrib.postgres',
     'admin_ordering',
     'rest_framework',
+    'django_filters',
     'fnp_django_pagination',
     'pipeline',
     'sorl.thumbnail',
     'fnp_django_pagination',
     'pipeline',
     'sorl.thumbnail',
index 16b5e0a..d673772 100644 (file)
@@ -76,3 +76,5 @@ SEARCH_CONFIG_SIMPLE = 'simple'
 SEARCH_USE_UNACCENT = False
 
 FEATURE_SYNCHRO = False
 SEARCH_USE_UNACCENT = False
 
 FEATURE_SYNCHRO = False
+
+FEATURE_API_REGISTER = False
index 9d90c65..70704f3 100644 (file)
@@ -51,7 +51,7 @@
     .annoy-banner-off {
         font-size: 1.2rem !important;
     }
     .annoy-banner-off {
         font-size: 1.2rem !important;
     }
-    
+
     @include rwd($break-flow) {
         .annoy-banner-inner {
             padding: 0;
     @include rwd($break-flow) {
         .annoy-banner-inner {
             padding: 0;
 
         }
         .annoy-banner-off {
 
         }
         .annoy-banner-off {
-            
+
         }
     }
 }
 
 
         }
     }
 }
 
 
-.annoy-banner_crisis-container {
+.annoy-banner_crisis-container,
+.annoy-banner_top-container {
     position: sticky;
     top: 0;
     position: sticky;
     top: 0;
-    height: 160px;
     z-index: 10;
     box-shadow: 0 0 10px black;
     display: flex;
     z-index: 10;
     box-shadow: 0 0 10px black;
     display: flex;
     align-items:center;
     cursor: pointer;
 
     align-items:center;
     cursor: pointer;
 
-    @media screen and (min-height: 480px) {
-       height: 33vh;
-       top: calc(-33vh + 160px);
+    &.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 (max-width: 940px) {
-       padding: 10px 0;
-       height: auto;
-       top: 0;
+       @media screen and (min-height: 480px) {
+           height: 33vh;
+           top: calc(-33vh + 160px);
+       }
+
+       @media screen and (max-width: 940px) {
+           padding: 10px 0;
+           height: auto;
+           top: 0;
+       }
     }
 
     }
 
-    .annoy-banner_crisis {
+
+    .annoy-banner_crisis,
+    .annoy-banner_top {
        position: sticky;
        top: 0;
        width: 100%;
        position: sticky;
        top: 0;
        width: 100%;
            .image-box {
                position: relative;
                img {
            .image-box {
                position: relative;
                img {
-                   height: 159px;
+                   max-height: 159px;
                    display: block;
 
                    @media screen and (max-width: 700px) {
                    display: block;
 
                    @media screen and (max-width: 700px) {
-                       height: 120px;
+                       max-height: 120px;
                    }
                }
            }
                    }
                }
            }
                    }
                }
 
                    }
                }
 
+               @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;
                }
                .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;
                }
                a {
                    color: #c32721;
                }
                display: block;
                transition: background-color .2s;
 
                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;
                &:hover {
                    background: #ffd430;
                    text-decoration: none;
            }
        }
     }
            }
        }
     }
-    &.annoy-banner-style_crisis_quiet {
+    &.annoy-banner-style_crisis_quiet,
+    &.annoy-banner_top-container {
        background: black;
        color: white;
        .annoy-banner-inner {
        background: black;
        color: white;
        .annoy-banner-inner {
index 65bc48a..1fde2b9 100644 (file)
@@ -294,6 +294,7 @@ h2 {
 h3, .subtitle2 {
     font-size: 1.5em;
     margin: 1.5em 0 1em 0;
 h3, .subtitle2 {
     font-size: 1.5em;
     margin: 1.5em 0 1em 0;
+    padding-right: 48px;
     font-weight: normal;
     line-height: 1.5em;
 }
     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;
+}
index eff64a0..dde96ed 100644 (file)
@@ -94,33 +94,85 @@ $("#menu a").each(function() {
 $("#menu-other").show();
 
 
 $("#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 = $('<div class="other">');
+               d.attr('lang', lang);
+               d.append(e);
+               d.insertBefore(cursor);
+           }
+       });
+    }
+    
 /* Load other version of text. */
 $(".display-other").click(function(e) {
     e.preventDefault();
     release_menu();
 
 /* 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) {
     $("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-waiter").hide();
-            $("#other-text-body").show();
-            loaded_text($("#other-text-body"));
+            loaded_text($(".other"));
         }
     });
     _paq.push(['trackEvent', 'html', 'other-text']);
 });
 
 
         }
     });
     _paq.push(['trackEvent', 'html', 'other-text']);
 });
 
 
+
+    
+
 /* Remove other version of text. */
 $(".other-text-close").click(function(e) {
     release_menu();
     e.preventDefault();
 /* 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');
     $("body").removeClass('with-other-text');
-    $("#other-text-body").html("");
     _paq.push(['trackEvent', 'html', 'other-text-close']);
 });
 
     _paq.push(['trackEvent', 'html', 'other-text-close']);
 });
 
index 17e2379..3cc4851 100644 (file)
@@ -7,6 +7,7 @@
 {% load preview_ad from catalogue_tags %}
 
 {% annoy_banner_crisis %}
 {% load preview_ad from catalogue_tags %}
 
 {% annoy_banner_crisis %}
+{% annoy_banner_top %}
 
 {% annoy_banner_blackout %}
 
 
 {% annoy_banner_blackout %}