donation fixes master
authorRadek Czajka <rczajka@rczajka.pl>
Fri, 12 Dec 2025 14:55:21 +0000 (15:55 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Fri, 12 Dec 2025 14:55:21 +0000 (15:55 +0100)
57 files changed:
Dockerfile
Makefile
docker-compose.yml
manage
requirements/requirements.txt
src/annoy/admin.py
src/annoy/migrations/0019_campaign_banner_books_alter_banner_place_and_more.py [new file with mode: 0644]
src/annoy/models.py
src/annoy/places.py
src/annoy/templates/annoy/banner.html
src/annoy/templatetags/annoy.py
src/catalogue/api/serializers.py
src/catalogue/api/urls2.py
src/catalogue/api/views.py
src/catalogue/migrations/0050_book_narrators.py [new file with mode: 0644]
src/catalogue/migrations/0051_book_has_audio.py [new file with mode: 0644]
src/catalogue/models/book.py
src/catalogue/models/bookmedia.py
src/catalogue/static/book/filter.js
src/catalogue/static/player/player.js
src/catalogue/templates/catalogue/book_box.html
src/catalogue/templates/catalogue/book_detail.html
src/catalogue/templates/catalogue/book_list.html
src/catalogue/templates/catalogue/snippets/jplayer.html
src/catalogue/templates/catalogue/snippets/jplayer_link.html [deleted file]
src/catalogue/views.py
src/club/admin.py
src/club/forms.py
src/club/models.py
src/club/templates/club/donation_infobox.html [new file with mode: 0644]
src/club/templates/club/donation_step1_form.html
src/club/templates/club/donation_step2.html
src/club/templates/club/donation_step3.html
src/club/templates/club/donation_step_base.html
src/club/templates/club/payment/paypal_invite.html
src/club/templates/payu/rec_widget.html
src/club/urls.py
src/club/views.py
src/references/models.py
src/social/views.py
src/wolnelektury/settings/basic.py
src/wolnelektury/settings/contrib.py
src/wolnelektury/settings/custom.py
src/wolnelektury/static/2022/styles/components/_media.scss
src/wolnelektury/static/2022/styles/components/_player.scss
src/wolnelektury/static/2022/styles/layout/_annoy.scss
src/wolnelektury/static/2022/styles/layout/_button.scss
src/wolnelektury/static/2022/styles/layout/_checkout.scss
src/wolnelektury/static/2022/styles/layout/_navigation.scss
src/wolnelektury/static/2022/styles/layout/_text.scss
src/wolnelektury/static/2022/styles/reader_player.scss
src/wolnelektury/static/img/elevenreader-21.png [new file with mode: 0644]
src/wolnelektury/static/img/elevenreader.png [new file with mode: 0644]
src/wolnelektury/static/js/book_text/progress.js
src/wolnelektury/static/js/book_text/references.js
src/wolnelektury/static/js/main.js
src/wolnelektury/templates/user_actions.html

index 032024b..f916be0 100644 (file)
@@ -1,8 +1,5 @@
 FROM python:3.8 AS base
 
-ARG UID=1000
-ARG GID=1000
-
 RUN     apt-get update && apt-get install -y \
        git \
        calibre \
@@ -21,9 +18,6 @@ RUN pip install --no-cache-dir \
     django-debug-toolbar==3.2.2 \
     python-bidi
 
-RUN addgroup --gid $GID app
-RUN adduser --gid $GID --home /app --uid $UID app
-
 RUN     apt-get install -y \
        texlive-extra-utils \
        texlive-lang-greek \
@@ -34,10 +28,8 @@ RUN     apt-get install -y \
        fonts-noto-core fonts-noto-extra
 
 
-USER app
-
 # fonts
-RUN cp -a /usr/local/lib/python*/site-packages/librarian/fonts /app/.fonts
+RUN cp -a /usr/local/lib/python*/site-packages/librarian/fonts /usr/local/share/fonts
 RUN fc-cache
 
 WORKDIR /app/src
@@ -46,12 +38,10 @@ WORKDIR /app/src
 FROM base AS dev
 
 #RUN pip install --no-cache-dir coverage
-USER app
 
 
 FROM base AS prod
 
 RUN pip install --no-cache-dir gunicorn
 
-USER app
 COPY src /app/src
index 77d345d..2d7d105 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -24,8 +24,8 @@ test:
 
 
 shell:
-       UID=$(UID) GID=$(GID) docker-compose run --rm dev bash
+       UID=$(UID) GID=$(GID) docker compose run --rm dev bash
 
 
 build:
-       UID=$(UID) GID=$(GID) docker-compose build dev
+       docker compose build dev
index 89b2216..8761250 100644 (file)
@@ -3,9 +3,7 @@ services:
     build:
       context: .
       target: dev
-      args:
-        - "UID=${UID}"
-        - "GID=${GID}"
+    user: "${UID}:${GID}"
     volumes:
       - ./src:/app/src
       - ./var/media:/app/var/media
@@ -13,10 +11,12 @@ services:
       - ./var/counters/:/app/var/counters
     depends_on:
       - db
+    env_file:
+      - .env
   db:
-    image: postgres
+    image: postgres:18.0
     container_name: db
     env_file:
       - .env
     volumes:
-      - ./var/postgresql-data/:/var/lib/postgresql/data/
+      - ./var/postgresql-data/:/var/lib/postgresql/
diff --git a/manage b/manage
index 1077a03..b70a319 100755 (executable)
--- a/manage
+++ b/manage
@@ -8,7 +8,7 @@ then
     [ -z "$PORT" ] && PORT=8000
     EXPOSED=127.0.0.1:"$PORT"
     echo "expose as: $EXPOSED"
-    exec docker-compose run --rm -p "$EXPOSED":"$PORT" dev python $PYARGS manage.py runserver 0.0.0.0:"$PORT"
+    exec docker compose run --rm -p "$EXPOSED":"$PORT" dev python $PYARGS manage.py runserver 0.0.0.0:"$PORT"
 else
-    exec docker-compose run --rm dev python $PYARGS manage.py "$@"
+    exec docker compose run --rm dev python $PYARGS manage.py "$@"
 fi
index 084f0da..396184d 100644 (file)
@@ -5,6 +5,8 @@ Django==4.0.8
 fnpdjango==0.6
 docutils==0.20
 
+python-memcached==1.59
+
 django-pipeline==3.1.0
 libsasscompiler==0.2.0
 jsmin==3.0.1
@@ -40,7 +42,7 @@ mutagen==1.47
 sorl-thumbnail==12.10.0
 
 # home-brewed & dependencies
-librarian==24.5.8
+librarian==24.5.10
 
 # celery tasks
 celery[redis]==5.4.0
index ab7be6a..ed651b2 100644 (file)
@@ -5,6 +5,9 @@ from modeltranslation.admin import TranslationAdmin
 from . import models
 
 
+admin.site.register(models.Campaign)
+
+
 class BannerAdmin(TranslationAdmin):
     list_display = [
             'place', 'text',
diff --git a/src/annoy/migrations/0019_campaign_banner_books_alter_banner_place_and_more.py b/src/annoy/migrations/0019_campaign_banner_books_alter_banner_place_and_more.py
new file mode 100644 (file)
index 0000000..51e3339
--- /dev/null
@@ -0,0 +1,38 @@
+# Generated by Django 4.0.8 on 2025-11-25 15:13
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('catalogue', '0051_book_has_audio'),
+        ('annoy', '0018_alter_banner_style'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Campaign',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(help_text='Dla zespołu', max_length=255)),
+                ('image', models.FileField(blank=True, upload_to='annoy/banners/', verbose_name='obraz')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='banner',
+            name='books',
+            field=models.ManyToManyField(blank=True, to='catalogue.book'),
+        ),
+        migrations.AlterField(
+            model_name='banner',
+            name='place',
+            field=models.SlugField(choices=[('top', 'U góry wszystkich stron'), ('book-page', 'Strona książki'), ('book-page-center', 'Strona książki, środek'), ('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'),
+        ),
+        migrations.AddField(
+            model_name='banner',
+            name='campaign',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='annoy.campaign'),
+        ),
+    ]
index 0d88792..6af1cc0 100644 (file)
@@ -8,8 +8,18 @@ from django.utils.timezone import now
 from .places import PLACES, PLACE_CHOICES, STYLES
 
 
+class Campaign(models.Model):
+    name = models.CharField(max_length=255, help_text='Dla zespołu')
+    image = models.FileField('obraz', upload_to='annoy/banners/', blank=True)
+
+    def __str__(self):
+        return self.name
+
+
 class Banner(models.Model):
     place = models.SlugField('miejsce', choices=PLACE_CHOICES)
+    campaign = models.ForeignKey(Campaign, models.PROTECT, null=True, blank=True)
+
     style = models.CharField(
         'styl', max_length=255, blank=True,
         choices=STYLES,
@@ -32,6 +42,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)
+    books = models.ManyToManyField('catalogue.Book', 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)
@@ -49,8 +61,20 @@ class Banner(models.Model):
     def get_text(self):
         return Template(self.text).render(Context())
 
+    def get_image(self):
+        if self.campaign and self.campaign.image:
+            return self.campaign.image
+        else:
+            return self.image
+
+    def is_external(self):
+        return (self.url and
+                not self.url.startswith('/') and
+                not self.url.startswith('https://wolnelektury.pl/')
+                )
+
     @classmethod
-    def choice(cls, place, request, exemptions=True):
+    def choice(cls, place, request, exemptions=True, book=None):
         Membership = apps.get_model('club', 'Membership')
 
         if exemptions and hasattr(request, 'annoy_banner_exempt'):
@@ -68,6 +92,12 @@ class Banner(models.Model):
             until__lt=n
         ).order_by('-priority', '?')
 
+        if book is None:
+            banners = banners.filter(books=None)
+        else:
+            banners = banners.filter(models.Q(books=None) | models.Q(books=book))
+            
+        
         if not request.user.is_authenticated:
             banners = banners.filter(only_authenticated=False)
 
index 8fa767b..0de0483 100644 (file)
@@ -1,6 +1,7 @@
 PLACE_DEFINITIONS = [
     ('top', 'U góry wszystkich stron', True),
     ('book-page', 'Strona książki', False),
+    ('book-page-center', 'Strona książki, środek', False),
     ('book-text-intermission', 'Przerwa w treści książki', False),
     ('book-fragment-list', 'Obok listy fragmentów książki', False),
     ('blackout', 'Blackout', True, (
index 4b2bdcf..9a67fcd 100644 (file)
@@ -11,7 +11,7 @@
               annoy-banner
               annoy-banner_{{ banner.place }}
               annoy-banner-style_{{ banner.style }}
-              {% if banner.image %}with-image{% endif %}
+              {% if banner.get_image %}with-image{% endif %}
               {% if banner.smallfont %}banner-smallfont{% endif %}
               "
         id="annoy-banner-{{ banner.id }}"
            {% if banner.background_color %}background-color: {{ banner.background_color }};{% endif %}
         ">
     {% if not banner.action_label %}
-      <a href="{{ banner.url }}">
+    <a
+      {% if banner.is_external %}target="_blank"{% endif %}
+      href="{{ banner.url }}">
     {% endif %}
     <div class="annoy-banner-inner">
 
-      {% if banner.image %}
-        <img src="{{ banner.image.url }}">
+      {% if banner.get_image %}
+      <div>
+        <img src="{{ banner.get_image.url }}">
+       </div>
       {% endif %}
       <div class="text">
         {{ banner.get_text|safe|linebreaks }}
       </div>
 
       {% if banner.action_label %}
-        <a class="action" href="{{ banner.url }}">
+      <a class="action"
+        {% if banner.is_external %}target="_blank"{% endif %}
+        href="{{ banner.url }}">
           {{ banner.action_label }}
         </a>
       {% endif %}
index 25293f1..cdf4dbc 100644 (file)
@@ -7,8 +7,8 @@ register = template.Library()
 
 
 @register.inclusion_tag('annoy/banner.html', takes_context=True)
-def annoy_banner(context, place):
-    banners = Banner.choice(place, request=context['request'])
+def annoy_banner(context, place, **kwargs):
+    banners = Banner.choice(place, request=context['request'], **kwargs)
     return {
         'banner': banners.first(),
         'closable': PLACES.get(place, False),
index a90d2ef..406cd39 100644 (file)
@@ -157,6 +157,7 @@ class BookSerializer2(serializers.ModelSerializer):
             'cover_thumb', 'cover',
             'isbn_pdf', 'isbn_epub', 'isbn_mobi',
             'abstract',
+            'has_mp3_file',
         ]
 
 class BookSerializer11Labs(serializers.ModelSerializer):
@@ -238,6 +239,17 @@ class MediaSerializer(LegacyMixin, serializers.ModelSerializer):
         legacy_non_null_fields = ['director', 'artist']
 
 
+class MediaSerializer2(MediaSerializer):
+    size = serializers.SerializerMethodField()
+
+    class Meta:
+        model = BookMedia
+        fields = ['url', 'director', 'type', 'name', 'part_name', 'artist', 'duration', 'size']
+
+    def get_size(self, obj):
+        return obj.file.size
+
+
 class BookDetailSerializer(LegacyMixin, serializers.ModelSerializer):
     url = AbsoluteURLField()
 
index 7dc131d..e90e13e 100644 (file)
@@ -23,6 +23,9 @@ urlpatterns = [
          piwik_track_view(views.BookFragmentView.as_view()),
          name='catalogue_api_book_fragment'
          ),
+    path('books/<slug:slug>/media/<slug:type>/',
+         views.BookMediaView.as_view()
+         ),
 
     path('suggested-tags/',
          piwik_track_view(views.SuggestedTags.as_view()),
index e45f80e..821b281 100644 (file)
@@ -517,3 +517,13 @@ class BookFragmentView(RetrieveAPIView):
         book = get_object_or_404(Book, slug=self.kwargs['slug'])
         return book.choose_fragment()
 
+
+class BookMediaView(ListAPIView):
+    serializer_class = serializers.MediaSerializer2
+    pagination_class = None
+
+    def get_queryset(self):
+        return BookMedia.objects.filter(
+            book__slug=self.kwargs['slug'],
+            type=self.kwargs['type']
+        ).order_by('index')
diff --git a/src/catalogue/migrations/0050_book_narrators.py b/src/catalogue/migrations/0050_book_narrators.py
new file mode 100644 (file)
index 0000000..388f150
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 4.0.8 on 2025-08-12 09:37
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('catalogue', '0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='book',
+            name='narrators',
+            field=models.ManyToManyField(blank=True, related_name='narrated', to='catalogue.tag'),
+        ),
+    ]
diff --git a/src/catalogue/migrations/0051_book_has_audio.py b/src/catalogue/migrations/0051_book_has_audio.py
new file mode 100644 (file)
index 0000000..e333bff
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 4.0.8 on 2025-08-12 10:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('catalogue', '0050_book_narrators'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='book',
+            name='has_audio',
+            field=models.BooleanField(default=False),
+        ),
+    ]
index c2b9eed..0400656 100644 (file)
@@ -7,6 +7,8 @@ from datetime import date, timedelta
 from random import randint
 import os.path
 import re
+from slugify import slugify
+from sortify import sortify
 from urllib.request import urlretrieve
 from django.apps import apps
 from django.conf import settings
@@ -94,6 +96,8 @@ class Book(models.Model):
     tags = managers.TagDescriptor(Tag)
     tag_relations = GenericRelation(Tag.intermediary_table_model)
     translators = models.ManyToManyField(Tag, blank=True)
+    narrators = models.ManyToManyField(Tag, blank=True, related_name='narrated')
+    has_audio = models.BooleanField(default=False)
 
     html_built = django.dispatch.Signal()
     published = django.dispatch.Signal()
@@ -266,17 +270,6 @@ class Book(models.Model):
             return sibling.get_first_text()
         return self.parent.get_next_text(inside=False)
 
-    def get_child_audiobook(self):
-        BookMedia = apps.get_model('catalogue', 'BookMedia')
-        if not BookMedia.objects.filter(book__ancestor=self).exists():
-            return None
-        for child in self.children.order_by('parent_number').all():
-            if child.has_mp3_file():
-                return child
-            child_sub = child.get_child_audiobook()
-            if child_sub is not None:
-                return child_sub
-
     def get_siblings(self):
         if not self.parent:
             return []
@@ -337,9 +330,6 @@ class Book(models.Model):
         else:
             return self.media.filter(type=type_).exists()
 
-    def has_audio(self):
-        return self.has_media('mp3')
-
     def get_media(self, type_):
         if self.has_media(type_):
             if type_ in Book.formats:
@@ -421,6 +411,67 @@ class Book(models.Model):
     def has_sync_file(self):
         return settings.FEATURE_SYNCHRO and self.has_media("sync")
 
+    def build_sync_file(self):
+        from lxml import html
+        from django.core.files.base import ContentFile
+        with self.html_file.open('rb') as f:
+            h = html.fragment_fromstring(f.read().decode('utf-8'))
+
+        durations = [
+            m['mp3'].duration
+            for m in self.get_audiobooks()[0]
+        ]
+        if settings.MOCK_DURATIONS:
+            durations = settings.MOCK_DURATIONS
+
+        sync = []
+        ts = None
+        sid = 1
+        dirty = False
+        for elem in h.iter():
+            if elem.get('data-audio-ts'):
+                part, ts = int(elem.get('data-audio-part')), float(elem.get('data-audio-ts'))
+                ts = str(round(sum(durations[:part - 1]) + ts, 3))
+                # check if inside verse
+                p = elem.getparent()
+                while p is not None:
+                    # Workaround for missing ids.
+                    if 'verse' in p.get('class', ''):
+                        if not p.get('id'):
+                            p.set('id', f'syn{sid}')
+                            dirty = True
+                            sid += 1
+                        sync.append((ts, p.get('id')))
+                        ts = None
+                        break
+                    p = p.getparent()
+            elif ts:
+                cls = elem.get('class', '')
+                # Workaround for missing ids.
+                if 'paragraph' in cls or 'verse' in cls or elem.tag in ('h1', 'h2', 'h3', 'h4'):
+                    if not elem.get('id'):
+                        elem.set('id', f'syn{sid}')
+                        dirty = True
+                        sid += 1
+                    sync.append((ts, elem.get('id')))
+                    ts = None
+        if dirty:
+            htext = html.tostring(h, encoding='utf-8')
+            with open(self.html_file.path, 'wb') as f:
+                f.write(htext)
+        try:
+            bm = self.media.get(type='sync')
+        except:
+            bm = BookMedia(book=self, type='sync')
+        sync = (
+            '27\n' + '\n'.join(
+                f'{s[0]}\t{sync[i+1][0]}\t{s[1]}' for i, s in enumerate(sync[:-1])
+            )).encode('latin1')
+        bm.file.save(
+            None, ContentFile(sync)
+            )
+
+    
     def get_sync(self):
         with self.get_media('sync').first().file.open('r') as f:
             sync = f.read().split('\n')
@@ -444,7 +495,7 @@ class Book(models.Model):
     def media_audio_epub(self):
         return self.get_media('audio.epub')
 
-    def get_audiobooks(self):
+    def get_audiobooks(self, with_children=False, processing=False):
         ogg_files = {}
         for m in self.media.filter(type='ogg').order_by().iterator():
             ogg_files[m.name] = m
@@ -470,13 +521,27 @@ class Book(models.Model):
                 media['ogg'] = ogg
             audiobooks.append(media)
 
-        projects = sorted(projects)
-        total_duration = '%d:%02d' % (
-            total_duration // 60,
-            total_duration % 60
-        )
+        if with_children:
+            for child in self.get_children():
+                ch_audiobooks, ch_projects, ch_duration = child.get_audiobooks(
+                    with_children=True, processing=True)
+                audiobooks.append({'part': child})
+                audiobooks += ch_audiobooks
+                projects.update(ch_projects)
+                total_duration += ch_duration
+
+        if not processing:
+            projects = sorted(projects)
+            total_duration = '%d:%02d' % (
+                total_duration // 60,
+                total_duration % 60
+            )
+
         return audiobooks, projects, total_duration
 
+    def get_audiobooks_with_children(self):
+        return self.get_audiobooks(with_children=True)
+    
     def wldocument(self, parse_dublincore=True, inherit=True):
         from catalogue.import_utils import ORMDocProvider
         from librarian.parser import WLDocument
@@ -769,6 +834,42 @@ class Book(models.Model):
     def references(self):
         return self.reference_set.all().select_related('entity')
 
+    def update_has_audio(self):
+        self.has_audio = False
+        if self.media.filter(type='mp3').exists():
+            self.has_audio = True
+        if self.descendant.filter(has_audio=True).exists():
+            self.has_audio = True
+        self.save(update_fields=['has_audio'])
+        if self.parent is not None:
+            self.parent.update_has_audio()
+
+    def update_narrators(self):
+        narrator_names = set()
+        for bm in self.media.filter(type='mp3'):
+            narrator_names.update(set(
+                a.strip() for a in re.split(r',|\si\s', bm.artist)
+            ))
+        narrators = []
+
+        for name in narrator_names:
+            if not name: continue
+            slug = slugify(name)
+            try:
+                t = Tag.objects.get(category='author', slug=slug)
+            except Tag.DoesNotExist:
+                sort_key = sortify(
+                    ' '.join(name.rsplit(' ', 1)[::-1]).lower()
+                )
+                t = Tag.objects.create(
+                    category='author',
+                    name_pl=name,
+                    slug=slug,
+                    sort_key=sort_key,
+                )
+            narrators.append(t)
+        self.narrators.set(narrators)
+
     @classmethod
     @transaction.atomic
     def repopulate_ancestors(cls):
index 0a1544f..87d7f78 100644 (file)
@@ -101,7 +101,9 @@ class BookMedia(models.Model):
         self.extra_info = json.dumps(extra_info)
         self.source_sha1 = self.read_source_sha1(self.file.path, self.type)
         self.duration = self.read_duration()
-        return super(BookMedia, self).save(*args, **kwargs)
+        super(BookMedia, self).save(*args, **kwargs)
+        self.book.update_narrators()
+        self.book.update_has_audio()
 
     def read_duration(self):
         try:
index 2ff4262..37613bf 100644 (file)
     }
 
     $("#paginator").on('click', 'a', function() {
-        get_page_by_url(url=$(this).attr('href'));
+        get_page_by_url(url=$(this).attr('href'), () => {
+            $("html").animate({
+                scrollTop: $("#book-list").offset().top - 50
+            });
+        });
         return false;
     });
 
index cbf9026..4857087 100644 (file)
                 $(".c-media__caption .license", $root).html($(".license", elem).html());
                 $(".c-media__caption .project-logo", $root).html($(".project-icon", elem).html());
 
-                console.log('sm 1');
                 doesUpdateSynchro = false;
                 if (!$currentMedia || $currentMedia[0] != elem[0]) {
-                    console.log('set', player.jPlayer("setMedia", media))
+                    player.jPlayer("setMedia", media);
                     player.jPlayer("option", "playbackRate", speed);
                 }
                 doesUpdateSynchro = true;
                 player.jPlayer(cmd, time);
 
                 $currentMedia = elem;
-                $(".play-next", $root).prop("disabled", !elem.next().length);
+                $(".play-next", $root).prop("disabled", !elem.nextAll('li').length);
 
                 let du = parseFloat(elem.data('duration'));
                 currentDuration = du;
-                elem.nextAll().each(function() {
+                elem.nextAll('li').each(function() {
                     du += parseFloat($(this).data('duration'));
                 });
                 totalDurationLeft = du;
 
                 let pdu = 0;
-                elem.prevAll().each(function() {
+                elem.prevAll('li').each(function() {
                     pdu += parseFloat($(this).data('duration'));
                 });
                 totalDurationBefore = pdu;
-                console.log('sm 3', du, pdu);
 
                 return player;
             };
                     // TODO: if snap then roll
                     locator.removeClass('up').removeClass('down');
                     if (locator.hasClass('snap')) {
-                        console.log('SCROLL!');
                         scrollTo();
                     } else {
                         if (y < miny) {
                 });
 
                 $('.play-next', $root).click(function() {
-                    let p = $currentMedia.next();
+                    let p = $currentMedia.nextAll('li').first();
                     if (p.length) {
                         setMedia(p).jPlayer("play");
                         _paq.push(['trackEvent', 'audiobook', 'next']);
                     }
                 });
                 $('.play-prev', $root).click(function() {
-                    let p = $currentMedia.prev();
+                    let p = $currentMedia.prevAll('li').first();
                     if (p.length) {
                         setMedia(p).jPlayer("play");
                         _paq.push(['trackEvent', 'audiobook', 'prev']);
                     _paq.push(['trackEvent', 'audiobook', 'chapter']);
                 });
 
-                console.log('READY 3!');
                 var initialElem = $('.jp-playlist li', $root).first();
                 var initialTime = 0;
-                console.log('READY 4!');
                 if (true || Modernizr.localstorage) {
                     try {
                         let speedStr = localStorage['audiobook-speed'];
                         initialTime = last[2];
                     }
                 }
-                console.log('READY 5!', initialElem, initialTime);
                 setMedia($(initialElem), initialTime);
-                console.log('READY 6!');
             },
 
             timeupdate: function(event) {
 
 
             ended: function(event) {
-                let p = $currentMedia.next();
+                let p = $currentMedia.nextAll('li');
                 if (p.length) {
                     setMedia(p).jPlayer("play");
                 }
index 4458b5a..b82d926 100644 (file)
@@ -34,7 +34,7 @@
     {% if book.is_book %}
       <span class="icon icon-book-alt" title="{% trans 'książka' %}"></span>
     {% endif %}
-    {% if book.has_mp3_file %}
+    {% if book.has_audio %}
       <span class="icon icon-audio" title="{% trans 'audiobook' %}"></span>
     {% endif %}
     {% if book.is_picture %}
index eb1ab8a..ccce18b 100644 (file)
@@ -5,9 +5,14 @@
 {% load choose_cites from social_tags %}
 {% load catalogue_tags %}
 {% load likes_book from social_tags %}
+{% load annoy %}
 
 
 {% block global-content %}
+<div class="l-container">
+  {% annoy_banner 'book-page' %}
+  </div>
+
     <div class="l-container">
       <div class="l-breadcrumb">
         <a href="/"><span>{% trans "Strona główna" %}</span></a>
                         <div class="c-media__btn">
                           <button class="l-button l-button--media" id="ebook"><i class="icon icon-book"></i> {% trans "pobierz książkę" %}</button>
                         </div>
+                        <div class="c-media__btn">
+                          {% if first_text %}
+                            <a href="https://elevenreader.io/audiobooks/wolnelektury:{{ first_text.slug }}" target="_blank" class="l-button l-button--media"><img src="{% static 'img/elevenreader-21.png' %}" title="{% trans "Posłuchaj w ElevenReader" %}" alt="{% trans "Posłuchaj w ElevenReader" %}"></a>
+                          {% endif %}
+                        </div>
                         <div class="c-media__btn">
                           {% if first_text %}
                             <a href="{% url 'book_text' first_text.slug %}" class="l-button l-button--media l-button--media--full"><i class="icon icon-eye"></i> {% trans "czytaj online" %}</a>
                   </div>
 
 
-
-                  {% if book.has_mp3_file %}
+                  {% if book.has_audio %}
                     {% include 'catalogue/snippets/jplayer.html' %}
-                  {% else %}
-                    {% with ch=book.get_child_audiobook %}
-                      {% if ch %}
-                        {% include 'catalogue/snippets/jplayer_link.html' with book=ch %}
-                      {% endif %}
-                    {% endwith %}
-
                   {% endif %}
 
 
-
-
                   <div class="c-media__popup" data-popup="ebook">
                     <div class="c-media__popup__box">
                       <div class="c-media__popup__box__lead">
               <button class="l-article__read-more" aria-label="{% trans 'Kliknij aby rozwinąć' %}" data-label="{% trans 'Czytaj więcej' %}" data-action="{% trans 'Zwiń tekst' %}">{% trans 'Czytaj więcej' %}</button>
             </article>
             {% if accessible %}
-            <div class="c-support">
-              <div>
-                <h2>
-                  {% blocktrans trimmed %}
-                    Ta książka jest dostępna dla tysięcy dzieciaków dzięki
-                    <span>darowiznom</span> od osób takich jak <span>Ty</span>!
-                  {% endblocktrans %}
-                </h2>
-                <a href="{% url 'club_join' %}?pk_campaign=layout">{% trans "Dorzuć się!" %}</a>
-              </div>
-              <div class="bg">
-                <!-- img src="{% static '2022/images/dziecko.jpeg' %}" alt="Dorzuć się!" -->
-              </div>
-            </div>
+           {% annoy_banner 'book-page-center' book=book %}
             {% endif %}
           </div>
         {% endwith %}
index c57bdd3..2a3db14 100644 (file)
     </div>
   {% endif %}
 
+  {% if narrated %}
+    <div class="l-section l-section--col">
+      <h2 class="header">Audiobooki</h2>
+      <div class="l-books__grid" id="book-list">
+        {% for book in narrated %}
+          {% include "catalogue/book_box.html" %}
+        {% endfor %}
+      </div>
+    </div>
+  {% endif %}
+
   {% if main_tag %}
     <section class="l-section">
       <div class="l-author">
index e0893fa..2399a09 100644 (file)
@@ -1,6 +1,6 @@
 {% load i18n l10n catalogue_tags %}
 
-{% with audiobooks=book.get_audiobooks %}
+{% with audiobooks=book.get_audiobooks_with_children %}
   <div class="c-media__player" id="jp_container_{{ book.pk }}" data-book-slug="{{ book.slug }}">
     <div class="jp-jplayer" data-player="jp_container_{{ book.pk }}"
          data-supplied="oga,mp3"></div>
@@ -38,6 +38,9 @@
           <ul class="jp-playlist">
             {% localize off %}
             {% for i in audiobooks.0 %}
+            {% if i.part %}
+              <div class="title"><strong>{{ i.part.title }}</strong></div>
+            {% else %}
               <li
                   data-mp3='{{ i.mp3.file.url }}'
                   data-ogg='{{ i.ogg.file.url }}'
@@ -67,6 +70,7 @@
                   </span>
                {% endwith %}
               </li>
+              {% endif %}
             {% endfor %}
             {% endlocalize %}
           </ul>
diff --git a/src/catalogue/templates/catalogue/snippets/jplayer_link.html b/src/catalogue/templates/catalogue/snippets/jplayer_link.html
deleted file mode 100644 (file)
index 7bbddcd..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-{% load i18n catalogue_tags %}
-
-<div class="c-media__player" id="jp_container_{{ book.pk }}">
-  <div class="c-player__head">
-    <span>&nbsp;</span>
-  </div>
-
-  <div class="c-player">
-    <div class="c-player__btns">
-      <button disabled class="play-prev"><i class="icon icon-prev"></i></button>
-      <button disabled class="c-player__btn--md"><i class="icon icon-play"></i></button>
-      <form action='{{ book.get_absolute_url }}'>
-        <button class="play-next"><i class="icon icon-next"></i></button>
-      </form>
-    </div>
-
-    <div class="c-player__timeline">
-      <div class="c-player__info">{{ book.pretty_title }}</div>
-    </div>
-
-  </div>
-  <div class="c-media__caption">
-  </div>
-</div>
index 3c4a55f..d5b83ab 100644 (file)
@@ -227,6 +227,7 @@ class TaggedObjectList(BookList):
         ]
         if len(self.ctx['tags']) == 1 and self.ctx['main_tag'].category == 'author':
             self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
+            self.ctx['narrated'] = self.ctx['main_tag'].narrated.all()
 
     def get_queryset(self):
         qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
index 4425928..8620e74 100644 (file)
@@ -11,6 +11,7 @@ from django.utils.safestring import mark_safe
 from fnpdjango.actions import export_as_csv_action
 from modeltranslation.admin import TranslationAdmin
 import annoy.models
+from messaging.models import Contact, Level
 from wolnelektury.utils import YesNoFilter
 from . import models
 
@@ -127,6 +128,11 @@ class CrisisFilter(admin.SimpleListFilter):
         )
 
 
+class OptOutFilter(YesNoFilter):
+    title = 'opt out'
+    parameter_name = 'optout'
+    q = Q(email__in=Contact.objects.filter(level=Level.OPT_OUT).values_list('email', flat=True))
+
 
 class ScheduleAdmin(admin.ModelAdmin):
     form = ScheduleForm
@@ -139,6 +145,7 @@ class ScheduleAdmin(admin.ModelAdmin):
     search_fields = ['email', 'source']
     list_filter = [
         'is_cancelled', 'monthly', 'yearly', 'method',
+        'consent', OptOutFilter,
         PayedFilter, ActiveFilter, ExpiredFilter,
         SourceFilter, CrisisFilter
     ]
index b75877d..e096303 100644 (file)
@@ -35,6 +35,8 @@ class DonationStep1Form(forms.ModelForm):
         self.referer = referer
         super().__init__(*args, **kwargs)
         club = models.Club.objects.first()
+        if self.instance.is_custom_amount():
+            self.fields['custom_amount'].initial = int(self.instance.amount)
         if club is not None:
             self.fields['custom_amount'].widget.attrs['min'] = club.min_amount
 
@@ -50,7 +52,8 @@ class DonationStep1Form(forms.ModelForm):
         return state
 
     def save(self, *args, **kwargs):
-        self.instance.source = self.referer
+        if self.referer is not None:
+            self.instance.source = self.referer
         return super().save(*args, **kwargs)
 
 
index c40428d..7715495 100644 (file)
@@ -161,6 +161,15 @@ class Schedule(models.Model):
         club = Club.objects.first()
         return club.get_description_for_amount(self.amount, self.monthly)
 
+    def is_custom_amount(self):
+        club = Club.objects.first()
+        if not self.amount:
+            return False
+        if self.monthly:
+            return not club.monthlyamount_set.filter(amount=self.amount).exists()
+        else:
+            return not club.singleamount_set.filter(amount=self.amount).exists()
+
     def initiate_payment(self, request):
         return self.get_payment_method().initiate(request, self)
 
diff --git a/src/club/templates/club/donation_infobox.html b/src/club/templates/club/donation_infobox.html
new file mode 100644 (file)
index 0000000..093480b
--- /dev/null
@@ -0,0 +1,10 @@
+<div class="checkout-infobar">
+  <div class="if-monthly">
+    Dziękujemy, że decydujesz się wspierać nas co miesiąc.<br/>
+    Jeśli to pomyłka, możesz zmienić darowiznę na <a class="donation-mod-monthly" data-url="{% url 'donation_set_monthly' schedule.key %}" data-monthly="false" href="{% url 'donation_step1' schedule.key %}">jednorazową</a>.
+  </div>
+  <div class="if-not-monthly">
+    Wolę wspierać co miesiąc! 
+    <a class="donation-mod-monthly" data-url="{% url 'donation_set_monthly' schedule.key %}" data-monthly="true" href="{% url 'donation_step1' schedule.key %}">Zmień na comiesięczną wpłatę.</a>
+  </div>
+</div>
index 033b0ea..4980555 100644 (file)
@@ -1,7 +1,7 @@
 {% load static %}
 {% load i18n %}
 
-<form method="post" action="{% url 'club_join' %}">
+<form method="post" action="{% if schedule %}{% url 'donation_step1' key=schedule.key %}{% else %}{% url 'club_join' %}{% endif %}">
   {% csrf_token %}
   {{ form.errors }}
   <input type="radio" name="switch" id="switch-once" value="single" class="toggle-input" {% if schedule and not schedule.monthly %}checked{% endif %}>
   {% with amounts=club.get_amounts %}
     <div class="l-checkout__payments payments-once wide-spot-{{ amounts.single_wide_spot }}">
       {% for amount in amounts.single %}
-        <div class="l-checkout__payments__box once{% if not schedule.monthly and schedule.amount == amount.amount or not schedule and club.default_single_amount == amount.amount %} is-active{% endif %}{% if amount.wide %} l-checkout__payments__box--special{% endif %} l-checkout__payments__box--{{ amount.box_variant }}">
+        <div class="l-checkout__payments__box once{% if not schedule.monthly and schedule.amount == amount.amount or not schedule and club.default_single_amount == amount.amount %} is-active initial-active{% endif %}{% if amount.wide %} l-checkout__payments__box--special{% endif %} l-checkout__payments__box--{{ amount.box_variant }}">
 
-          <h3>{{ amount.amount }} zł</h3>
           <div class="l-checkout__payments__box__btn-wrp">
-            {% if amount.description %}
-              <p>{{ amount.description|safe }}</p>
-            {% endif %}
-            <button name="single_amount" value="{{ amount.amount }}">{% trans "Wybierz" %}</button>
+            <button name="single_amount" value="{{ amount.amount }}">{{ amount.amount }} zł</button>
           </div>
         </div>
       {% endfor %}
 
     <div class="l-checkout__payments payments-recurring wide-spot-{{ amounts.monthly_wide_spot }}">
     {% for amount in amounts.monthly %}
-      <div class="l-checkout__payments__box{% if schedule.monthly and schedule.amount == amount.amount or not schedule and amount.amount == club.default_monthly_amount %} is-active{% endif %}{% if amount.wide %} l-checkout__payments__box--special{% endif %} l-checkout__payments__box--{{ amount.box_variant }}">
-        <h3>{{ amount.amount }} zł <span>{% trans "/mies." context "kwota na miesiąc" %}</span></h3>
+      <div class="l-checkout__payments__box{% if schedule.monthly and schedule.amount == amount.amount or not schedule and amount.amount == club.default_monthly_amount %} is-active initial-active{% endif %}{% if amount.wide %} l-checkout__payments__box--special{% endif %} l-checkout__payments__box--{{ amount.box_variant }}">
         <div class="l-checkout__payments__box__btn-wrp">
-          {% if amount.description %}
-            <p>{{ amount.description|safe }}</p>
-          {% endif %}
-          <button name="monthly_amount" value="{{ amount.amount }}">{% trans "Wybierz" %}</button>
+          <button name="monthly_amount" value="{{ amount.amount }}">{{ amount.amount }} zł <span> /mies.</span></button>
         </div>
       </div>
     {% endfor %}
@@ -58,7 +50,7 @@
 
   <div class="l-checkout__amount">
     <div class="l-checkout__input">
-      <label for="kwota">{% trans "Inna kwota" %}</label>
+      <label for="id_custom_amount">{% trans "Inna kwota" %}</label>
       {{ form.custom_amount }}
     </div>
     <button>{% trans "Dalej" %}</button>
index 3864ad5..5d60213 100644 (file)
@@ -8,26 +8,27 @@
 
 {% block donation-step-content %}
 
-  <div class="l-checkout__cols">
+  <div class="l-checkout__cols q-is-monthly {% if schedule.monthly %}is-monthly{% endif %}">
     <div class="l-checkout__col">
       <div class="l-checkout__payments__box is-active">
         <h3>
           {{ schedule.amount|floatformat }} zł
-          {% if schedule.monthly %}
-            <span>{% trans "/mies." context "kwota na miesiąc" %}</span>
-          {% endif %}
+          <span class="if-monthly">{% trans "miesięcznie" %}</span>
+          <span class="if-not-monthly">{% trans "jednorazowo" %}</span>
         </h3>
         <img src="{% static '2022/images/checkout-img-3.jpg' %}" alt="">
+       {% if schedule.get_description %}
         <p>{{ schedule.get_description }}</p>
+       {% endif %}
       </div>
     </div>
     <div class="l-checkout__col">
 
+      {% include "club/donation_infobox.html" %}
+      
       <form method='post'>
         {% csrf_token %}
         {{ form.errors }}
-        {{ form.amount }}
-        {{ form.monthly }}
         <div class="l-checkout__form">
           <div class="l-checkout__form__row">
             <div class="l-checkout__input">
index baeee9f..999c708 100644 (file)
@@ -8,21 +8,33 @@
 
 
 {% block donation-step-content %}
-  <div class="l-checkout__cols">
+  <div class="l-checkout__cols q-reload-is-monthly {% if schedule.monthly %}is-monthly{% endif %}">
     <div class="l-checkout__col">
       <div class="l-checkout__payments__box is-active">
         <h3>
           {{ schedule.amount|floatformat }} zł
           {% if schedule.monthly %}
-            <span>{% trans "/mies." context "kwota na miesiąc" %}</span>
+          <span>{% trans "miesięcznie" %}</span>
+         {% else %}
+         <span>{% trans "jednorazowo" %}</span>
           {% endif %}</h3>
           <img src="{% static '2022/images/checkout-img-3.jpg' %}" alt="">
       </div>
     </div>
     <div class="l-checkout__col">
+      {% include "club/donation_infobox.html" %}
+
+      {% if schedule.monthly %}
+      <h3>Darowizna będzie pobierana automatycznie co miesiąc.</h3>
+      <p>Możesz z niej zrezygnować w dowolnej chwili, korzystając z linku który dostaniesz mailem.</p>
+      {% endif %}
+
       <div class="l-checkout__form">
         <div class="l-checkout__form__row full">
-          <div class="iframe">
+
+
+      <div class="iframe">
+           
             {% for method in schedule.get_payment_methods %}
               {% invite_payment method schedule %}
             {% endfor %}
index cede1b4..3889324 100644 (file)
 
     <div class="l-checkout__box">
       <div class="l-checkout__box__header">
-        <img src="{% block donation-jumbo-image %}{% static '2022/images/checkout-img-1.jpg' %}{% endblock %}" alt="Wspieraj Wolne Lektury">
+        <div class="l-checkout__box__header__img"
+             style="background-image: url({% block donation-jumbo-image %}{% static '2022/images/checkout-img-1.jpg' %}{% endblock %}">
+          </div>
         <div class="l-checkout__box__header__content">
-          <h1>{% trans "Wspieraj Wolne Lektury" %}</h1>
-          <p>{% trans "Dziękujemy, że chcesz razem z nami uwalniać książki!" %}</p>
-          <p>{% trans "Wspieraj Wolne Lektury stałą wpłatą – nawet niewielka ma wielką moc! Możesz też wesprzeć Wolne Lektury jednorazowo." %}</p>
+          {% chunk "donate-top" %}
         </div>
       </div>
       <div class="l-checkout__steps">
@@ -43,7 +43,7 @@
         {% endif %}
         <div class="{% if view.step == 1 %}is-current{% else %}is-completed{% endif %}">
           <span>1</span>
-          <p>{% trans "Rodzaj wsparcia" %}</p>
+          <p>{% trans "Kwota wsparcia" %}</p>
         </div>
         {% if view.step > 1 and view.step != 4 %}
           </a>
@@ -84,6 +84,8 @@
 
     <div class="l-checkout__footer">
       <div class="l-checkout__footer__content">
+        {% chunk 'donate-bottom' %}
+
         <div class="l-checkout__footer__content__item">
           <h3>{% trans "Transparentność jest dla nas bardzo ważna." %}</h3>
           <div>
index 5ac0c4c..baaf09d 100644 (file)
@@ -1,5 +1,11 @@
 {% load i18n static %}
-<h3>{% trans "Wolisz wpłacić przez PayPal?" %}</h3>
+<h3>
+  {% if schedule.monthly %}
+  {% trans "Wolisz ustawić comiesięczną darowiznę przez PayPal?" %}
+  {% else %}
+  {% trans "Wolisz wpłacić przez PayPal?" %}
+  {% endif %}
+</h3>
 <a href="{% url 'paypal_init' schedule.key %}">
   <div class="iframe">
     <img src="{% static 'club/paypal.png' %}" alt="PayPal">
index 13cf53b..be61a53 100644 (file)
@@ -1,5 +1,5 @@
 {% load i18n %}
-<h3>{% trans "Podaj dane karty płatniczej" %}</h3>
+<h3>{% trans "Podaj dane karty płatniczej do comiesięcznej darowizny" %}</h3>
 <div class="iframe">
   <form id="theform" method='POST'  action='{% url "club_payu_rec_payment" schedule.key %}'>
     {% csrf_token %}
index 87bfa5a..c7bab6a 100644 (file)
@@ -18,6 +18,7 @@ urlpatterns = [
     path('plan/<key>/zestawienie/<int:year>/', banner_exempt(views.YearSummaryView.as_view()), name='club_year_summary'),
     path('plan/<key>/rodzaj/', banner_exempt(views.DonationStep1.as_view()), name='donation_step1'),
     path('plan/<key>/dane/', banner_exempt(views.DonationStep2.as_view()), name='donation_step2'),
+    path('plan/<key>/ustaw-miesiecznie/', views.set_monthly, name='donation_set_monthly'),
 
     path('przylacz/<key>/', views.claim, name='club_claim'),
     path('anuluj/<key>/', views.cancel, name='club_cancel'),
index b2657e6..503a431 100644 (file)
@@ -4,7 +4,7 @@
 from django.conf import settings
 from django.contrib.auth.decorators import login_required, permission_required
 from django.db.models import Sum
-from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
 from django.shortcuts import get_object_or_404, render
 from django.urls import reverse
 from django.utils.decorators import method_decorator
@@ -30,6 +30,7 @@ class ClubView(TemplateView):
 
 
 
+@method_decorator(never_cache, name='dispatch')
 class DonationStep1(UpdateView):
     queryset = models.Schedule.objects.filter(payed_at=None)
     form_class = forms.DonationStep1Form
@@ -46,6 +47,7 @@ class DonationStep1(UpdateView):
         return reverse('donation_step2', args=[self.object.key])
 
 
+@method_decorator(never_cache, name='dispatch')
 class DonationStep2(UpdateView):
     queryset = models.Schedule.objects.filter(payed_at=None)
     form_class = forms.DonationStep2Form
@@ -59,6 +61,17 @@ class DonationStep2(UpdateView):
         return c
 
 
+def set_monthly(request, key):
+    schedule = get_object_or_404(models.Schedule, payed_at=None, key=key)
+    if request.POST:
+        schedule.monthly = request.POST.get('monthly') == 'true'
+        schedule.save(update_fields=['monthly'])
+    return JsonResponse({
+        "amount": schedule.amount,
+        "monthly": schedule.monthly,
+    })
+    
+
 class JoinView(CreateView):
     form_class = forms.DonationStep1Form
     template_name = 'club/donation_step1.html'
index d4733ff..24a6dad 100644 (file)
@@ -31,6 +31,9 @@ class Entity(models.Model):
 
     def populate_from_wikidata(self, wikidata_id):
         client = Client()
+        client.opener.addheaders = [(
+            'User-Agent', 'Wolne Lektury / https://wolnelektury.pl / Python-wikidata'
+        )]
         entity = client.get(wikidata_id)
 
         self.label = entity.label.get('pl', entity.label) or ''
index 3dfcd9e..8f27b87 100644 (file)
@@ -19,12 +19,13 @@ from wolnelektury.utils import is_ajax
 # ====================
 
 
-@require_POST
+@login_required
 def like_book(request, slug):
-    if not request.user.is_authenticated:
-        return HttpResponseForbidden('Login required.')
     book = get_object_or_404(Book, slug=slug)
 
+    if request.method != 'POST':
+        return redirect(book)
+
     book.like(request.user)
 
     if is_ajax(request):
@@ -50,12 +51,13 @@ class RemoveSetView(AddSetView):
     form_class = forms.RemoveSetForm
 
 
-@require_POST
+@login_required
 def unlike_book(request, slug):
-    if not request.user.is_authenticated:
-        return HttpResponseForbidden('Login required.')
     book = get_object_or_404(Book, slug=slug)
 
+    if request.method != 'POST':
+        return redirect(book)
+
     book.unlike(request.user)
 
     if is_ajax(request):
index aa50736..918bed9 100644 (file)
@@ -1,8 +1,7 @@
 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
 #
-from os import path
-from .paths import PROJECT_DIR
+import os
 
 DEBUG = True
 
@@ -14,15 +13,17 @@ MANAGERS = ADMINS
 
 CONTACT_EMAIL = 'fundacja@wolnelektury.pl'
 
+ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split()
+
 CACHE_MIDDLEWARE_SECONDS = 3 * 60
 
 DATABASES = {
     'default': {
-        'ENGINE': 'django.db.backends.sqlite3',  # 'postgresql_psycopg2'
-        'NAME': path.join(PROJECT_DIR, 'dev.db'),
-        'USER': '',                      # Not used with sqlite3.
-        'PASSWORD': '',                  # Not used with sqlite3.
-        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
+        'ENGINE': 'django.db.backends.postgresql',
+        'HOST': 'db',
+        'USER': os.environ.get('POSTGRES_USER'),
+        'PASSWORD': os.environ.get('POSTGRES_PASSWORD'),
+        'NAME': os.environ.get('POSTGRES_USER'),
     }
 }
 
index e5b2f60..bd40547 100644 (file)
@@ -43,13 +43,6 @@ DEBUG_TOOLBAR_CONFIG = {
 }
 
 
-HAYSTACK_CONNECTIONS = {
-    'default': {
-        'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',
-    },
-}
-
-
 FORMS_BUILDER_USE_SITES = False
 FORMS_BUILDER_EDITABLE_FIELD_MAX_LENGTH = True
 FORMS_BUILDER_EDITABLE_SLUGS = True
index d673772..4400319 100644 (file)
@@ -22,7 +22,10 @@ LIBRARIAN_PDF_MOREFLOATS = None
 
 LATEST_BLOG_POSTS = "https://fundacja.wolnelektury.pl/feed/?cat=-135"
 
-CATALOGUE_COUNTERS_FILE = os.path.join(VAR_DIR, 'catalogue_counters.p')
+CATALOGUE_COUNTERS_FILE = os.path.join(VAR_DIR, 'counters/catalogue_counters.p')
+LESMIANATOR_PICKLE = os.path.join(VAR_DIR, 'counters/lesmianator.p')
+
+NO_SEARCH_INDEX = False
 
 CATALOGUE_MIN_INITIALS = 60
 
@@ -55,8 +58,6 @@ NEWSLETTER_PHPLIST_SUBSCRIBE_URL = None
 VARIANTS = {
 }
 
-EPUB_FUNDRAISING = []
-
 CIVICRM_BASE = None
 CIVICRM_KEY = None
 
index c4af4eb..9c5f34f 100644 (file)
@@ -10,7 +10,7 @@
 
 .c-media__actions {
     display: flex;
-    column-gap: 38px;
+    column-gap: 20px;
     row-gap: 10px;
 }
 
index 52729f9..d1fb096 100644 (file)
           display: inline;
       }
     }
+    div.title {
+      font-size: 12px;
+      line-height: 140%;
+      letter-spacing: 0.05em;
+      list-style-type: decimal;
+      margin: 5px 0;
+      color: white;
+      cursor: pointer;
+    }
   }
 }
 
index 70704f3..5c76498 100644 (file)
        }
     }
 }
+
+
+
+.annoy-banner_book-page {
+    background-color: #ffd430;
+    color: #083F4D;
+    border-radius: 10px;
+    padding: 15px 20px;
+    margin-top: 20px;
+
+    .annoy-banner-inner {
+       display: flex;
+       flex-direction: row;
+       gap: 20px;
+       align-items: flex-start;
+       justify-content: space-between;
+
+       p {
+           margin: 0;
+       }
+       a {
+           line-height: 1.35;
+           color: #c32721;
+           white-space: nowrap;
+           border: solid #c32721;
+           border-width: 0 0 1px;
+
+           &:hover {
+               text-decoration: none;
+               border-bottom-width: 2px;
+           }
+       }
+    }
+}
+
+.annoy-banner_book-page-center {
+    background: white;
+    margin-top: 50px;
+    padding: 20px;
+    border: 1px solid #018189;
+    border-radius: 15px;
+    color:#018189;
+
+    .annoy-banner-inner {
+       display: flex;
+       gap: 15px;
+       align-items: center;
+
+       img {
+           width: 150px;
+       }
+       p {
+           margin: 0;
+       }
+    }
+}
index 78b33bb..b1844d8 100644 (file)
     @include rwd($break-flow) {
         @include font-size(16px);
         line-height: 20px;
-        padding: 19px 20px;
+        padding: 19px 0;
     }
     
     .icon {
       font-size: 21px;
-      margin-right: 15px;
+      margin-right: 10px;
+    }
+
+    img {
+        height: 21px;
     }
 
     &:hover {
index 593794b..6bc86d0 100644 (file)
   display: flex;
   background: #083F4D;
 
-  img {
+  .l-checkout__box__header__img {
       display: none;
+      background-position: center;
+      background-size: cover;
       @include rwd($break-flow) {
           display: block;
+          width: 50%;
       }
   }
 }
     }
   }
   .l-checkout__payments__box__btn-wrp {
-    padding: 0 20px 20px 20px;
+      padding: 20px;
     margin-bottom: 0;
     margin-top: auto;
-    @include rwd($break-flow) {
-        padding-top: 20px;
-    }
   }
   p {
     margin-top: 0;
     }
   }
   button {
-    height: 56px;
+      margin: 0;
+      font-family: "Source Sans Pro",sans-serif;
+    font-weight: bold;
+    font-size: 44px;
+    letter-spacing: -0.01em;
+    height: 90px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all $ease-out 250ms;
+
     background: #FFFFFF;
     border: 1px solid #92BD39;
     border-radius: 3px;
     width: 100%;
     outline: 0;
     cursor: pointer;
-    font-weight: 600;
-    font-size: 16px;
-    line-height: 24px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
     text-align: center;
     color: #083F4D;
-    transition: background $ease-out 250ms;
 
+    flex-direction: column;
     @include rwd($break-flow) {
-        font-size: 20px;
-        line-height: 25px;
+        flex-direction: row;
+        line-height: 130%;
+    }
+
+    span {
+      font-weight: 600;
+      font-size: 25px;
+      letter-spacing: -0.01em;
+      color: #92BD39;
+      margin-left: 10px;
+      transition: opacity $ease-out 250ms;
     }
     
     &:hover {
       &:hover {
         background: #83AD2B;
       }
+      span {
+         color: white;
+      }
     }
   }
 
         }
     }
 }
+
+
+.if-monthly { display: none; }
+.is-monthly {
+    .if-monthly {
+       display: block;
+    }
+    .if-not-monthly {
+       display: none;
+    }
+}
+
+
+.checkout-infobar {
+    margin: 0 0 20px;
+    padding: 20px;
+    border-radius: 10px;
+    border: 1px solid #edc016;
+    background: #edc016;
+}
index c4d4f7a..a47875b 100644 (file)
@@ -161,6 +161,15 @@ body {
               color: white;
           }
       }
+
+      .menubar-donate {
+         color: #fff;
+         background: #c92834;
+         padding: 9px 20px 11px;
+         font-weight: 600;
+         margin-right: 20px;
+         border-radius: 15px;
+      }
   }
 
     .user {
@@ -267,6 +276,10 @@ body {
               display: block;
               left: 16px;
           }
+
+         .menubar-donate {
+             display: none;
+         }
       }
   }
 }
index 1fde2b9..c4ff0f3 100644 (file)
@@ -1064,6 +1064,10 @@ background: #fff;
     }
 
     #other {
+        display: none;
+        @include rwd($break-wide) {
+            display: block;
+        }
        ul {
            list-style: none;
            margin: 0;
index bf0fb2b..4f85a03 100644 (file)
 
 
 #menu {
-    padding-bottom: 50px;
     * {
         box-sizing: content-box;
     }
diff --git a/src/wolnelektury/static/img/elevenreader-21.png b/src/wolnelektury/static/img/elevenreader-21.png
new file mode 100644 (file)
index 0000000..86e433d
Binary files /dev/null and b/src/wolnelektury/static/img/elevenreader-21.png differ
diff --git a/src/wolnelektury/static/img/elevenreader.png b/src/wolnelektury/static/img/elevenreader.png
new file mode 100644 (file)
index 0000000..16dcade
Binary files /dev/null and b/src/wolnelektury/static/img/elevenreader.png differ
index ae5b14a..723e03e 100644 (file)
@@ -4,6 +4,8 @@
 
     function upd_t() {
         $text = $('#main-text #book-text');
+        if (!$text.length) return;
+
         texttop = $text.offset().top;
 
         $footnotes = $('#footnotes', $text);
index fa4a35a..3f5b7e3 100644 (file)
     }
 
     var map_enabled = false;
-    var marker = L.circleMarker([0,0]);
+    var marker = null;
     var map = null;
 
     function enable_map() {
+        if (!$("#reference-map").length) return;
+
         $("#reference-map").show('slow');
 
         if (map_enabled) return;
@@ -22,6 +24,7 @@
         L.tileLayer('https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=a8a97f0ae5134403ac38c1a075b03e15', {
             attribution: 'Maps © <a href="http://www.thunderforest.com">Thunderforest</a>, Data © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>'
         }).addTo(map);
+        marker = L.circleMarker([0,0]);
 
         map_enabled = true;
     }
index 2feb7b8..9821c77 100644 (file)
         $('input', container).val($(this).val());
         $('.is-active', container).removeClass('is-active');
         $(this).closest('.l-checkout__payments__box').addClass('is-active');
-        $('#kwota').val('');
-        return false;
+        $('#id_custom_amount').val('');
+    });
+
+    $('#id_custom_amount').on('input', function() {
+       if ($(this).val() > 0) {
+           $('.l-checkout__payments__box.is-active').removeClass('is-active');
+       } else {
+           $('.l-checkout__payments__box.initial-active').addClass('is-active');
+       }
+    });
+
+    $('.donation-mod-monthly').on('click', function() {
+       $.ajax({
+           method: 'POST',
+           data: {
+               csrfmiddlewaretoken: $("[name=csrfmiddlewaretoken]").val(),
+               monthly: $(this).data('monthly'),
+           },
+           url: $(this).data('url'),
+           success: function(data) {
+               if ($(".q-reload-is-monthly").length) {
+                   window.location.reload()
+               } else {
+                   $(".q-is-monthly").toggleClass('is-monthly', data.monthly);
+               }
+           }
+       });
+       return false;
     });
     
 })();
             $.post({
                 url: '/ludzie/lektura/' + $(this).attr('data-book-slug') + '/nie_lubie/',
                 data: {'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]').val()},
+                dataType: 'json',
                 success: function() {
                     delete state.liked[$btn.attr('data-book')];
                     updateLiked($btn);
+                },
+                error: function() {
+                    window.location.href = $('#login-link').attr('href');
                 }
             })
         } else {
             $.post({
                 url: '/ludzie/lektura/' + $(this).attr('data-book-slug') + '/lubie/',
                 data: {'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]').val()},
+                dataType: 'json',
                 success: function() {
                     state.liked[$btn.attr('data-book')] = [];
                     updateLiked($btn);
                 },
-                error: function(e) {
-                    if (e.status == 403) {
-                        $('#login-link').click();
-                    }
-                },
+                error: function() {
+                    window.location.href = $('#login-link').attr('href')
+                }
             });
         }
     })
index 7b9ea2d..1a804f1 100644 (file)
     </ul>
   </div>
   <a href="{% url 'user_settings' %}" class="user">
-    {% if request.user.is_staffs %}
+    {% if request.user.is_staff %}
       <img src="{% static '2022/images/icons/user-staff.svg' %}">
     {% elif request.user.membership %}
       <img src="{% static '2022/images/icons/user-vip.svg' %}">
     {% else %}
       <img src="{% static '2022/images/icons/user.svg' %}">
     {% endif %}
 </a>
+ </a>
 {% else %}
   <div class="l-navigation__login">
     <a id="login-link" href='{% url 'login' %}?next={{ request.path }}'>{% trans "Zaloguj się" %}</a>
@@ -38,3 +38,6 @@
     <a href='{% url 'register' %}?next={{ request.path }}'>{% trans "Załóż konto" %}</a>
   </div>
 {% endif %}
+
+<a href="/pomagam/?pk_campaign=menubar" class="menubar-donate">Wspieram!</a>
+