--- /dev/null
+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
-.PHONY: deploy test
+.PHONY: deploy test shell
+
+
+UID != id -u
+GID != id -g
deploy: src/wolnelektury/localsettings.py
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
--- /dev/null
+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/
--- /dev/null
+#!/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
sorl-thumbnail==12.10.0
# home-brewed & dependencies
-librarian==24.5.7
+librarian==24.5.8
# celery tasks
celery[redis]==5.4.0
path('login/', csrf_exempt(views.Login2View.as_view())),
path('me/', views.UserView.as_view()),
path('', include('catalogue.api.urls2')),
+ path('', include('social.api.urls2')),
]
'epub', 'mobi', 'pdf', 'html', 'txt', 'fb2', 'xml',
'cover_thumb', 'cover',
'isbn_pdf', 'isbn_epub', 'isbn_mobi',
+ 'abstract',
]
class BookSerializer11Labs(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
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()),
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()
+
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:
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):
if result:
return result[0]['id']
- def create_contact(self, email, key=None):
+ def create_contact(self, email, fields):
data = {
'values': {},
'chain': {
]
}
}
- 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]
]
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:
--- /dev/null
+# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
+#
+from django.urls import path
+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()),
+]
+
+
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
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]
<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>