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:
'collective_noun',
]
+class ThemeSerializer(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+ href = AbsoluteURLField(
+ view_name='catalogue_api_theme',
+ view_args=('slug',)
+ )
+ class Meta:
+ model = Tag
+ fields = [
+ 'url', 'href', 'name', 'slug', 'sort_key', 'description',
+ ]
+
class TranslatorSerializer(serializers.Serializer):
name = serializers.CharField(source='*')
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>/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()),
path('genres/<slug:slug>/',
piwik_track_view(views.GenreView.as_view()),
name='catalogue_api_genre'),
+ path('themes/',
+ piwik_track_view(views.ThemeList.as_view()),
+ name="catalogue_api_theme_list"),
+ path('themes/<slug:slug>/',
+ piwik_track_view(views.ThemeView.as_view()),
+ name='catalogue_api_theme'),
]
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):
queryset = Tag.objects.filter(category='kind')
lookup_field = 'slug'
+class ThemeList(ListAPIView):
+ serializer_class = serializers.ThemeSerializer
+ queryset = Tag.objects.filter(category='theme')
+
+class ThemeView(RetrieveAPIView):
+ serializer_class = serializers.ThemeSerializer
+ queryset = Tag.objects.filter(category='theme')
+ lookup_field = 'slug'
+
class TagView(RetrieveAPIView):
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
pagination_class = None
def get_queryset(self):
- return BookMedia.objects.filter(book__slug=self.kwargs['slug'], type=self.kwargs['type']).order_by('index')
+ 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})
-
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
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)
+ narrators = models.ManyToManyField(Tag, blank=True, related_name='narrated')
+ has_audio = models.BooleanField(default=False)
html_built = django.dispatch.Signal()
published = django.dispatch.Signal()
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 []
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:
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')
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
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
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):
return None
def update_popularity(self):
- count = self.tags.filter(category='set').values('user').order_by('user').distinct().count()
+ count = self.userlistitem_set.values('list__user').order_by('list__user').distinct().count()
try:
pop = self.popularity
pop.count = count
def ridero_link(self):
return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
- def like(self, user):
- from social.utils import likes, get_set, set_sets
- if not likes(user, self):
- tag = get_set(user, '')
- set_sets(user, self, [tag])
-
- def unlike(self, user):
- from social.utils import likes, set_sets
- if likes(user, self):
- set_sets(user, self, [])
-
def full_sort_key(self):
return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
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:
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:
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:
from club.models import Club
from annoy.models import DynamicTextInsert
from pdcounter import views as pdcounter_views
+from social.models import UserList
from wolnelektury.utils import is_ajax
from catalogue import constants
from catalogue import forms
class TaggedObjectList(BookList):
def analyse(self):
super().analyse()
+
self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
- self.ctx['fragment_tags'] = [t for t in self.ctx['tags'] if t.category in ('theme', 'object')]
- self.ctx['work_tags'] = [t for t in self.ctx['tags'] if t not in self.ctx['fragment_tags']]
+ self.ctx.update({
+ 'fragment_tags': [],
+ 'work_tags': [],
+ 'user_lists': [],
+ })
+ for tag in self.ctx['tags']:
+ if isinstance(tag, UserList):
+ self.ctx['user_lists'].append(tag)
+ elif tag.category == 'theme':
+ self.ctx['fragment_tags'].append(tag)
+ else:
+ self.ctx['work_tags'].append(tag)
+
self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
if self.is_themed:
self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
- elif self.ctx['tags']:
- self.ctx['main_tag'] = self.ctx['tags'][0]
+ elif self.ctx['work_tags']:
+ self.ctx['main_tag'] = self.ctx['work_tags'][0]
else:
self.ctx['main_tag'] = None
self.ctx['filtering_tags'] = [
t for t in self.ctx['tags']
if t is not self.ctx['main_tag']
]
- if len(self.ctx['tags']) == 1 and self.ctx['main_tag'].category == 'author':
+ if len(self.ctx['tags']) == 1 and self.ctx['main_tag'] is not None 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)
+ if self.ctx['work_tags']:
+ qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
+ else:
+ qs = Book.objects.filter(findable=True)
+ for ul in self.ctx['user_lists']:
+ qs = qs.filter(id__in=[i.id for i in ul.get_books()])
qs = qs.exclude(ancestor__in=qs)
if self.is_themed:
fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
return qs
def get_suggested_tags(self, queryset):
+ if self.ctx['user_lists']:
+ # TODO
+ return []
tag_ids = [t.id for t in self.ctx['tags']]
if self.is_themed:
related_tags = []
containing_books,
).exclude(category='set').exclude(pk__in=tag_ids)
))
+ ### FIXME: These won't be tags
if self.request.user.is_authenticated:
related_tags.extend(list(
Tag.objects.usage_for_queryset(
Tag.objects.usage_for_queryset(
objects, counts=True
).exclude(category='set'))
+ ### FIXME: these won't be tags
if request.user.is_authenticated:
related_tag_lists.append(
Tag.objects.usage_for_queryset(
.only('name', 'sort_key', 'category', 'slug'))
if isinstance(objects, QuerySet):
objects = prefetch_relations(objects, 'author')
-
+
categories = split_tags(*related_tag_lists)
suggest = []
for c in ['set', 'author', 'epoch', 'kind', 'genre']:
}
template = 'catalogue/author_detail.html'
-
+
return render(
request, template, result,
)
from django.views.decorators.http import require_POST
from django.views.generic.edit import FormView
-from catalogue.models import Book, Tag
-import catalogue.models.tag
+from catalogue.models import Book
from social import forms, models
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)
- book.like(request.user)
+ if request.method != 'POST':
+ return redirect(book)
+
+ models.UserList.like(request.user, book)
if is_ajax(request):
return JsonResponse({"success": True, "msg": "ok", "like": True})
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)
- book.unlike(request.user)
+ if request.method != 'POST':
+ return redirect(book)
+
+ models.UserList.unlike(request.user, book)
if is_ajax(request):
return JsonResponse({"success": True, "msg": "ok", "like": False})
@login_required
def my_shelf(request):
template_name = 'social/my_shelf.html'
- tags = list(request.user.tag_set.all())
- suggest = [t for t in tags if t.name]
- print(suggest)
+ ulists = list(request.user.userlist_set.all())
+ suggest = [t for t in ulists if t.name]
return render(request, template_name, {
- 'tags': tags,
- 'books': Book.tagged.with_any(tags),
+ 'tags': ulists,
+ 'books': Book.objects_filter(userlistitem__list__user=request.user),
'suggest': suggest,
})
def get_sets_for_book_ids(book_ids, user):
data = {}
- tagged = catalogue.models.tag.TagRelation.objects.filter(
- tag__user=user,
- #content_type= # for books,
- object_id__in=book_ids
- ).order_by('tag__sort_key')
+ tagged = models.UserListItem.objects.filter(
+ list__user=user,
+ book_id__in=book_ids
+ ).order_by('list__name')
for t in tagged:
- # related?
- item = data.setdefault(t.object_id, [])
- if t.tag.name:
+ item = data.setdefault(t.book_id, [])
+ if t.list.name:
item.append({
- "slug": t.tag.slug,
- "url": t.tag.get_absolute_url(),
- "name": t.tag.name,
+ "slug": t.list.slug,
+ "url": t.list.get_absolute_url(),
+ "name": t.list.name,
})
for b in book_ids:
if b not in data:
@login_required
def my_tags(request):
term = request.GET.get('term', '')
- tags = Tag.objects.filter(user=request.user).order_by('sort_key')
+ tags = models.UserList.objects.filter(user=request.user).order_by('name')
if term:
- tags = tags.filter(name__icontains=term)
+ ulists = tags.filter(name__icontains=term)
return JsonResponse(
[
- t.name for t in tags
+ ul.name for ul in ulists
], safe=False
)