Source code for reader.views

"""The views of the reader app."""

from typing import TYPE_CHECKING

from django.db.models import Count, Prefetch, Q
from django.http import FileResponse, Http404
from django.shortcuts import redirect, render
from django.utils import timezone as tz
from django.views.decorators.cache import cache_control
from django.views.decorators.http import condition

from MangAdventure import jsonld
from MangAdventure.utils import HttpResponseUnauthorized

from .models import Chapter, Page, Series

if TYPE_CHECKING:  # pragma: no cover
    from datetime import datetime  # isort:skip
    from typing import Optional, Union  # isort:skip
    from django.http import (  # isort:skip
        HttpRequest, HttpResponse, HttpResponsePermanentRedirect
    )


def _latest(request: 'HttpRequest', slug: 'Optional[str]' = None,
            vol: 'Optional[int]' = None, num: 'Optional[float]' = None,
            page: 'Optional[int]' = None) -> 'Optional[datetime]':
    try:
        if slug is None:
            q = Q(chapters__published__lte=tz.now())
            return Series.objects.only('modified').annotate(
                chapter_count=Count('chapters', filter=q)
            ).filter(chapter_count__gt=0).latest().modified
        if vol is None:
            return Series.objects.only('modified').filter(
                chapters__published__lte=tz.now(), slug=slug
            ).distinct().get().modified
        return Chapter.objects.only('modified').filter(
            series__slug=slug, volume=vol,
            number=num, published__lte=tz.now()
        ).latest().modified
    except (Series.DoesNotExist, Chapter.DoesNotExist):
        return None


def _cbz_etag(request: 'HttpRequest', slug: str, vol: int, num: float) -> str:
    return 'W/"%x"' % (hash(f'{slug}-{vol}-{num}.cbz') & (1 << 64) - 1)


[docs]@condition(last_modified_func=_latest) @cache_control(max_age=600, must_revalidate=True) def directory(request: 'HttpRequest') -> 'HttpResponse': """ View that serves a page which lists all the series. :param request: The original request. :return: A response with the rendered ``all_series.html`` template. """ qs = Chapter.objects.filter(published__lte=tz.now()) q = Q(chapters__published__lte=tz.now()) _series = Series.objects.annotate( chapter_count=Count('chapters', filter=q) ).filter(chapter_count__gt=0).prefetch_related( Prefetch('chapters', queryset=qs.order_by('-published')) ).distinct().order_by('title') uri = request.build_absolute_uri(request.path) crumbs = jsonld.breadcrumbs([('Reader', uri)]) library = jsonld.carousel([s.get_absolute_url() for s in _series]) return render(request, 'directory.html', { 'all_series': _series, 'library': library, 'breadcrumbs': crumbs })
[docs]@condition(last_modified_func=_latest) @cache_control(max_age=1800, must_revalidate=True) def series(request: 'HttpRequest', slug: str) -> 'HttpResponse': """ View that serves the page of a single series. If the series doesn't have any published chapters, only staff members will be able to see it. :param request: The original request. :param slug: The slug of the series. :return: A response with the rendered ``series.html`` template. :raises Http404: If there is no series with the specified ``slug``. """ try: _series = Series.objects.prefetch_related( 'chapters__groups', 'artists', 'categories', 'authors', 'aliases' ).get(slug=slug) except Series.DoesNotExist as e: raise Http404 from e chapters = _series.chapters.filter(published__lte=tz.now()).reverse() if not chapters: return render(request, 'error.html', { 'error_message': 'Sorry. This series is not yet available.', 'error_status': 403 }, status=403) marked = request.user.is_authenticated and \ request.user.bookmarks.filter(series=_series).exists() url = request.path p_url = url.rsplit('/', 2)[0] + '/' uri = request.build_absolute_uri(url) crumbs = jsonld.breadcrumbs([ ('Reader', request.build_absolute_uri(p_url)), (_series.title, uri) ]) tags = list(_series.categories.values_list('name', flat=True)) book = jsonld.schema('Book', { 'url': uri, 'name': _series.title, 'abstract': _series.description, 'author': [{ '@type': 'Person', 'name': au.name, 'alternateName': au.aliases.names() } for au in _series.authors.iterator()], 'illustrator': [{ '@type': 'Person', 'name': ar.name, 'alternateName': ar.aliases.names() } for ar in _series.artists.iterator()], 'alternateName': _series.aliases.names(), 'creativeWorkStatus': ( 'Published' if _series.completed else 'Incomplete' ), 'dateCreated': _series.created.strftime('%F'), 'dateModified': _series.modified.strftime('%F'), 'bookFormat': 'GraphicNovel', 'genre': tags, }) return render(request, 'series.html', { 'series': _series, 'chapters': chapters, 'marked': marked, 'breadcrumbs': crumbs, 'book_ld': book, 'tags': ','.join(tags) })
[docs]@condition(last_modified_func=_latest) @cache_control(max_age=3600, must_revalidate=True) def chapter_page(request: 'HttpRequest', slug: str, vol: int, num: float, page: int) -> 'HttpResponse': """ View that serves a chapter page. :param request: The original request. :param slug: The slug of the series. :param vol: The volume of the chapter. :param num: The number of the chapter. :param page: The number of the page. :return: A response with the rendered ``chapter.html`` template. :raises Http404: If there is no matching chapter or page. """ if page == 0: raise Http404('Page cannot be 0') chapters = Chapter.objects.filter( series__slug=slug, published__lte=tz.now() ) try: current = chapters.select_related('series') \ .prefetch_related('pages').get(volume=vol, number=num) all_pages = current.pages.all() curr_page = next(p for p in all_pages if p.number == page) tags = list(current.series.categories.values_list('name', flat=True)) except (Chapter.DoesNotExist, Page.DoesNotExist, StopIteration) as e: raise Http404 from e url = request.path p_url = url.rsplit('/', 4)[0] + '/' p2_url = url.rsplit('/', 5)[0] + '/' crumbs = jsonld.breadcrumbs([ ('Reader', request.build_absolute_uri(p2_url)), (current.series.title, request.build_absolute_uri(p_url)), (current.title, request.build_absolute_uri(url)) ]) return render(request, 'chapter.html', { 'all_chapters': chapters.reverse(), 'curr_chapter': current, 'next_chapter': current.next, 'prev_chapter': current.prev, 'all_pages': all_pages, 'curr_page': curr_page, 'breadcrumbs': crumbs, 'tags': ','.join(tags) })
[docs]def chapter_redirect(request: 'HttpRequest', slug: str, vol: int, num: float) -> 'HttpResponsePermanentRedirect': """ View that redirects a chapter to its first page. :param request: The original request. :param slug: The slug of the series. :param vol: The volume of the chapter. :param num: The number of the chapter. :return: A redirect to :func:`chapter_page`. """ return redirect('reader:page', slug, vol, num, 1, permanent=True)
[docs]@condition(etag_func=_cbz_etag, last_modified_func=_latest) @cache_control(public=True, max_age=3600) def chapter_download(request: 'HttpRequest', slug: str, vol: int, num: float ) -> 'Union[FileResponse, HttpResponseUnauthorized]': """ View that generates a ``.cbz`` file from a chapter. :param request: The original request. :param slug: The slug of the chapter's series. :param vol: The volume of the chapter. :param num: The number of the chapter. :return: A response with the ``.cbz`` file if the user is logged in. :raises Http404: If the chapter does not exist. """ if not request.user.is_authenticated: return HttpResponseUnauthorized( b'You must be logged in to download this file.', content_type='text/plain', realm='chapter archive' ) try: _chapter = Chapter.objects.prefetch_related('pages').get( series__slug=slug, volume=vol, number=num, published__lte=tz.now() ) except Chapter.DoesNotExist as e: raise Http404 from e mime = 'application/vnd.comicbook+zip' name = '{0.series} - v{0.volume} c{0.number:g}.cbz'.format(_chapter) return FileResponse( _chapter.zip(), as_attachment=True, filename=name, content_type=mime )
# def chapter_comments(request, slug, vol, num): # try: # chapter = Chapter.objects.get( # series__slug=slug, volume=vol, number=num # ) # except Chapter.DoesNotExist as e: # raise Http404 from e # return render(request, 'comments.html', {'chapter': chapter}) __all__ = [ 'directory', 'series', 'chapter_page', 'chapter_redirect', 'chapter_download' ]