helper script master
authorRadek Czajka <rczajka@rczajka.pl>
Tue, 15 Apr 2025 13:52:18 +0000 (15:52 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Tue, 15 Apr 2025 13:52:18 +0000 (15:52 +0200)
14 files changed:
Dockerfile [new file with mode: 0644]
Makefile
docker-compose.yml [new file with mode: 0644]
manage [new file with mode: 0755]
requirements/requirements.txt
src/api/urls.py
src/catalogue/api/serializers.py
src/catalogue/api/urls2.py
src/catalogue/api/views.py
src/catalogue/models/bookmedia.py
src/club/civicrm.py
src/social/api/urls2.py [new file with mode: 0644]
src/social/api/views.py
src/wolnelektury/templates/admin/catalogue/book/change_list.html

diff --git a/Dockerfile b/Dockerfile
new file mode 100644 (file)
index 0000000..032024b
--- /dev/null
@@ -0,0 +1,57 @@
+FROM python:3.8 AS base
+
+ARG UID=1000
+ARG GID=1000
+
+RUN     apt-get update && apt-get install -y \
+       git \
+       calibre \
+       texlive-xetex texlive-lang-polish \
+       libespeak-dev
+
+COPY requirements/requirements.txt requirements.txt
+
+# numpy -> aeneas
+RUN pip install numpy
+RUN pip install aeneas
+
+RUN pip install --no-cache-dir -r requirements.txt
+RUN pip install --no-cache-dir \
+    psycopg2-binary \
+    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 \
+       texlive-lang-other \
+       texlive-luatex \
+       texlive-fonts-extra \
+       texlive-fonts-extra-links \
+       fonts-noto-core fonts-noto-extra
+
+
+USER app
+
+# fonts
+RUN cp -a /usr/local/lib/python*/site-packages/librarian/fonts /app/.fonts
+RUN fc-cache
+
+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 d43ff1e..77d345d 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,8 @@
-.PHONY: deploy test
+.PHONY: deploy test shell
+
+
+UID != id -u
+GID != id -g
 
 
 deploy: src/wolnelektury/localsettings.py
@@ -17,3 +21,11 @@ test:
        mv ../htmlcov.new ../htmlcov
        coverage report
        rm .coverage
+
+
+shell:
+       UID=$(UID) GID=$(GID) docker-compose run --rm dev bash
+
+
+build:
+       UID=$(UID) GID=$(GID) docker-compose build dev
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644 (file)
index 0000000..89b2216
--- /dev/null
@@ -0,0 +1,22 @@
+services:
+  dev:
+    build:
+      context: .
+      target: dev
+      args:
+        - "UID=${UID}"
+        - "GID=${GID}"
+    volumes:
+      - ./src:/app/src
+      - ./var/media:/app/var/media
+      - ./var/static:/app/var/static
+      - ./var/counters/:/app/var/counters
+    depends_on:
+      - db
+  db:
+    image: postgres
+    container_name: db
+    env_file:
+      - .env
+    volumes:
+      - ./var/postgresql-data/:/var/lib/postgresql/data/
diff --git a/manage b/manage
new file mode 100755 (executable)
index 0000000..1077a03
--- /dev/null
+++ b/manage
@@ -0,0 +1,14 @@
+#!/bin/sh
+export UID=`id -u`
+export GID=`id -g`
+
+if [ "$1" = "runserver" ]
+then
+    PORT="$2"
+    [ -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"
+else
+    exec docker-compose run --rm dev python $PYARGS manage.py "$@"
+fi
index 10498cb..084f0da 100644 (file)
@@ -40,7 +40,7 @@ mutagen==1.47
 sorl-thumbnail==12.10.0
 
 # home-brewed & dependencies
-librarian==24.5.7
+librarian==24.5.8
 
 # celery tasks
 celery[redis]==5.4.0
index de3dba7..62d4fa7 100644 (file)
@@ -16,6 +16,7 @@ urlpatterns1 = [
     path('login/', csrf_exempt(views.Login2View.as_view())),
     path('me/', views.UserView.as_view()),
     path('', include('catalogue.api.urls2')),
+    path('', include('social.api.urls2')),
 ]
 
 
index 60e52a0..a90d2ef 100644 (file)
@@ -156,6 +156,7 @@ class BookSerializer2(serializers.ModelSerializer):
             'epub', 'mobi', 'pdf', 'html', 'txt', 'fb2', 'xml',
             'cover_thumb', 'cover',
             'isbn_pdf', 'isbn_epub', 'isbn_mobi',
+            'abstract',
         ]
 
 class BookSerializer11Labs(serializers.ModelSerializer):
@@ -352,6 +353,15 @@ class FragmentDetailSerializer(serializers.ModelSerializer):
         fields = ['book', 'anchor', 'text', 'url', 'themes']
 
 
+class FragmentSerializer2(serializers.ModelSerializer):
+    url = AbsoluteURLField()
+    html = serializers.CharField(source='text')
+
+    class Meta:
+        model = Fragment
+        fields = ['anchor', 'html', 'url']
+
+
 class FilterTagSerializer(serializers.ModelSerializer):
     class Meta:
         model = Tag
index b16af66..7dc131d 100644 (file)
@@ -19,6 +19,10 @@ urlpatterns = [
          piwik_track_view(views.BookDetail2.as_view()),
          name='catalogue_api_book'
          ),
+    path('books/<slug:slug>/fragment/',
+         piwik_track_view(views.BookFragmentView.as_view()),
+         name='catalogue_api_book_fragment'
+         ),
 
     path('suggested-tags/',
          piwik_track_view(views.SuggestedTags.as_view()),
index 0e758b1..e45f80e 100644 (file)
@@ -501,6 +501,19 @@ class SuggestedTags(ListAPIView):
 
     def get_queryset(self):
         tag_ids = self.request.GET.getlist('tag', [])
+        search = self.request.GET.get('search')
         tags = [get_object_or_404(Tag, id=tid) for tid in tag_ids]
         related_tags = list(t.id for t in get_top_level_related_tags(tags))
-        return Tag.objects.filter(id__in=related_tags)
+        tags = Tag.objects.filter(id__in=related_tags)
+        if search:
+            tags = tags.filter(name__icontains=search)
+        return tags
+
+
+class BookFragmentView(RetrieveAPIView):
+    serializer_class = serializers.FragmentSerializer2
+
+    def get_object(self):
+        book = get_object_or_404(Book, slug=self.kwargs['slug'])
+        return book.choose_fragment()
+
index acb1881..0a1544f 100644 (file)
@@ -70,6 +70,8 @@ class BookMedia(models.Model):
         return f'{name}.{ext}'
 
     def save(self, parts_count=None, *args, **kwargs):
+        if self.type in ('daisy', 'audio.epub'):
+            return super().save(*args, **kwargs)
         from catalogue.utils import ExistingFile, remove_zip
 
         if not parts_count:
index 20b287a..dffeaa7 100644 (file)
@@ -27,12 +27,12 @@ class CiviCRM:
         d = response.json()
         return d
 
-    def create_or_update_contact(self, email, key=None):
+    def create_or_update_contact(self, email, fields=None):
         contact_id = self.get_contact_id(email)
         if contact_id is None:
-            contact_id = self.create_contact(email, key)
-        elif key:
-            self.update_contact(contact_id, key)
+            contact_id = self.create_contact(email, fields)
+        elif fields:
+            self.update_contact(contact_id, fields)
         return contact_id
 
     def get_contact_id(self, email):
@@ -49,7 +49,7 @@ class CiviCRM:
         if result:
             return result[0]['id']
 
-    def create_contact(self, email, key=None):
+    def create_contact(self, email, fields):
         data = {
             'values': {},
             'chain': {
@@ -65,19 +65,22 @@ class CiviCRM:
                 ]
             }
         }
-        if key:
-            data['values']['WL.TPWL_key'] =  key
+        if fields:
+            data['values'].update(fields)
         result = self.request('Contact', 'create', data)
         return result['values'][0]['id']
-    
-    def update_contact(self, contact_id, key):
+
+    def update_phone(self, contact_id, phone):
+        if self.request('Phone', 'get', {'where': [['phone', "=", phone], ['contact_id', "=",  contact_id]]})['count']:
+            return
+        self.request('Phone', 'create', {'values': {'phone': phone, 'contact_id': contact_id}})
+
+    def update_contact(self, contact_id, fields):
         return self.request(
             'Contact',
             'update',
             {
-                'values': {
-                    'WL.TPWL_key': key,
-                },
+                'values': fields,
                 'where': [
                     ['id', '=', contact_id]
                 ]
@@ -89,7 +92,8 @@ class CiviCRM:
         if not self.enabled:
             return
 
-        contact_id = self.create_or_update_contact(email, tpwl_key)
+        fields = {'WL.TPWL_key': tpwl_key}
+        contact_id = self.create_or_update_contact(email, fields)
 
         activity_id = self.get_activity_id(key)
         if activity_id is None:
diff --git a/src/social/api/urls2.py b/src/social/api/urls2.py
new file mode 100644 (file)
index 0000000..b150e61
--- /dev/null
@@ -0,0 +1,17 @@
+# 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
+from stats.utils import piwik_track_view
+from . import views
+
+
+urlpatterns = [
+    path('like/<slug:slug>/',
+        piwik_track_view(views.LikeView2.as_view()),
+        name='social_api_like'),
+    path('likes/', views.LikesView.as_view()),
+    path('my-likes/', views.MyLikesView.as_view()),
+]
+
+
index a299304..22a0e9c 100644 (file)
@@ -11,7 +11,9 @@ from api.utils import vary_on_auth
 from catalogue.api.helpers import order_books, books_after
 from catalogue.api.serializers import BookSerializer
 from catalogue.models import Book
+import catalogue.models
 from social.utils import likes
+from social.views import get_sets_for_book_ids
 
 
 @vary_on_auth
@@ -32,6 +34,53 @@ class LikeView(APIView):
         return Response({})
 
 
+@vary_on_auth
+class LikeView2(APIView):
+    permission_classes = [IsAuthenticated]
+
+    def get(self, request, slug):
+        book = get_object_or_404(Book, slug=slug)
+        return Response({"likes": likes(request.user, book)})
+
+    def put(self, request, slug):
+        book = get_object_or_404(Book, slug=slug)
+        book.like(request.user)
+        return Response({"likes": likes(request.user, book)})
+
+    def delete(self, request, slug):
+        book = get_object_or_404(Book, slug=slug)
+        book.unlike(request.user)
+        return Response({"likes": likes(request.user, book)})
+
+
+@vary_on_auth
+class LikesView(APIView):
+    permission_classes = [IsAuthenticated]
+
+    def get(self, request):
+        slugs = request.GET.getlist('slug')
+        books = Book.objects.filter(slug__in=slugs)
+        books = {b.id: b.slug for b in books}
+        ids = books.keys()
+        res = get_sets_for_book_ids(ids, request.user)
+        res = {books[bid]: v for bid, v in res.items()}
+        return Response(res)
+
+
+@vary_on_auth
+class MyLikesView(APIView):
+    permission_classes = [IsAuthenticated]
+
+    def get(self, request):
+        ids = catalogue.models.tag.TagRelation.objects.filter(tag__user=request.user).values_list('object_id', flat=True).distinct()
+        books = Book.objects.filter(id__in=ids)
+        books = {b.id: b.slug for b in books}
+        res = get_sets_for_book_ids(ids, request.user)
+        res = {books[bid]: v for bid, v in res.items()}
+        return Response(res)
+
+
+
 @vary_on_auth
 class ShelfView(ListAPIView):
     permission_classes = [IsAuthenticated]
index b9574d9..db33084 100644 (file)
@@ -4,7 +4,7 @@
   <form action="{% url 'import_book' %}" method="post" enctype="multipart/form-data">
     {% csrf_token %}
     <p>
-      <input type="file" id="id_book_xml_file" name="book_xml_file"/>
+      <input type="file" id="id_book_xml_file" name="book_xml_file" required/>
       <input type="submit" value="Import książki" />
     </p>
   </form>