# django
Django==4.0.8
fnpdjango==0.6
-docutils==0.16
+docutils==0.20
-django-pipeline==2.0.8
-libsasscompiler==0.1.9
+django-pipeline==3.1.0
+libsasscompiler==0.2.0
jsmin==3.0.1
fnp-django-pagination==2.2.5
-django-modeltranslation==0.18.4
+django-modeltranslation==0.18.12
django-allauth==0.51
-django-extensions==3.2.1
-djangorestframework==3.13.1
+django-extensions==3.2.3
+djangorestframework==3.15.1
+django-filter==23.5
djangorestframework-xml==2.0.0
-django-admin-ordering==0.16
-django-countries==7.3.2
+django-admin-ordering==0.18
+django-countries==7.6.1
-e git+https://github.com/rczajka/django-forms-builder@270cc22f80ec4681735b4e01689562fbab79ceda#egg=django-forms-builder
# contact
pyyaml==5.4.1
-polib==1.1.1
+polib==1.2
-django-honeypot==1.0.3
+django-honeypot==1.2.1
python-fb==0.2
-Feedparser==6.0.10
+Feedparser==6.0.11
-Pillow==9.2.0
-mutagen==1.45.1
-sorl-thumbnail==12.8.0
+Pillow==9.5.0
+mutagen==1.47
+sorl-thumbnail==12.10.0
# home-brewed & dependencies
-librarian==24.5.4
+librarian==24.5.8
# celery tasks
-celery[redis]==5.2.7
+celery[redis]==5.4.0
# OAI-PMH
#pyoai==2.5.1
-e git+https://github.com/infrae/pyoai@5ff2f15e869869e70d8139e4c37b7832854d7049#egg=pyoai
-sentry-sdk==0.10.2
+sentry-sdk==2.19.2
-requests
+requests==2.32.3
paypalrestsdk
-python-slugify
+python-slugify==8.0.4
-firebase-admin
-Wikidata==0.7.0
+firebase-admin==6.6.0
+Wikidata==0.8.1
--- /dev/null
+{% 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 %}
+
'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):
#
from rest_framework import serializers
from sorl.thumbnail import default
-from django.urls import reverse
+from rest_framework.reverse import reverse
from club.models import Membership
self.view_args[fields[0]] = fields[1] if len(fields) > 1 else fields[0]
def to_representation(self, value):
+ request = self.context['request']
if self.view_name is not None:
kwargs = {
arg: getattr(value, field)
for (arg, field) in self.view_args.items()
}
- value = reverse(self.view_name, kwargs=kwargs)
- return self.context['request'].build_absolute_uri(value)
+ return reverse(self.view_name, kwargs=kwargs, request=request)
+ return request.build_absolute_uri(value)
class LegacyMixin:
--- /dev/null
+# 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')]),
+ ),
+ ]
class Token(models.Model):
REQUEST = 1
ACCESS = 2
- TOKEN_TYPES = ((REQUEST, 'Request'), (ACCESS, 'Access'))
+ REFRESH = 3
+ TOKEN_TYPES = (
+ (REQUEST, 'Request'),
+ (ACCESS, 'Access'),
+ (REFRESH, 'Refresh')
+ )
key = models.CharField(max_length=KEY_SIZE)
secret = models.CharField(max_length=SECRET_SIZE)
--- /dev/null
+from rest_framework.pagination import LimitOffsetPagination, PageLink
+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,
+ "totalItems": self.count,
+ "view": {
+ "previous": self.get_previous_link(),
+ "next": self.get_next_link(),
+ }
+ })
class UserSerializer(serializers.ModelSerializer):
premium = UserPremiumField()
+ confirmed = serializers.BooleanField(source='is_active')
class Meta:
model = User
- fields = ['username', 'premium']
+ fields = ['username', 'premium', 'confirmed']
class BookUserDataSerializer(serializers.ModelSerializer):
class LoginSerializer(serializers.Serializer):
username = 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(), required=False)
+
+
+class RefreshTokenSerializer(serializers.Serializer):
+ refresh_token = serializers.CharField(style={'input_type': 'password'})
+
+
+class RequestConfirmSerializer(serializers.Serializer):
+ email = serializers.CharField()
<?xml version="1.0" encoding="utf-8"?>
-<response><resource><kind></kind><full_sort_key>$child$2</full_sort_key><title>Child</title><url>http://testserver/katalog/lektura/child/</url><cover_color>#000000</cover_color><author></author><cover></cover><epoch></epoch><href>http://testserver/api/books/child/</href><has_audio>False</has_audio><genre>Wiersz</genre><simple_thumb></simple_thumb><slug>child</slug><cover_thumb></cover_thumb><liked></liked></resource><resource><kind></kind><full_sort_key>$grandchild$3</full_sort_key><title>Grandchild</title><url>http://testserver/katalog/lektura/grandchild/</url><cover_color>#000000</cover_color><author></author><cover></cover><epoch></epoch><href>http://testserver/api/books/grandchild/</href><has_audio>False</has_audio><genre>Sonet</genre><simple_thumb></simple_thumb><slug>grandchild</slug><cover_thumb></cover_thumb><liked></liked></resource><resource><kind>Liryka</kind><full_sort_key>john doe$parent$1</full_sort_key><title>Parent</title><url>http://testserver/katalog/lektura/parent/</url><cover_color>#a6820a</cover_color><author>John Doe</author><cover>cover/parent.jpg</cover><epoch>Barok</epoch><href>http://testserver/api/books/parent/</href><has_audio>True</has_audio><genre>Sonet</genre><simple_thumb>http://testserver/media/cover_api_thumb/parent.jpg</simple_thumb><slug>parent</slug><cover_thumb>cover_thumb/parent.jpg</cover_thumb><liked></liked></resource></response>
+<response><resource><kind></kind><full_sort_key>$child$2</full_sort_key><title>Child</title><url>http://testserver/katalog/lektura/child/</url><cover_color>#000000</cover_color><author></author><cover></cover><epoch></epoch><href>http://testserver/api/books/child/?format=xml</href><has_audio>False</has_audio><genre>Wiersz</genre><simple_thumb></simple_thumb><slug>child</slug><cover_thumb></cover_thumb><liked></liked></resource><resource><kind></kind><full_sort_key>$grandchild$3</full_sort_key><title>Grandchild</title><url>http://testserver/katalog/lektura/grandchild/</url><cover_color>#000000</cover_color><author></author><cover></cover><epoch></epoch><href>http://testserver/api/books/grandchild/?format=xml</href><has_audio>False</has_audio><genre>Sonet</genre><simple_thumb></simple_thumb><slug>grandchild</slug><cover_thumb></cover_thumb><liked></liked></resource><resource><kind>Liryka</kind><full_sort_key>john doe$parent$1</full_sort_key><title>Parent</title><url>http://testserver/katalog/lektura/parent/</url><cover_color>#a6820a</cover_color><author>John Doe</author><cover>cover/parent.jpg</cover><epoch>Barok</epoch><href>http://testserver/api/books/parent/?format=xml</href><has_audio>True</has_audio><genre>Sonet</genre><simple_thumb>http://testserver/media/cover_api_thumb/parent.jpg</simple_thumb><slug>parent</slug><cover_thumb>cover_thumb/parent.jpg</cover_thumb><liked></liked></resource></response>
from . import views
+urlpatterns1 = [
+ path('register/', csrf_exempt(views.RegisterView.as_view())),
+ path('refreshToken/', csrf_exempt(views.RefreshTokenView.as_view())),
+ path('requestConfirm/', csrf_exempt(views.RequestConfirmView.as_view())),
+ path('login/', csrf_exempt(views.Login2View.as_view())),
+ path('me/', views.UserView.as_view()),
+ path('', include('catalogue.api.urls2')),
+]
+
+
urlpatterns = [
+ path('2/', include((urlpatterns1, 'api'), namespace="v2")),
+
path('oauth/request_token/', csrf_exempt(views.OAuth1RequestTokenView.as_view())),
path('oauth/authorize/', views.oauth_user_auth, name='oauth_user_auth'),
path('oauth/access_token/', csrf_exempt(views.OAuth1AccessTokenView.as_view())),
path('', include('social.api.urls')),
path('', include('catalogue.api.urls')),
+
]
# 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
from django import forms
from django.http import HttpResponse
from django.http import Http404
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
return Response({"access_token": key})
+class Login2View(GenericAPIView):
+ serializer_class = serializers.LoginSerializer
+
+ def post(self, request):
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ d = serializer.validated_data
+ user = authenticate(username=d['username'], password=d['password'])
+ if user is None:
+ return Response({"detail": "Invalid credentials."})
+
+ access_token = generate_token()[:KEY_SIZE]
+ Token.objects.create(
+ key=access_token,
+ token_type=Token.ACCESS,
+ timestamp=time(),
+ user=user,
+ )
+ refresh_token = generate_token()[:KEY_SIZE]
+ Token.objects.create(
+ key=refresh_token,
+ token_type=Token.REFRESH,
+ timestamp=time(),
+ user=user,
+ )
+ return Response({
+ "access_token": access_token,
+ "refresh_token": refresh_token,
+ "expires": 3600,
+ })
+
+
@vary_on_auth
class UserView(RetrieveAPIView):
permission_classes = [IsAuthenticated]
class BlogView(APIView):
def get(self, request):
return Response([])
+
+
+
+class RegisterView(GenericAPIView):
+ serializer_class = serializers.RegisterSerializer
+
+ def get(self, request):
+ return Response({
+ "options": [
+ {
+ "id": 1,
+ "html": "Chcę otrzymywać newsletter Wolnych Lektur",
+ "required": False
+ }
+ ],
+ "info": [
+ 'Administratorem danych osobowych jest Fundacja Wolne Lektury (ul. Marszałkowska 84/92 lok. 125, 00-514 Warszawa). Podanie danych osobowych jest dobrowolne. Dane są przetwarzane w zakresie niezbędnym do prowadzenia serwisu, a także w celach prowadzenia statystyk, ewaluacji i sprawozdawczości. W przypadku wyrażenia dodatkowej zgody adres e-mail zostanie wykorzystany także w celu przesyłania newslettera Wolnych Lektur. Osobom, których dane są zbierane, przysługuje prawo dostępu do treści swoich danych oraz ich poprawiania. Więcej informacji w <a href="https://fundacja.wolnelektury.pl/prywatnosc/">polityce prywatności</a>.'
+ ]
+ })
+
+ 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
+
+ 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
+
+ def post(self, request):
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ d = serializer.validated_data
+
+ t = Token.objects.get(
+ key=d['refresh_token'],
+ token_type=Token.REFRESH
+ )
+ user = t.user
+
+ access_token = generate_token()[:KEY_SIZE]
+ Token.objects.create(
+ key=access_token,
+ token_type=Token.ACCESS,
+ timestamp=time(),
+ user=user,
+ )
+ refresh_token = generate_token()[:KEY_SIZE]
+ Token.objects.create(
+ key=refresh_token,
+ token_type=Token.REFRESH,
+ timestamp=time(),
+ user=user,
+ )
+ return Response({
+ "access_token": access_token,
+ "refresh_token": refresh_token,
+ "expires": 3600,
+ })
+
+
+class RequestConfirmView(APIView):
+ 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({})
+
]
+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 BookSerializer2(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+ href = AbsoluteURLField(view_name='catalogue_api_book', view_args=['slug'])
+ xml = EmbargoURLField(source='xml_url')
+ html = EmbargoURLField(source='html_url')
+ txt = EmbargoURLField(source='txt_url')
+ fb2 = EmbargoURLField(source='fb2_url')
+ epub = EmbargoURLField(source='epub_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 = [
+ 'slug', 'title', 'full_sort_key',
+ 'href', 'url', 'language',
+ 'authors', 'translators',
+ 'epochs', 'genres', 'kinds',
+ #'children',
+ 'parent', 'preview',
+ 'epub', 'mobi', 'pdf', 'html', 'txt', 'fb2', 'xml',
+ 'cover_thumb', 'cover',
+ '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 Meta:
model = Fragment
fields = ['book', 'anchor', 'text', 'url', 'themes']
+
+
+class FilterTagSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Tag
+ fields = ['id', 'category', 'name']
--- /dev/null
+# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
+#
+from django.urls import path, re_path
+from stats.utils import piwik_track_view
+from . import views
+
+
+urlpatterns = [
+ path('books/',
+ 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('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'),
+]
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 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
book_tag_categories = ['author', 'epoch', 'kind', 'genre']
+class LegacyListAPIView(ListAPIView):
+ pagination_class = None
+
+
class CreateOnPutMixin:
'''
Creates a new model instance when PUTting a nonexistent resource.
raise
-class CollectionList(ListAPIView):
+class CollectionList(LegacyListAPIView):
queryset = Collection.objects.filter(listed=True)
serializer_class = serializers.CollectionListSerializer
@vary_on_auth # Because of 'liked'.
-class BookList(ListAPIView):
+class BookList(LegacyListAPIView):
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
queryset = Book.objects.none() # Required for DjangoModelPermissions
serializer_class = serializers.BookListSerializer
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
+ 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)
+ books = books.filter(license='')
+ books = order_books(books, True)
+
+ return books
+
+
@vary_on_auth # Because of 'liked'.
class BookDetail(RetrieveAPIView):
queryset = Book.objects.all()
serializer_class = serializers.BookDetailSerializer
+class BookDetail2(RetrieveAPIView):
+ queryset = Book.objects.all()
+ lookup_field = 'slug'
+ serializer_class = serializers.BookSerializer2
+
+
@vary_on_auth # Because of embargo links.
class EbookList(BookList):
serializer_class = serializers.EbookSerializer
@method_decorator(never_cache, name='dispatch')
-class Preview(ListAPIView):
+class Preview(LegacyListAPIView):
#queryset = Book.objects.filter(preview=True)
serializer_class = serializers.BookPreviewSerializer
@vary_on_auth # Because of 'liked'.
-class FilterBookList(ListAPIView):
+class FilterBookList(LegacyListAPIView):
serializer_class = serializers.FilterBookListSerializer
def parse_bool(self, s):
return HttpResponse(self.get_object().get_media('epub'))
-class TagCategoryView(ListAPIView):
+class TagCategoryView(LegacyListAPIView):
serializer_class = serializers.TagSerializer
def get_queryset(self):
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]
@vary_on_auth # Because of 'liked'.
-class FragmentList(ListAPIView):
+class FragmentList(LegacyListAPIView):
serializer_class = serializers.FragmentSerializer
def get_queryset(self):
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)
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'
--- /dev/null
+# 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'),
+ ),
+ ]
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)
# 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)
'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)
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')
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:
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)
<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 }} →</a>
{% endif %}
<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' %}
# allow single definition for multiple-value fields
_salias = {
'authors': 'author',
+ 'genres': 'genre',
+ 'epochs': 'epoch',
+ 'kinds': 'kind',
}
def __init__(self, **kwargs):
from django.utils.safestring import mark_safe
from fnpdjango.actions import export_as_csv_action
from modeltranslation.admin import TranslationAdmin
+import annoy.models
from wolnelektury.utils import YesNoFilter
from . import models
(m, m) for m in
model_admin.model.objects.exclude(source='').values_list('source', flat=True).distinct()[:10]
]
- print(lookups)
return lookups
def queryset(self, request, queryset):
return queryset
- #field_name = 'source' # name of the foreign key field
+class CrisisFilter(admin.SimpleListFilter):
+ title = 'czas zbiórki kryzysowej'
+ parameter_name = 'crisis'
+
+ def lookups(self, request, model_admin):
+ lookups = [
+ (b.id, '%s — %s' % (b.since, b.until)) for b in
+ annoy.models.Banner.objects.filter(place='crisis')
+ ]
+ return lookups
+
+ def queryset(self, request, queryset):
+ bid = self.value()
+ if not bid:
+ return
+ try:
+ b = annoy.models.Banner.objects.get(id=self.value())
+ except annoy.models.Banner.DoesNotExist:
+ return
+ return queryset.filter(
+ started_at__gte=b.since,
+ started_at__lte=b.until
+ )
list_filter = [
'is_cancelled', 'monthly', 'yearly', 'method',
PayedFilter, ActiveFilter, ExpiredFilter,
- SourceFilter,
+ SourceFilter, CrisisFilter
]
filter_horizontal = ['consent']
date_hierarchy = 'started_at'
)
@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')
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()
"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):
{% load club %}
{% block content %}
+<div style="display: flex; gap:50px;">
<table class="table">
<tr>
<td>Aktywne miesięczne wpłaty cykliczne:</td>
<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 %}
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'),
]
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
'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"',
+ }
+ )
+
{% 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>
</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>
{% 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." %}
- {% 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>
class ShelfView(ListAPIView):
permission_classes = [IsAuthenticated]
serializer_class = BookSerializer
+ pagination_class = None
def get_queryset(self):
state = self.kwargs['state']
--- /dev/null
+# 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)),
+ ],
+ ),
+ ]
# 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
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()
--- /dev/null
+{% 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 %}
+
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'),
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
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,
+ })
'django.contrib.postgres',
'admin_ordering',
'rest_framework',
+ 'django_filters',
'fnp_django_pagination',
'pipeline',
'sorl.thumbnail',
'api.drf_auth.WLTokenAuthentication',
'api.drf_auth.PistonOAuthAuthentication',
'rest_framework.authentication.SessionAuthentication',
- )
+ ),
+ 'DEFAULT_PAGINATION_CLASS': 'api.pagination.WLLimitOffsetPagination',
+ 'PAGE_SIZE': 10,
+ 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
}
SEARCH_USE_UNACCENT = False
FEATURE_SYNCHRO = False
+
+FEATURE_API_REGISTER = False
.annoy-banner-off {
font-size: 1.2rem !important;
}
-
+
@include rwd($break-flow) {
.annoy-banner-inner {
padding: 0;
}
.annoy-banner-off {
-
+
}
}
}
-.annoy-banner_crisis-container {
+.annoy-banner_crisis-container,
+.annoy-banner_top-container {
position: sticky;
top: 0;
- height: 160px;
z-index: 10;
box-shadow: 0 0 10px black;
display: flex;
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%;
.image-box {
position: relative;
img {
- height: 159px;
+ max-height: 159px;
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;
}
+
+ @media screen and (max-height: 400px) {
+ align-items: center;
+ .text {
+ height: 1.2em;
+ overflow: hidden;
+ }
+ }
+
a {
color: #c32721;
}
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;
}
}
}
- &.annoy-banner-style_crisis_quiet {
+ &.annoy-banner-style_crisis_quiet,
+ &.annoy-banner_top-container {
background: black;
color: white;
.annoy-banner-inner {
h3, .subtitle2 {
font-size: 1.5em;
margin: 1.5em 0 1em 0;
+ padding-right: 48px;
font-weight: normal;
line-height: 1.5em;
}
}
}
}
+
+
+
+#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;
+}
$("#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();
- $("#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']);
});
const crisis = document.querySelector(".annoy-banner_crisis-container");
const crisisLink = document.querySelector('.annoy-banner_crisis-container a.action');
- crisis.addEventListener("click", function() {
- crisisLink.click();
- });
+ if (crisis) {
+ crisis.addEventListener("click", function() {
+ crisisLink.click();
+ });
+ }
})();
{% load preview_ad from catalogue_tags %}
{% annoy_banner_crisis %}
+{% annoy_banner_top %}
-{#% annoy_banner_blackout %#}
+{% annoy_banner_blackout %}
<nav class="l-navigation">
<div class="l-container">