# 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
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==10.4
+mutagen==1.47
+sorl-thumbnail==12.10.0
# home-brewed & dependencies
-librarian==24.9
+librarian==24.5.4
# 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
class AnnoyConfig(AppConfig):
name = 'annoy'
+
+ def ready(self):
+ from . import signals
--- /dev/null
+# Generated by Django 4.0.8 on 2024-12-04 11:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('annoy', '0016_alter_mediainsertset_file_format'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='banner',
+ name='progress',
+ field=models.IntegerField(blank=True, null=True, verbose_name='postęp'),
+ ),
+ migrations.AddField(
+ model_name='banner',
+ name='target',
+ field=models.IntegerField(blank=True, null=True, verbose_name='cel'),
+ ),
+ migrations.AlterField(
+ model_name='banner',
+ name='place',
+ field=models.SlugField(choices=[('top', 'U góry wszystkich stron'), ('book-page', 'Strona książki'), ('book-text-intermission', 'Przerwa w treści książki'), ('book-fragment-list', 'Obok listy fragmentów książki'), ('blackout', 'Blackout'), ('crisis', 'Kryzysowa')], verbose_name='miejsce'),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2024-12-06 08:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('annoy', '0017_banner_progress_banner_target_alter_banner_place'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='banner',
+ name='style',
+ field=models.CharField(blank=True, choices=[('blackout_full', 'Blackout — Cały ekran'), ('blackout_upper', 'Blackout — Górna połowa ekranu'), ('crisis_quiet', 'Kryzysowa — Spokojny'), ('crisis_loud', 'Kryzysowa — Ostry')], max_length=255, verbose_name='styl'),
+ ),
+ ]
style = models.CharField(
'styl', max_length=255, blank=True,
choices=STYLES,
- help_text='Dotyczy blackoutu.'
)
smallfont = models.BooleanField('mały font', default=False)
text_color = models.CharField(max_length=10, blank=True)
help_text='Bannery z wyższym priorytetem mają pierwszeństwo.')
since = models.DateTimeField('od', null=True, blank=True)
until = models.DateTimeField('do', null=True, blank=True)
+ target = models.IntegerField('cel', null=True, blank=True)
+ progress = models.IntegerField('postęp', null=True, blank=True)
show_members = models.BooleanField('widoczny dla członków klubu', default=False)
staff_preview = models.BooleanField('podgląd tylko dla zespołu', default=False)
only_authenticated = models.BooleanField('tylko dla zalogowanych', default=False)
return Template(self.text).render(Context())
@classmethod
- def choice(cls, place, request):
+ def choice(cls, place, request, exemptions=True):
Membership = apps.get_model('club', 'Membership')
- if hasattr(request, 'annoy_banner_exempt'):
+ if exemptions and hasattr(request, 'annoy_banner_exempt'):
return cls.objects.none()
if settings.DEBUG:
return banners
+ @property
+ def progress_percent(self):
+ if not self.target:
+ return 0
+ return (self.progress or 0) / self.target * 100
+
+ @property
+ def progress_percent_pretty(self):
+ return int(self.progress_percent)
+
+ def update_progress(self):
+ # Total of new payments during the action.
+ # This definition will need to change for longer timespans.
+ if not self.since or not self.until or not self.target:
+ return
+ Schedule = apps.get_model('club', 'Schedule')
+ self.progress = Schedule.objects.filter(
+ payed_at__gte=self.since,
+ payed_at__lte=self.until,
+ ).aggregate(c=models.Sum('amount'))['c']
+ self.save(update_fields=['progress'])
+
+ @classmethod
+ def update_all_progress(cls):
+ for obj in cls.objects.exclude(target=None):
+ obj.update_progress()
+
class DynamicTextInsert(models.Model):
paragraphs = models.IntegerField('akapity')
class Meta:
ordering = ('ordering',)
-
-
-from django.db.models.signals import post_save, post_delete
-from django.dispatch import receiver
-
-@receiver(post_delete, sender=MediaInsertText)
-@receiver(post_save, sender=MediaInsertText)
-def update_etag(sender, instance, **kwargs):
- instance.media_insert_set.update_etag()
# ('centre', 'Środek ekranu'),
('upper', 'Górna połowa ekranu'),
)),
+ ('crisis', 'Kryzysowa', False, (
+ ('quiet', 'Spokojny'),
+ ('loud', 'Ostry'),
+ )),
]
PLACE_CHOICES = [p[:2] for p in PLACE_DEFINITIONS]
for p in PLACE_DEFINITIONS:
if len(p) > 3:
STYLES.extend([
- (f'{p[0]}_{s[0]}', s[1])
+ (f'{p[0]}_{s[0]}', f'{p[1]} — {s[1]}')
for s in p[3]
])
--- /dev/null
+from django.db.models.signals import post_save, post_delete
+from django.dispatch import receiver
+import club.models
+from . import models
+
+
+@receiver(post_delete, sender=models.MediaInsertText)
+@receiver(post_save, sender=models.MediaInsertText)
+def update_etag(sender, instance, **kwargs):
+ instance.media_insert_set.update_etag()
+
+
+@receiver(post_save, sender=club.models.Schedule)
+def update_progress(sender, instance, **kwargs):
+ try:
+ models.Banner.update_all_progress()
+ except:
+ pass
--- /dev/null
+{% load l10n %}
+{% load time_tags %}
+
+{% if banner %}
+<div class="
+ annoy-banner_crisis-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="progress-box">
+ <div>
+ <div class="l-checkout__support__bar">
+ <span data-label="{{ banner.progress_percent_pretty }}%" style="width: {{ banner.progress_percent|stringformat:".3f" }}%;"></span>
+ </div>
+ </div>
+ </div>
+ <div class="time-box">
+ <strong class="countdown inline" data-until='{{ banner.until|date_to_utc:True|utc_for_js }}'> </strong>
+ </div>
+ <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 %}
+
'banners': Banner.choice(place, request=context['request']),
'closable': PLACES.get(place, False),
}
+
+
+@register.inclusion_tag('annoy/banner_crisis.html', takes_context=True)
+def annoy_banner_crisis(context):
+ banners = Banner.choice('crisis', request=context['request'], exemptions=False)
+ return {
+ 'banner': banners.first(),
+ 'closable': True,
+ }
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
from oauthlib.oauth1 import ResourceEndpoint
-from rest_framework.authentication import BaseAuthentication
+from rest_framework.authentication import BaseAuthentication, TokenAuthentication
from .request_validator import PistonRequestValidator
from .utils import oauthlib_request
+from .models import Token
class PistonOAuthAuthentication(BaseAuthentication):
)
if v:
return r.token.user, r.token
+
+
+class WLTokenAuthentication(TokenAuthentication):
+ model = Token
#
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 2024-11-08 10:14
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0006_alter_bookuserdata_unique_together'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='token',
+ name='consumer',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.consumer'),
+ ),
+ ]
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)
settings.AUTH_USER_MODEL, models.CASCADE,
null=True, blank=True, related_name='tokens'
)
- consumer = models.ForeignKey(Consumer, models.CASCADE)
+ consumer = models.ForeignKey(Consumer, models.CASCADE, null=True, blank=True)
def __str__(self):
return "%s Token %s for %s" % (self.get_token_type_display(), self.key, self.consumer)
--- /dev/null
+from rest_framework.pagination import LimitOffsetPagination, PageLink
+from rest_framework.response import Response
+
+
+class WLLimitOffsetPagination(LimitOffsetPagination):
+ 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 Meta:
model = BookUserData
fields = ['state']
+
+
+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())
+
+
+class RefreshTokenSerializer(serializers.Serializer):
+ refresh_token = serializers.CharField(style={'input_type': 'password'})
<?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('login/', csrf_exempt(views.LoginView.as_view())),
path('', TemplateView.as_view(template_name='api/main.html'), name='api'),
path('', include('social.api.urls')),
path('', include('catalogue.api.urls')),
+
]
# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
+from time import time
+from django.contrib.auth import authenticate
from django.contrib.auth.decorators import login_required
from django import forms
from django.http import HttpResponse
from django.http import Http404
from django.shortcuts import render
from django.views.generic.base import View
-from oauthlib.common import urlencode
+from oauthlib.common import urlencode, generate_token
from oauthlib.oauth1 import RequestTokenEndpoint, AccessTokenEndpoint
from oauthlib.oauth1 import AuthorizationEndpoint, OAuth1Error
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
-from rest_framework.generics import RetrieveAPIView, get_object_or_404
+from rest_framework.generics import GenericAPIView, RetrieveAPIView, get_object_or_404
from catalogue.models import Book
-from .models import BookUserData, KEY_SIZE, SECRET_SIZE
+from .models import BookUserData, KEY_SIZE, SECRET_SIZE, Token
from . import serializers
from .request_validator import PistonRequestValidator
from .utils import oauthlib_request, oauthlib_response, vary_on_auth
)
+class LoginView(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."})
+
+ key = generate_token()[:KEY_SIZE]
+ Token.objects.create(
+ key=key,
+ token_type=Token.ACCESS,
+ timestamp=time(),
+ user=user,
+ )
+ 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(APIView):
+ 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):
+ pass
+
+
+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):
+ pass
]
+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 BookSerializer(LegacyMixin, serializers.ModelSerializer):
author = serializers.CharField(source='author_unicode')
kind = serializers.CharField(source='kind_unicode')
--- /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('books/<slug:slug>/',
+ piwik_track_view(views.BookDetail2.as_view()),
+ name='catalogue_api_book'
+ ),
+
+ 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'),
+]
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 BookList2(ListAPIView):
+ permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
+ queryset = Book.objects.none() # Required for DjangoModelPermissions
+ serializer_class = serializers.BookSerializer2
+
+ def get_queryset(self):
+ books = Book.objects.all()
+ books = books.filter(findable=True)
+ 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):
class TxtField(EbookField):
ext = 'txt'
for_parents = False
- librarian2_api = True
@staticmethod
def transform(wldoc, book):
- from librarian.builders.txt import TxtBuilder
- return TxtBuilder().build(wldoc)
+ return wldoc.as_text()
class Fb2Field(EbookField):
class HtmlField(EbookField):
ext = 'html'
for_parents = False
- librarian2_api = True
def build(self, fieldfile):
from django.core.files.base import ContentFile
book = fieldfile.instance
- html_output = self.transform(book.wldocument2(), book)
+ html_output = self.transform(book.wldocument(parse_dublincore=False), book)
# Delete old fragments, create from scratch if necessary.
book.fragments.all().delete()
@staticmethod
def transform(wldoc, book):
- from librarian.builders.html import HtmlBuilder
- url = wldoc.meta.url
- if not url:
+ # 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:
- gal_url = gallery_url(slug=url.slug)
- gal_path = gallery_path(slug=url.slug)
- return HtmlBuilder(gallery_path=gal_path, gallery_url=gal_url, base_url=absolute_url(gal_url)).build(wldoc)
+ 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))
class CoverField(EbookField):
--- /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.core.management.base import BaseCommand
+
+from catalogue.models import Book
+
+
+class Command(BaseCommand):
+ def handle(self, *args, **options):
+ for b in Book.objects.order_by('slug'):
+ print(b.slug)
+ b.load_abstract()
+ b.save()
--- /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.core.management import BaseCommand
+from catalogue.models import Tag
+
+
+class Command(BaseCommand):
+ help = "Update description for given tag."
+
+ def add_arguments(self, parser):
+ parser.add_argument('category')
+ parser.add_argument('slug')
+ parser.add_argument('description_filename')
+
+ def handle(self, category, slug, description_filename, **options):
+ tag = Tag.objects.get(category=category, slug=slug)
+ description = open(description_filename).read().decode('utf-8')
+ tag.description = description
+ tag.save()
+++ /dev/null
-# Generated by Django 4.0.8 on 2024-09-17 14:14
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('catalogue', '0048_remove_collection_kind_remove_tag_for_books_and_more'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='snippet',
- name='anchor',
- field=models.CharField(default='', max_length=64),
- preserve_default=False,
- ),
- ]
from fnpdjango.storage import BofhFileSystemStorage
from lxml import html
from librarian.cover import WLCover
-from librarian.builders.html import AbstraktHtmlBuilder
+from librarian.html import transform_abstrakt
from librarian.builders import builders
from newtagging import managers
from catalogue import constants
return int(total)
def get_time(self):
- try:
- return round(self.xml_file.size / 1000 * 40)
- except ValueError:
- return 0
+ return round(self.xml_file.size / 1000 * 40)
def has_media(self, type_):
if type_ in Book.formats:
urlretrieve('%s/%s' % (remote_gallery_url, ilustr_src), ilustr_path)
def load_abstract(self):
- self.abstract = AbstraktHtmlBuilder().build(
- self.wldocument2()).get_bytes().decode('utf-8')
+ abstract = self.wldocument(parse_dublincore=False).edoc.getroot().find('.//abstrakt')
+ if abstract is not None:
+ self.abstract = transform_abstrakt(abstract)
+ else:
+ self.abstract = ''
def load_toc(self):
self.toc = ''
cls.published.send(sender=cls, instance=book)
return book
- # TODO TEST
def update_references(self):
Entity = apps.get_model('references', 'Entity')
doc = self.wldocument2()
- doc.assign_ids()
-
+ doc._compat_assign_section_ids()
+ doc._compat_assign_ordered_ids()
refs = {}
- for i, ref_elem in enumerate(doc.references()):
+ for ref_elem in doc.references():
uri = ref_elem.attrib.get('href', '')
if not uri:
continue
refs[uri] = ref
if not ref_created:
ref.occurence_set.all().delete()
- anchor = ref_elem.get_link()
-
+ sec = ref_elem.get_link()
+ m = re.match(r'sec(\d+)', sec)
+ assert m is not None
+ sec = int(m.group(1))
snippet = ref_elem.get_snippet()
b = builders['html-snippet']()
for s in snippet:
html = b.output().get_bytes().decode('utf-8')
ref.occurence_set.create(
- section=i,
- anchor=anchor,
+ section=sec,
html=html
)
self.reference_set.exclude(entity__uri__in=refs).delete()
class Snippet(models.Model):
book = models.ForeignKey('Book', models.CASCADE)
sec = models.IntegerField()
- anchor = models.CharField(max_length=64)
text = models.TextField()
search_vector = SearchVectorField()
@register.simple_tag
def related_books_2022(book=None, limit=4, taken=0):
limit -= taken
- if limit < 0:
- return []
max_books = limit
books_qs = Book.objects.filter(findable=True)
# 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
q = Q(expires_at__isnull=False, expires_at__lt=Now())
+class ActiveFilter(YesNoFilter):
+ title = 'płatność aktualna'
+ parameter_name = 'active'
+ q = Q(expires_at__gt=Now())
+
+
class ScheduleForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
(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
+ )
search_fields = ['email', 'source']
list_filter = [
'is_cancelled', 'monthly', 'yearly', 'method',
- PayedFilter, ExpiredFilter,
- SourceFilter,
+ PayedFilter, ActiveFilter, ExpiredFilter,
+ SourceFilter, CrisisFilter
]
filter_horizontal = ['consent']
date_hierarchy = 'started_at'
'monthly'
]
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args, referer=None, **kwargs):
+ self.referer = referer
super().__init__(*args, **kwargs)
club = models.Club.objects.first()
if club is not None:
return state
+ def save(self, *args, **kwargs):
+ self.instance.source = self.referer
+ return super().save(*args, **kwargs)
class DonationStep2Form(forms.ModelForm, NewsletterForm):
'monthly': forms.HiddenInput,
}
- def __init__(self, referer=None, **kwargs):
- self.referer = referer
+ def __init__(self, **kwargs):
super().__init__(**kwargs)
self.fields['first_name'].required = True
self.fields['last_name'].required = True
- self.fields['phone'].required = True
self.consent = []
for c in models.Consent.objects.filter(active=True).order_by('order'):
c, key, (lambda k: lambda: self[k])(key)
))
-
-
def save(self, *args, **kwargs):
NewsletterForm.save(self, *args, **kwargs)
- self.instance.source = self.referer or ''
instance = super().save(*args, **kwargs)
consents = []
{{ form.email.errors }}
</div>
<div class="l-checkout__input">
- <label for="id_phone"><span>*</span> {% trans "Telefon" %}</label>
+ <label for="id_phone">{% trans "Telefon" %}</label>
{{ form.phone }}
{{ form.phone.errors }}
</div>
<main class="l-main">
+ {% comment %}
+ <!-- TODO: hide when crisis banner on -->
<div class="l-checkout__support">
<div class="l-checkout__support__bar">
<span data-label="{% club_count_recurring as c %}{% blocktrans %}Jest nas {{ c }}{% endblocktrans %}" style="width: calc({{ c }}% / 5);"></span>
<p>{% blocktrans with c=500 %}Potrzebujemy <strong>{{ c }}</strong> regularnych darczyńców, by Wolne Lektury mogły działać!{% endblocktrans %}</p>
</div>
</div>
+ {% endcomment %}
<div class="l-checkout__box">
<div class="l-checkout__box__header">
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
- #kwargs['referer'] = self.request.META.get('HTTP_REFERER', '')
+ kwargs['referer'] = self.request.META.get('HTTP_REFERER', '')
return kwargs
def form_valid(self, form):
'pe': _("przypisy redaktorów Wolnych Lektur"),
'pr': _("przypisy źródła"),
'pt': _("przypisy tłumacza"),
+ 'ptrad': _("przypisy tradycyjne"),
}
+++ /dev/null
-# Generated by Django 4.0.8 on 2024-09-18 11:29
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('references', '0002_remove_reference_first_section_occurence'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='occurence',
- name='anchor',
- field=models.CharField(default='', max_length=64),
- preserve_default=False,
- ),
- ]
+++ /dev/null
-# Generated by Django 4.0.8 on 2024-09-18 11:29
-
-from django.db import migrations, models
-from django.db.models.functions import Concat
-
-
-def update_anchor(apps, schema_editor):
- Occurence = apps.get_model('references', 'Occurence')
- Occurence.objects.filter(anchor='').update(
- anchor=Concat(models.Value('sec'), 'section')
- )
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('references', '0003_occurence_anchor'),
- ]
-
- operations = [
- migrations.RunPython(update_anchor, migrations.RunPython.noop)
- ]
class Occurence(models.Model):
reference = models.ForeignKey(Reference, models.CASCADE)
section = models.IntegerField()
- anchor = models.CharField(max_length=64)
html = models.TextField()
class Meta:
</div>
{% for occ in ref.occurence_set.all %}
- <a target="_blank" href="/katalog/lektura/{{ ref.book.slug }}.html#{{ occ.anchor }}" class="c-search-result-fragment-text">
+ <a target="_blank" href="/katalog/lektura/{{ ref.book.slug }}.html#sec{{ occ.section }}" class="c-search-result-fragment-text">
{{ occ.html|safe }}
</a>
{% endfor %}
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
import re
-from librarian.elements.base import WLElement
-from librarian.document import WLDocument
+from librarian.parser import WLDocument
from lxml import etree
skip_header_tags = ['autor_utworu', 'nazwa_utworu', 'dzielo_nadrzedne',
'{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF']
+ @classmethod
+ def get_master(cls, root):
+ """
+ Returns the first master tag from an etree.
+ """
+ for master in root.iter():
+ if master.tag in cls.master_tags:
+ return master
+
@staticmethod
- def add_snippet(book, text, position, anchor):
+ def add_snippet(book, text, position):
book.snippet_set.create(
sec=position + 1,
- text=text,
- anchor=anchor
+ text=text
)
- # TODO: The section links stuff won't work.
@classmethod
def index_book(cls, book):
"""
book.snippet_set.all().delete()
- wld = WLDocument(filename=book.xml_file.path)
- wld.assign_ids()
+ wld = WLDocument.from_file(book.xml_file.path, parse_dublincore=False)
+ root = wld.edoc.getroot()
- master = wld.tree.getroot().master
+ master = cls.get_master(root)
if master is None:
return []
- def get_indexable(element):
- for child in element:
- if not isinstance(child, WLElement):
- continue
- if not child.attrib.get('_id'):
- for e in get_indexable(child):
- yield e
- else:
- yield child
-
def walker(node):
if node.tag not in cls.ignore_content_tags:
yield node, None, None
return re.sub("(?m)/$", "", text)
- for position, header in enumerate(get_indexable(master)):
+ for position, header in enumerate(master):
if header.tag in cls.skip_header_tags:
continue
if header.tag is etree.Comment:
continue
- el_id = header.attrib['_id']
-
# section content
content = []
footnote = []
handle_text.append(collect_footnote)
elif end is not None and footnote is not [] and end.tag in cls.footnote_tags:
handle_text.pop()
- cls.add_snippet(book, ''.join(footnote), position, el_id)
+ cls.add_snippet(book, ''.join(footnote), position)
footnote = []
if text is not None and handle_text is not []:
hdl(text)
# in the end, add a section text.
- cls.add_snippet(book, fix_format(content), position, el_id)
+ cls.add_snippet(book, fix_format(content), position)
{{ book.title }}
</a>
{% for f in snippets %}
- <a class="c-search-result-fragment-text" href='{% url 'book_text' f.book.slug %}#{{ f.anchor }}'>
+ <a class="c-search-result-fragment-text" href='{% url 'book_text' f.book.slug %}#sec{{ f.sec }}'>
{{ f.headline|safe }}
</a>
{% endfor %}
class ShelfView(ListAPIView):
permission_classes = [IsAuthenticated]
serializer_class = BookSerializer
+ pagination_class = None
def get_queryset(self):
state = self.kwargs['state']
'api.renderers.LegacyXMLRenderer',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
+ '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',
}
}
}
}
+
+
+.annoy-banner_crisis-container {
+ position: sticky;
+ top: 0;
+ height: 160px;
+ z-index: 10;
+ box-shadow: 0 0 10px black;
+ display: flex;
+ background: #c32721;
+ color: black;
+ align-items:center;
+ cursor: pointer;
+
+ @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 {
+ position: sticky;
+ top: 0;
+ width: 100%;
+
+ .annoy-banner-inner {
+ max-width: 1172px;
+ margin: auto;
+ padding-right: 16px;
+ padding-left: 16px;
+
+ display: flex;
+ gap: 20px;
+ align-items: flex-start;
+
+ .image-box {
+ position: relative;
+ img {
+ height: 159px;
+ display: block;
+
+ @media screen and (max-width: 700px) {
+ height: 120px;
+ }
+ }
+ }
+
+ .text-box {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ @media screen and (max-width: 700px) {
+ p {
+ font-size: .9em;
+ }
+ }
+
+ .text {
+ background: #edc016;
+ padding: 1em;
+ border: 3px solid black;
+ }
+ a {
+ color: #c32721;
+ }
+ .state-box {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ @media screen and (max-width: 700px) {
+ flex-direction: column;
+ align-items: stretch;
+ text-align: center;
+ }
+ .progress-box {
+ flex-grow: 1;
+
+ .l-checkout__support__bar span::after {
+ color: black;
+ }
+ }
+ .time-box {
+ width: 110px;
+ margin: auto;
+ }
+ .countdown {
+ margin: 0;
+ }
+ }
+ }
+
+ p {
+ margin: 0;
+ }
+
+ a.action {
+ background: #edc016;
+ color: black;
+ padding: .8em 1em;
+ border: 3px solid black;
+ border-radius: 10px;
+ display: block;
+ transition: background-color .2s;
+
+ &:hover {
+ background: #ffd430;
+ text-decoration: none;
+ }
+ }
+ }
+ }
+ &.annoy-banner-style_crisis_quiet {
+ background: black;
+ color: white;
+ .annoy-banner-inner {
+ align-items: center;
+
+ .text-box {
+ .text {
+ background: none;
+ padding: 0;
+ border: none;
+ a {
+ color: #ffd430;
+ }
+ }
+ }
+ }
+ }
+}
h3, .subtitle2 {
font-size: 1.5em;
- margin: 1.5em 0 0;
+ margin: 1.5em 0 1em 0;
font-weight: normal;
line-height: 1.5em;
}
margin: 0;
text-indent: 1.5em;
}
+p.paragraph.werset {
+ text-indent: 0;
+ margin-bottom: 1em;
+}
.motto {
text-align: justify;
margin: 1.5em 0 0;
}
+.paragraph {
+ &.miejsce_data, &.pozdrowienie, &.podpis {
+ text-align: right;
+ }
+ &.naglowek_listu {
+ text-align: center;
+ margin: .5em 0;
+ }
+}
+
+
p.motto_podpis {
font-size: 0.875em;
text-align: right;
em.math, em.foreign-word, em.book-title, em.didaskalia {
font-style: italic;
}
+em.foreign-word.foreign-word-protected {
+ font-style: normal;
+}
em.author-emphasis {
letter-spacing: 0.1em;
font-size: 18px;
font-style: normal;
font-weight: 400;
- line-height: 28px; /* 155.556% */
+ line-height: 28px; /* 155.556% */
+
+ em.foreign-word.foreign-word-protected {
+ font-style: normal;
+ }
}
#footnote-link {
$('a.anchor').on('click', function(e) {
// Workaround for bad TOC markers.
- if (($this).closest('#toc').length) return;
+ if ($(this).closest('#toc').length) return;
+ if ($(this).closest('#wltoc').length) return;
e.preventDefault();
let sel = window.getSelection();
let range = document.createRange();
let $p = $(this).nextAll('.paragraph').first()
- range.selectNode($p[0]);
+ range.selectNodeContents($p[0]);
sel.addRange(range);
qbox.showForSelection(sel);
$(".c-media__settings").toggleClass('active');
});
+ const crisis = document.querySelector(".annoy-banner_crisis-container");
+ const crisisLink = document.querySelector('.annoy-banner_crisis-container a.action');
+ if (crisis) {
+ crisis.addEventListener("click", function() {
+ crisisLink.click();
+ });
+ }
+
})();
{% load latest_blog_posts from blog %}
{% load preview_ad from catalogue_tags %}
+{% annoy_banner_crisis %}
+
{% annoy_banner_blackout %}
<nav class="l-navigation">