path('me/', views.UserView.as_view()),
path('', include('catalogue.api.urls2')),
path('', include('social.api.urls2')),
+ path('', include('bookmarks.api.urls'))
]
from django.utils.decorators import method_decorator
from django.utils.encoding import iri_to_uri
from django.views.decorators.vary import vary_on_headers
+import django.views.decorators.cache
def oauthlib_request(request):
vary_on_auth = method_decorator(vary_on_headers('Authorization'), 'dispatch')
+never_cache = method_decorator(django.views.decorators.cache.never_cache, 'dispatch')
class HttpResponseAppRedirect(HttpResponseRedirect):
class Meta:
model = Tag
fields = [
- 'url', 'href', 'name'
+ 'url', 'href', 'name', 'slug'
]
class AuthorSerializer(AuthorItemSerializer):
class Meta:
model = Tag
fields = [
- 'url', 'href', 'name', 'slug', 'sort_key', 'description',
+ 'id', 'url', 'href', 'name', 'slug', 'sort_key', 'description',
'genitive', 'photo', 'photo_thumb', 'photo_attribution',
]
)
class Meta:
model = Tag
- fields = ['url', 'href', 'name']
+ fields = ['url', 'href', 'name', 'slug']
class EpochSerializer(EpochItemSerializer):
class Meta:
)
class Meta:
model = Tag
- fields = ['url', 'href', 'name']
+ fields = ['url', 'href', 'name', 'slug']
class GenreSerializer(GenreItemSerializer):
class Meta:
)
class Meta:
model = Tag
- fields = ['url', 'href', 'name']
+ fields = ['url', 'href', 'name', 'slug']
class KindSerializer(KindItemSerializer):
class Meta:
'cover_thumb', 'cover',
'isbn_pdf', 'isbn_epub', 'isbn_mobi',
'abstract',
+ 'has_mp3_file',
]
class BookSerializer11Labs(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()
class FilterTagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
- fields = ['id', 'category', 'name']
+ fields = ['id', 'category', 'name', 'slug']
piwik_track_view(views.BookFragmentView.as_view()),
name='catalogue_api_book_fragment'
),
+ path('books/<slug:slug>/media/<slug:type>/', views.BookMediaView.as_view()),
+ path('books/<slug:slug>.json',
+ views.BookJsonView.as_view()),
path('suggested-tags/',
piwik_track_view(views.SuggestedTags.as_view()),
from urllib.request import urlopen
from django.conf import settings
from django.core.files.base import ContentFile
-from django.http import Http404, HttpResponse
+from django.http import Http404, HttpResponse, JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django_filters import rest_framework as dfilters
queryset=Tag.objects.filter(category__in=('author', 'epoch', 'genre', 'kind')),
conjoined=True,
)
+ translator = dfilters.ModelMultipleChoiceFilter(
+ field_name='translators',
+ queryset=Tag.objects.filter(category='author'),
+ conjoined=True,
+ )
class BookList2(ListAPIView):
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')
+
+
+from .tojson import conv
+from lxml import etree
+from rest_framework.views import APIView
+class BookJsonView(APIView):
+ def get(self, request, slug):
+ book = get_object_or_404(Book, slug=slug)
+ js = conv(etree.parse(book.xml_file.path))
+ return JsonResponse(js, json_dumps_params={'ensure_ascii': False})
+
objects = models.Manager()
tagged = managers.ModelTaggedItemManager(Tag)
tags = managers.TagDescriptor(Tag)
- tag_relations = GenericRelation(Tag.intermediary_table_model)
+ tag_relations = GenericRelation(Tag.intermediary_table_model, related_query_name='tagged_book')
translators = models.ManyToManyField(Tag, blank=True)
html_built = django.dispatch.Signal()
except BookMedia.DoesNotExist:
old = None
- super(BookMedia, self).save(*args, **kwargs)
+ #super(BookMedia, self).save(*args, **kwargs)
# remove the zip package for book with modified media
if old:
meta_tags.append((tag, relationship))
return meta_tags
+# def get_books(self):
+# """ Only useful for sets. """
+# return
+
+
TagRelation.tag_model = Tag
name='social_api_like'),
path('likes/', views.LikesView.as_view()),
path('my-likes/', views.MyLikesView.as_view()),
+
+ path('lists/', views.ListsView.as_view()),
+ path('lists/<slug:slug>/', views.ListView.as_view()),
+ path('lists/<slug:slug>/<slug:book>/', views.ListItemView.as_view()),
+
+ path('progress/', views.ProgressListView.as_view()),
+ path('progress/<slug:slug>/', views.ProgressView.as_view()),
+ path('progress/<slug:slug>/text/', views.TextProgressView.as_view()),
+ path('progress/<slug:slug>/audio/', views.AudioProgressView.as_view()),
]
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
from django.http import Http404
-from rest_framework.generics import ListAPIView, get_object_or_404
-from rest_framework.permissions import IsAuthenticated
+from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, DestroyAPIView, get_object_or_404
+from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from rest_framework.response import Response
+from rest_framework import serializers
from rest_framework.views import APIView
from api.models import BookUserData
-from api.utils import vary_on_auth
+from api.utils import vary_on_auth, never_cache
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.utils import likes, get_set
from social.views import get_sets_for_book_ids
+from social import models
-@vary_on_auth
+@never_cache
class LikeView(APIView):
permission_classes = [IsAuthenticated]
return Response({})
-@vary_on_auth
+@never_cache
class LikeView2(APIView):
permission_classes = [IsAuthenticated]
return Response({"likes": likes(request.user, book)})
-@vary_on_auth
+@never_cache
class LikesView(APIView):
permission_classes = [IsAuthenticated]
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
+@never_cache
class MyLikesView(APIView):
permission_classes = [IsAuthenticated]
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()}
+
+ res = list(books.values())
+ res.sort()
return Response(res)
+class TaggedBooksField(serializers.Field):
+ def to_representation(self, value):
+ return catalogue.models.Book.tagged.with_all([value]).values_list('slug', flat=True)
+
+ def to_internal_value(self, value):
+ return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
+
+
+class UserListSerializer(serializers.ModelSerializer):
+ books = TaggedBooksField(source='*')
+
+ class Meta:
+ model = catalogue.models.Tag
+ fields = ['name', 'slug', 'books']
+ read_only_fields = ['slug']
+
+ def create(self, validated_data):
+ instance = get_set(validated_data['user'], validated_data['name'])
+ catalogue.models.tag.TagRelation.objects.filter(tag=instance).delete()
+ for book in validated_data['books']:
+ catalogue.models.Tag.objects.add_tag(book, instance)
+ return instance
+
+ def update(self, instance, validated_data):
+ catalogue.models.tag.TagRelation.objects.filter(tag=instance).delete()
+ for book in validated_data['books']:
+ catalogue.models.Tag.objects.add_tag(book, instance)
+ return instance
+
+class UserListBooksSerializer(UserListSerializer):
+ class Meta:
+ model = catalogue.models.Tag
+ fields = ['books']
+
+
+@never_cache
+class ListsView(ListCreateAPIView):
+ permission_classes = [IsAuthenticated]
+ #pagination_class = None
+ serializer_class = UserListSerializer
+
+ def get_queryset(self):
+ return catalogue.models.Tag.objects.filter(user=self.request.user).exclude(name='')
+
+ def perform_create(self, serializer):
+ serializer.save(user=self.request.user)
+
+
+@never_cache
+class ListView(RetrieveUpdateDestroyAPIView):
+ # TODO: check if can modify
+ permission_classes = [IsAuthenticated]
+ serializer_class = UserListSerializer
+
+ def get_object(self):
+ return get_object_or_404(catalogue.models.Tag, slug=self.kwargs['slug'], user=self.request.user)
+
+ def perform_update(self, serializer):
+ serializer.save(user=self.request.user)
+
+ def post(self, request, slug):
+ serializer = UserListBooksSerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ instance = self.get_object()
+ for book in serializer.validated_data['books']:
+ catalogue.models.Tag.objects.add_tag(book, instance)
+ return Response(self.get_serializer(instance).data)
+
+
+@never_cache
+class ListItemView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def delete(self, request, slug, book):
+ instance = get_object_or_404(catalogue.models.Tag, slug=slug, user=self.request.user)
+ book = get_object_or_404(catalogue.models.Book, slug=book)
+ catalogue.models.Tag.objects.remove_tag(book, instance)
+ return Response(UserListSerializer(instance).data)
+
@vary_on_auth
class ShelfView(ListAPIView):
return books
+
+
+class ProgressSerializer(serializers.ModelSerializer):
+ book = serializers.HyperlinkedRelatedField(
+ read_only=True,
+ view_name='catalogue_api_book',
+ lookup_field='slug'
+ )
+ book_slug = serializers.SlugRelatedField(source='book', read_only=True, slug_field='slug')
+
+ class Meta:
+ model = models.Progress
+ fields = ['book', 'book_slug', 'last_mode', 'text_percent',
+ 'text_anchor',
+ 'audio_percent',
+ 'audio_timestamp',
+ 'implicit_text_percent',
+ 'implicit_text_anchor',
+ 'implicit_audio_percent',
+ 'implicit_audio_timestamp',
+ ]
+
+
+class TextProgressSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = models.Progress
+ fields = [
+ 'text_percent',
+ 'text_anchor',
+ ]
+ read_only_fields = ['text_percent']
+
+class AudioProgressSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = models.Progress
+ fields = ['audio_percent', 'audio_timestamp']
+ read_only_fields = ['audio_percent']
+
+
+@never_cache
+class ProgressListView(ListAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = ProgressSerializer
+
+ def get_queryset(self):
+ return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
+
+
+class ProgressMixin:
+ def get_object(self):
+ try:
+ return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
+ except models.Progress.DoesNotExist:
+ book = get_object_or_404(Book, slug=self.kwargs['slug'])
+ return models.Progress(user=self.request.user, book=book)
+
+
+
+@never_cache
+class ProgressView(ProgressMixin, RetrieveAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = ProgressSerializer
+
+
+@never_cache
+class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = TextProgressSerializer
+
+ def perform_update(self, serializer):
+ serializer.instance.last_mode = 'text'
+ serializer.save()
+
+
+@never_cache
+class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = AudioProgressSerializer
+
+ def perform_update(self, serializer):
+ serializer.instance.last_mode = 'audio'
+ serializer.save()
+
--- /dev/null
+# Generated by Django 4.0.8 on 2025-05-07 13:21
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('social', '0017_userconfirmation'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Progress',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('last_mode', models.CharField(choices=[('text', 'text'), ('audio', 'audio')], max_length=64)),
+ ('text_percent', models.FloatField(blank=True, null=True)),
+ ('text_anchor', models.CharField(blank=True, max_length=64)),
+ ('audio_percent', models.FloatField(blank=True, null=True)),
+ ('audio_timestamp', models.FloatField(blank=True, null=True)),
+ ('implicit_text_percent', models.FloatField(blank=True, null=True)),
+ ('implicit_text_anchor', models.CharField(blank=True, max_length=64)),
+ ('implicit_audio_percent', models.FloatField(blank=True, null=True)),
+ ('implicit_audio_timestamp', models.FloatField(blank=True, null=True)),
+ ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalogue.book')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('user', 'book')},
+ },
+ ),
+ ]
user=user,
key=generate_token()
).send()
+
+
+
+class Progress(models.Model):
+ user = models.ForeignKey(User, models.CASCADE)
+ book = models.ForeignKey('catalogue.Book', models.CASCADE)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ last_mode = models.CharField(max_length=64, choices=[
+ ('text', 'text'),
+ ('audio', 'audio'),
+ ])
+ text_percent = models.FloatField(null=True, blank=True)
+ text_anchor = models.CharField(max_length=64, blank=True)
+ audio_percent = models.FloatField(null=True, blank=True)
+ audio_timestamp = models.FloatField(null=True, blank=True)
+ implicit_text_percent = models.FloatField(null=True, blank=True)
+ implicit_text_anchor = models.CharField(max_length=64, blank=True)
+ implicit_audio_percent = models.FloatField(null=True, blank=True)
+ implicit_audio_timestamp = models.FloatField(null=True, blank=True)
+
+ class Meta:
+ unique_together = [('user', 'book')]
+
+ def save(self, *args, **kwargs):
+ audio_l = self.book.get_audio_length()
+ if self.text_anchor:
+ self.text_percent = 33
+ if audio_l:
+ self.implicit_audio_percent = 40
+ self.implicit_audio_timestamp = audio_l * .4
+ if self.audio_timestamp:
+ if self.audio_timestamp > audio_l:
+ self.audio_timestamp = audio_l
+ if audio_l:
+ self.audio_percent = 100 * self.audio_timestamp / audio_l
+ self.implicit_text_percent = 60
+ self.implicit_text_anchor = 'f20'
+ return super().save(*args, **kwargs)