From: Radek Czajka Date: Wed, 22 Jan 2025 14:21:58 +0000 (+0100) Subject: Some more data in api. X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/refs/heads/master?ds=inline;hp=4faaa8414a1038804df9999a094c8dfe4c5f513f Some more data in api. --- diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 54b199b6f..6b650e391 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -3,20 +3,20 @@ # 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 @@ -26,36 +26,36 @@ oauthlib>=3.0.1,<3.1 # 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 diff --git a/src/annoy/apps.py b/src/annoy/apps.py index 213a4e98e..b2b7eff82 100644 --- a/src/annoy/apps.py +++ b/src/annoy/apps.py @@ -3,3 +3,6 @@ from django.apps import AppConfig class AnnoyConfig(AppConfig): name = 'annoy' + + def ready(self): + from . import signals diff --git a/src/annoy/migrations/0017_banner_progress_banner_target_alter_banner_place.py b/src/annoy/migrations/0017_banner_progress_banner_target_alter_banner_place.py new file mode 100644 index 000000000..62246bf2d --- /dev/null +++ b/src/annoy/migrations/0017_banner_progress_banner_target_alter_banner_place.py @@ -0,0 +1,28 @@ +# 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'), + ), + ] diff --git a/src/annoy/migrations/0018_alter_banner_style.py b/src/annoy/migrations/0018_alter_banner_style.py new file mode 100644 index 000000000..45ed1e6b1 --- /dev/null +++ b/src/annoy/migrations/0018_alter_banner_style.py @@ -0,0 +1,18 @@ +# 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'), + ), + ] diff --git a/src/annoy/models.py b/src/annoy/models.py index e60e3c024..0d887920d 100644 --- a/src/annoy/models.py +++ b/src/annoy/models.py @@ -13,7 +13,6 @@ class Banner(models.Model): 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) @@ -33,6 +32,8 @@ class Banner(models.Model): 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) @@ -49,10 +50,10 @@ class Banner(models.Model): 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: @@ -78,6 +79,33 @@ class Banner(models.Model): 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') @@ -146,12 +174,3 @@ class MediaInsertText(models.Model): 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() diff --git a/src/annoy/places.py b/src/annoy/places.py index c9f8031af..8fa767bba 100644 --- a/src/annoy/places.py +++ b/src/annoy/places.py @@ -8,6 +8,10 @@ PLACE_DEFINITIONS = [ # ('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] @@ -22,6 +26,6 @@ STYLES = [] 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] ]) diff --git a/src/annoy/signals.py b/src/annoy/signals.py new file mode 100644 index 000000000..c98528247 --- /dev/null +++ b/src/annoy/signals.py @@ -0,0 +1,18 @@ +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 diff --git a/src/annoy/templates/annoy/banner_crisis.html b/src/annoy/templates/annoy/banner_crisis.html new file mode 100644 index 000000000..9feffbf5f --- /dev/null +++ b/src/annoy/templates/annoy/banner_crisis.html @@ -0,0 +1,62 @@ +{% load l10n %} +{% load time_tags %} + +{% if banner %} +
+
+
+ +
+ {% if banner.image %} + + {% endif %} +
+ +
+
+ {{ banner.get_text|safe|linebreaks }} +
+ +
+
+
+
+ +
+
+
+
+   +
+
+ {% if banner.action_label %} + + {{ banner.action_label }} + + {% endif %} +
+
+
+ + + +
+
+
+ +{% endif %} + diff --git a/src/annoy/templatetags/annoy.py b/src/annoy/templatetags/annoy.py index 2bc93143c..40f7511a3 100644 --- a/src/annoy/templatetags/annoy.py +++ b/src/annoy/templatetags/annoy.py @@ -29,3 +29,12 @@ def annoy_banners(context, place): '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, + } diff --git a/src/api/drf_auth.py b/src/api/drf_auth.py index adee780d3..813d9298e 100644 --- a/src/api/drf_auth.py +++ b/src/api/drf_auth.py @@ -2,9 +2,10 @@ # 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): @@ -21,3 +22,7 @@ class PistonOAuthAuthentication(BaseAuthentication): ) if v: return r.token.user, r.token + + +class WLTokenAuthentication(TokenAuthentication): + model = Token diff --git a/src/api/fields.py b/src/api/fields.py index cdfcc4735..728323473 100644 --- a/src/api/fields.py +++ b/src/api/fields.py @@ -3,7 +3,7 @@ # 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 @@ -20,13 +20,14 @@ class AbsoluteURLField(serializers.ReadOnlyField): 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: diff --git a/src/api/migrations/0007_alter_token_consumer.py b/src/api/migrations/0007_alter_token_consumer.py new file mode 100644 index 000000000..410516a4b --- /dev/null +++ b/src/api/migrations/0007_alter_token_consumer.py @@ -0,0 +1,19 @@ +# 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'), + ), + ] diff --git a/src/api/models.py b/src/api/models.py index 04d08e96d..4f0556561 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -101,7 +101,12 @@ class Consumer(models.Model): 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) @@ -112,7 +117,7 @@ class Token(models.Model): 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) diff --git a/src/api/pagination.py b/src/api/pagination.py new file mode 100644 index 000000000..0c4ae09d4 --- /dev/null +++ b/src/api/pagination.py @@ -0,0 +1,14 @@ +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(), + } + }) diff --git a/src/api/serializers.py b/src/api/serializers.py index e0bf0eeee..4ba660e26 100644 --- a/src/api/serializers.py +++ b/src/api/serializers.py @@ -15,13 +15,30 @@ class PlainSerializer(serializers.ModelSerializer): 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'}) diff --git a/src/api/tests/res/responses/books.xml b/src/api/tests/res/responses/books.xml index f39864480..1eb4a97d6 100644 --- a/src/api/tests/res/responses/books.xml +++ b/src/api/tests/res/responses/books.xml @@ -1,2 +1,2 @@ -$child$2Childhttp://testserver/katalog/lektura/child/#000000http://testserver/api/books/child/FalseWierszchild$grandchild$3Grandchildhttp://testserver/katalog/lektura/grandchild/#000000http://testserver/api/books/grandchild/FalseSonetgrandchildLirykajohn doe$parent$1Parenthttp://testserver/katalog/lektura/parent/#a6820aJohn Doecover/parent.jpgBarokhttp://testserver/api/books/parent/TrueSonethttp://testserver/media/cover_api_thumb/parent.jpgparentcover_thumb/parent.jpg +$child$2Childhttp://testserver/katalog/lektura/child/#000000http://testserver/api/books/child/?format=xmlFalseWierszchild$grandchild$3Grandchildhttp://testserver/katalog/lektura/grandchild/#000000http://testserver/api/books/grandchild/?format=xmlFalseSonetgrandchildLirykajohn doe$parent$1Parenthttp://testserver/katalog/lektura/parent/#a6820aJohn Doecover/parent.jpgBarokhttp://testserver/api/books/parent/?format=xmlTrueSonethttp://testserver/media/cover_api_thumb/parent.jpgparentcover_thumb/parent.jpg diff --git a/src/api/urls.py b/src/api/urls.py index 1f05e2d51..de3dba77f 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -9,10 +9,23 @@ from stats.utils import piwik_track_view 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'), @@ -36,4 +49,5 @@ urlpatterns = [ path('', include('social.api.urls')), path('', include('catalogue.api.urls')), + ] diff --git a/src/api/views.py b/src/api/views.py index 64a7eb0ee..08d1650e7 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -1,21 +1,23 @@ # 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 @@ -131,6 +133,59 @@ class OAuth1AccessTokenView(View): ) +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] @@ -169,3 +224,64 @@ class BookUserDataView(RetrieveAPIView): 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 polityce prywatności.' + ] + }) + + 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 diff --git a/src/catalogue/api/serializers.py b/src/catalogue/api/serializers.py index daa86fec8..5f96e0270 100644 --- a/src/catalogue/api/serializers.py +++ b/src/catalogue/api/serializers.py @@ -40,10 +40,124 @@ 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 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') diff --git a/src/catalogue/api/urls2.py b/src/catalogue/api/urls2.py new file mode 100644 index 000000000..8748e2cf1 --- /dev/null +++ b/src/catalogue/api/urls2.py @@ -0,0 +1,43 @@ +# 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//', + 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//', + 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//', + 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//', + 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//', + piwik_track_view(views.GenreView.as_view()), + name='catalogue_api_genre'), +] diff --git a/src/catalogue/api/views.py b/src/catalogue/api/views.py index c0dc57f76..9c8f4f177 100644 --- a/src/catalogue/api/views.py +++ b/src/catalogue/api/views.py @@ -30,6 +30,10 @@ from . import serializers 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. @@ -47,7 +51,7 @@ class CreateOnPutMixin: raise -class CollectionList(ListAPIView): +class CollectionList(LegacyListAPIView): queryset = Collection.objects.filter(listed=True) serializer_class = serializers.CollectionListSerializer @@ -61,7 +65,7 @@ class CollectionDetail(CreateOnPutMixin, RetrieveUpdateAPIView): @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 @@ -179,6 +183,19 @@ class BookList(ListAPIView): 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() @@ -186,13 +203,19 @@ class BookDetail(RetrieveAPIView): 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 @@ -205,7 +228,7 @@ class Preview(ListAPIView): @vary_on_auth # Because of 'liked'. -class FilterBookList(ListAPIView): +class FilterBookList(LegacyListAPIView): serializer_class = serializers.FilterBookListSerializer def parse_bool(self, s): @@ -289,7 +312,7 @@ class EpubView(RetrieveAPIView): return HttpResponse(self.get_object().get_media('epub')) -class TagCategoryView(ListAPIView): +class TagCategoryView(LegacyListAPIView): serializer_class = serializers.TagSerializer def get_queryset(self): @@ -305,6 +328,42 @@ class TagCategoryView(ListAPIView): 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] @@ -373,7 +432,7 @@ class TagView(RetrieveAPIView): @vary_on_auth # Because of 'liked'. -class FragmentList(ListAPIView): +class FragmentList(LegacyListAPIView): serializer_class = serializers.FragmentSerializer def get_queryset(self): diff --git a/src/catalogue/fields.py b/src/catalogue/fields.py index ebe5cf478..9c5696fa6 100644 --- a/src/catalogue/fields.py +++ b/src/catalogue/fields.py @@ -229,12 +229,10 @@ class XmlField(EbookField): 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): @@ -301,7 +299,6 @@ class MobiField(EbookField): class HtmlField(EbookField): ext = 'html' for_parents = False - librarian2_api = True def build(self, fieldfile): from django.core.files.base import ContentFile @@ -312,7 +309,7 @@ class HtmlField(EbookField): 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() @@ -388,15 +385,17 @@ class HtmlField(EbookField): @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): diff --git a/src/catalogue/management/commands/load_abstracts.py b/src/catalogue/management/commands/load_abstracts.py new file mode 100644 index 000000000..f9fb2c155 --- /dev/null +++ b/src/catalogue/management/commands/load_abstracts.py @@ -0,0 +1,14 @@ +# 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() diff --git a/src/catalogue/management/commands/update_tag_description.py b/src/catalogue/management/commands/update_tag_description.py new file mode 100644 index 000000000..0fb2ffd95 --- /dev/null +++ b/src/catalogue/management/commands/update_tag_description.py @@ -0,0 +1,20 @@ +# 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() diff --git a/src/catalogue/migrations/0049_snippet_anchor.py b/src/catalogue/migrations/0049_snippet_anchor.py deleted file mode 100644 index c3e11d121..000000000 --- a/src/catalogue/migrations/0049_snippet_anchor.py +++ /dev/null @@ -1,19 +0,0 @@ -# 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, - ), - ] diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 7d3666277..29e375464 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -19,7 +19,7 @@ from django.utils.translation import gettext_lazy as _, get_language 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 @@ -327,10 +327,7 @@ class Book(models.Model): 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: @@ -557,8 +554,11 @@ class Book(models.Model): 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 = '' @@ -717,14 +717,13 @@ class Book(models.Model): 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 @@ -743,8 +742,10 @@ class Book(models.Model): 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: @@ -752,8 +753,7 @@ class Book(models.Model): 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() diff --git a/src/catalogue/models/snippet.py b/src/catalogue/models/snippet.py index ec7e2f9d1..4c25b8c97 100644 --- a/src/catalogue/models/snippet.py +++ b/src/catalogue/models/snippet.py @@ -7,7 +7,6 @@ from search.utils import UnaccentSearchVector 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() diff --git a/src/catalogue/templatetags/catalogue_tags.py b/src/catalogue/templatetags/catalogue_tags.py index d70ad970e..d2298d9f5 100644 --- a/src/catalogue/templatetags/catalogue_tags.py +++ b/src/catalogue/templatetags/catalogue_tags.py @@ -307,8 +307,6 @@ def plain_list(context, object_list, with_initials=True, by_author=False, choice @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) diff --git a/src/catalogue/test_utils.py b/src/catalogue/test_utils.py index f5d98cab5..a14552287 100644 --- a/src/catalogue/test_utils.py +++ b/src/catalogue/test_utils.py @@ -42,6 +42,9 @@ class BookInfoStub: # allow single definition for multiple-value fields _salias = { 'authors': 'author', + 'genres': 'genre', + 'epochs': 'epoch', + 'kinds': 'kind', } def __init__(self, **kwargs): diff --git a/src/club/admin.py b/src/club/admin.py index 8e4380eab..442592803 100644 --- a/src/club/admin.py +++ b/src/club/admin.py @@ -10,6 +10,7 @@ from django.utils.html import conditional_escape 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 @@ -67,6 +68,12 @@ class ExpiredFilter(YesNoFilter): 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) @@ -89,14 +96,35 @@ class SourceFilter(admin.SimpleListFilter): (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 + ) @@ -111,8 +139,8 @@ class ScheduleAdmin(admin.ModelAdmin): 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' diff --git a/src/club/forms.py b/src/club/forms.py index df92524b9..b75877d49 100644 --- a/src/club/forms.py +++ b/src/club/forms.py @@ -31,7 +31,8 @@ class DonationStep1Form(forms.ModelForm): '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: @@ -48,6 +49,9 @@ class DonationStep1Form(forms.ModelForm): return state + def save(self, *args, **kwargs): + self.instance.source = self.referer + return super().save(*args, **kwargs) class DonationStep2Form(forms.ModelForm, NewsletterForm): @@ -64,13 +68,11 @@ 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'): @@ -83,11 +85,8 @@ class DonationStep2Form(forms.ModelForm, NewsletterForm): 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 = [] diff --git a/src/club/templates/club/donation_step2.html b/src/club/templates/club/donation_step2.html index 623cd0c4e..3864ad5a5 100644 --- a/src/club/templates/club/donation_step2.html +++ b/src/club/templates/club/donation_step2.html @@ -48,7 +48,7 @@ {{ form.email.errors }}
- + {{ form.phone }} {{ form.phone.errors }}
diff --git a/src/club/templates/club/donation_step_base.html b/src/club/templates/club/donation_step_base.html index 52256fecd..cede1b4ca 100644 --- a/src/club/templates/club/donation_step_base.html +++ b/src/club/templates/club/donation_step_base.html @@ -14,6 +14,8 @@
+ {% comment %} +
@@ -23,6 +25,7 @@

{% blocktrans with c=500 %}Potrzebujemy {{ c }} regularnych darczyńców, by Wolne Lektury mogły działać!{% endblocktrans %}

+ {% endcomment %}
diff --git a/src/club/views.py b/src/club/views.py index 1ab1eae06..c6136b67f 100644 --- a/src/club/views.py +++ b/src/club/views.py @@ -99,7 +99,7 @@ class JoinView(CreateView): 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): diff --git a/src/dictionary/constants.py b/src/dictionary/constants.py index 2cef90bf9..59c8b910e 100644 --- a/src/dictionary/constants.py +++ b/src/dictionary/constants.py @@ -9,4 +9,5 @@ FN_TYPES = { 'pe': _("przypisy redaktorów Wolnych Lektur"), 'pr': _("przypisy źródła"), 'pt': _("przypisy tłumacza"), + 'ptrad': _("przypisy tradycyjne"), } diff --git a/src/references/migrations/0003_occurence_anchor.py b/src/references/migrations/0003_occurence_anchor.py deleted file mode 100644 index 05e60a4a3..000000000 --- a/src/references/migrations/0003_occurence_anchor.py +++ /dev/null @@ -1,19 +0,0 @@ -# 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, - ), - ] diff --git a/src/references/migrations/0004_update_anchor.py b/src/references/migrations/0004_update_anchor.py deleted file mode 100644 index cd971857f..000000000 --- a/src/references/migrations/0004_update_anchor.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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) - ] diff --git a/src/references/models.py b/src/references/models.py index f83518dcd..d4733ffcf 100644 --- a/src/references/models.py +++ b/src/references/models.py @@ -84,7 +84,6 @@ class Reference(models.Model): class Occurence(models.Model): reference = models.ForeignKey(Reference, models.CASCADE) section = models.IntegerField() - anchor = models.CharField(max_length=64) html = models.TextField() class Meta: diff --git a/src/references/templates/references/popup.html b/src/references/templates/references/popup.html index 3655309e3..f8977e6d8 100644 --- a/src/references/templates/references/popup.html +++ b/src/references/templates/references/popup.html @@ -28,7 +28,7 @@
{% for occ in ref.occurence_set.all %} - + {{ occ.html|safe }} {% endfor %} diff --git a/src/search/index.py b/src/search/index.py index 3b0edebf8..e0a727ca2 100644 --- a/src/search/index.py +++ b/src/search/index.py @@ -2,8 +2,7 @@ # 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 @@ -32,15 +31,22 @@ class Index: 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): """ @@ -51,23 +57,13 @@ class Index: 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 @@ -89,14 +85,12 @@ class Index: 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 = [] @@ -116,7 +110,7 @@ class Index: 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 []: @@ -124,4 +118,4 @@ class Index: 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) diff --git a/src/search/templates/search/results.html b/src/search/templates/search/results.html index 2902f2601..c71cf45c8 100644 --- a/src/search/templates/search/results.html +++ b/src/search/templates/search/results.html @@ -126,7 +126,7 @@ {{ book.title }} {% for f in snippets %} - + {{ f.headline|safe }} {% endfor %} diff --git a/src/social/api/views.py b/src/social/api/views.py index c5506ce79..a29930423 100644 --- a/src/social/api/views.py +++ b/src/social/api/views.py @@ -36,6 +36,7 @@ class LikeView(APIView): class ShelfView(ListAPIView): permission_classes = [IsAuthenticated] serializer_class = BookSerializer + pagination_class = None def get_queryset(self): state = self.kwargs['state'] diff --git a/src/wolnelektury/settings/contrib.py b/src/wolnelektury/settings/contrib.py index d03479f82..e5b2f60d6 100644 --- a/src/wolnelektury/settings/contrib.py +++ b/src/wolnelektury/settings/contrib.py @@ -28,9 +28,13 @@ REST_FRAMEWORK = { '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', } diff --git a/src/wolnelektury/static/2022/styles/layout/_annoy.scss b/src/wolnelektury/static/2022/styles/layout/_annoy.scss index f0f06360a..9d90c65ff 100644 --- a/src/wolnelektury/static/2022/styles/layout/_annoy.scss +++ b/src/wolnelektury/static/2022/styles/layout/_annoy.scss @@ -73,3 +73,140 @@ } } } + + +.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; + } + } + } + } + } +} diff --git a/src/wolnelektury/static/2022/styles/layout/_text.scss b/src/wolnelektury/static/2022/styles/layout/_text.scss index d74496800..65bc48aef 100644 --- a/src/wolnelektury/static/2022/styles/layout/_text.scss +++ b/src/wolnelektury/static/2022/styles/layout/_text.scss @@ -293,7 +293,7 @@ h2 { h3, .subtitle2 { font-size: 1.5em; - margin: 1.5em 0 0; + margin: 1.5em 0 1em 0; font-weight: normal; line-height: 1.5em; } @@ -471,6 +471,10 @@ p.paragraph { margin: 0; text-indent: 1.5em; } +p.paragraph.werset { + text-indent: 0; + margin-bottom: 1em; +} .motto { text-align: justify; @@ -478,6 +482,17 @@ p.paragraph { 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; @@ -530,6 +545,9 @@ p.place-and-time { 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; @@ -719,7 +737,11 @@ a.reference.interesting:after { 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 { diff --git a/src/wolnelektury/static/js/book_text/references.js b/src/wolnelektury/static/js/book_text/references.js index 1a284a0eb..fa4a35a69 100644 --- a/src/wolnelektury/static/js/book_text/references.js +++ b/src/wolnelektury/static/js/book_text/references.js @@ -606,7 +606,8 @@ $('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(); @@ -614,7 +615,7 @@ let range = document.createRange(); let $p = $(this).nextAll('.paragraph').first() - range.selectNode($p[0]); + range.selectNodeContents($p[0]); sel.addRange(range); qbox.showForSelection(sel); diff --git a/src/wolnelektury/static/js/main.js b/src/wolnelektury/static/js/main.js index acc38a933..2feb7b8eb 100644 --- a/src/wolnelektury/static/js/main.js +++ b/src/wolnelektury/static/js/main.js @@ -543,4 +543,12 @@ $(".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(); + }); + } + })(); diff --git a/src/wolnelektury/templates/header.html b/src/wolnelektury/templates/header.html index e7a29b807..17e237948 100644 --- a/src/wolnelektury/templates/header.html +++ b/src/wolnelektury/templates/header.html @@ -6,6 +6,8 @@ {% load latest_blog_posts from blog %} {% load preview_ad from catalogue_tags %} +{% annoy_banner_crisis %} + {% annoy_banner_blackout %}