Some more data in api. master
authorRadek Czajka <rczajka@rczajka.pl>
Wed, 22 Jan 2025 14:21:58 +0000 (15:21 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Wed, 22 Jan 2025 14:21:58 +0000 (15:21 +0100)
48 files changed:
requirements/requirements.txt
src/annoy/apps.py
src/annoy/migrations/0017_banner_progress_banner_target_alter_banner_place.py [new file with mode: 0644]
src/annoy/migrations/0018_alter_banner_style.py [new file with mode: 0644]
src/annoy/models.py
src/annoy/places.py
src/annoy/signals.py [new file with mode: 0644]
src/annoy/templates/annoy/banner_crisis.html [new file with mode: 0644]
src/annoy/templatetags/annoy.py
src/api/drf_auth.py
src/api/fields.py
src/api/migrations/0007_alter_token_consumer.py [new file with mode: 0644]
src/api/models.py
src/api/pagination.py [new file with mode: 0644]
src/api/serializers.py
src/api/tests/res/responses/books.xml
src/api/urls.py
src/api/views.py
src/catalogue/api/serializers.py
src/catalogue/api/urls2.py [new file with mode: 0644]
src/catalogue/api/views.py
src/catalogue/fields.py
src/catalogue/management/commands/load_abstracts.py [new file with mode: 0644]
src/catalogue/management/commands/update_tag_description.py [new file with mode: 0644]
src/catalogue/migrations/0049_snippet_anchor.py [deleted file]
src/catalogue/models/book.py
src/catalogue/models/snippet.py
src/catalogue/templatetags/catalogue_tags.py
src/catalogue/test_utils.py
src/club/admin.py
src/club/forms.py
src/club/templates/club/donation_step2.html
src/club/templates/club/donation_step_base.html
src/club/views.py
src/dictionary/constants.py
src/references/migrations/0003_occurence_anchor.py [deleted file]
src/references/migrations/0004_update_anchor.py [deleted file]
src/references/models.py
src/references/templates/references/popup.html
src/search/index.py
src/search/templates/search/results.html
src/social/api/views.py
src/wolnelektury/settings/contrib.py
src/wolnelektury/static/2022/styles/layout/_annoy.scss
src/wolnelektury/static/2022/styles/layout/_text.scss
src/wolnelektury/static/js/book_text/references.js
src/wolnelektury/static/js/main.js
src/wolnelektury/templates/header.html

index 54b199b..6b650e3 100644 (file)
@@ -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
 
index 213a4e9..b2b7eff 100644 (file)
@@ -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 (file)
index 0000000..62246bf
--- /dev/null
@@ -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 (file)
index 0000000..45ed1e6
--- /dev/null
@@ -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'),
+        ),
+    ]
index e60e3c0..0d88792 100644 (file)
@@ -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()
index c9f8031..8fa767b 100644 (file)
@@ -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 (file)
index 0000000..c985282
--- /dev/null
@@ -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 (file)
index 0000000..9feffbf
--- /dev/null
@@ -0,0 +1,62 @@
+{% 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 }}'>&nbsp;</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 %}
+
index 2bc9314..40f7511 100644 (file)
@@ -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,
+    }
index adee780..813d929 100644 (file)
@@ -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
index cdfcc47..7283234 100644 (file)
@@ -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 (file)
index 0000000..410516a
--- /dev/null
@@ -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'),
+        ),
+    ]
index 04d08e9..4f05565 100644 (file)
@@ -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 (file)
index 0000000..0c4ae09
--- /dev/null
@@ -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(),
+            }
+        })
index e0bf0ee..4ba660e 100644 (file)
@@ -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'})
index f398644..1eb4a97 100644 (file)
@@ -1,2 +1,2 @@
 <?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>
index 1f05e2d..de3dba7 100644 (file)
@@ -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')),
+
 ]
index 64a7eb0..08d1650 100644 (file)
@@ -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 <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
index daa86fe..5f96e02 100644 (file)
@@ -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 (file)
index 0000000..8748e2c
--- /dev/null
@@ -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/<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'),
+]
index c0dc57f..9c8f4f1 100644 (file)
@@ -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):
index ebe5cf4..9c5696f 100644 (file)
@@ -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 (file)
index 0000000..f9fb2c1
--- /dev/null
@@ -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 (file)
index 0000000..0fb2ffd
--- /dev/null
@@ -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 (file)
index c3e11d1..0000000
+++ /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,
-        ),
-    ]
index 7d36662..29e3754 100644 (file)
@@ -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()
index ec7e2f9..4c25b8c 100644 (file)
@@ -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()
 
index d70ad97..d2298d9 100644 (file)
@@ -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)
index f5d98ca..a145522 100644 (file)
@@ -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):
index 8e4380e..4425928 100644 (file)
@@ -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'
index df92524..b75877d 100644 (file)
@@ -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 = []
index 623cd0c..3864ad5 100644 (file)
@@ -48,7 +48,7 @@
               {{ 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>
index 52256fe..cede1b4 100644 (file)
@@ -14,6 +14,8 @@
 
 
   <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>
@@ -23,6 +25,7 @@
         <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">
index 1ab1eae..c6136b6 100644 (file)
@@ -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):
index 2cef90b..59c8b91 100644 (file)
@@ -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 (file)
index 05e60a4..0000000
+++ /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 (file)
index cd97185..0000000
+++ /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)
-    ]
index f83518d..d4733ff 100644 (file)
@@ -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:
index 3655309..f8977e6 100644 (file)
@@ -28,7 +28,7 @@
     </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 %}
index 3b0edeb..e0a727c 100644 (file)
@@ -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)
index 2902f26..c71cf45 100644 (file)
               {{ 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 %}
index c5506ce..a299304 100644 (file)
@@ -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']
index d03479f..e5b2f60 100644 (file)
@@ -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',
 }
 
 
index f0f0636..9d90c65 100644 (file)
         }
     }
 }
+
+
+.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;
+                   }
+               }
+           }
+       }
+    }
+}
index d744968..65bc48a 100644 (file)
@@ -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 {
index 1a284a0..fa4a35a 100644 (file)
 
     $('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);
index acc38a9..2feb7b8 100644 (file)
         $(".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();
+       });
+    }
+
 })();
index e7a29b8..17e2379 100644 (file)
@@ -6,6 +6,8 @@
 {% load latest_blog_posts from blog %}
 {% load preview_ad from catalogue_tags %}
 
+{% annoy_banner_crisis %}
+
 {% annoy_banner_blackout %}
 
 <nav class="l-navigation">