Source code for api.v1.views

"""The views of the api.v1 app."""

from typing import TYPE_CHECKING, Dict, Iterable, Optional

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse
from django.utils import timezone as tz
from django.utils.http import http_date
from django.views.decorators.cache import cache_control
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import last_modified

from MangAdventure.search import get_response

from groups.models import Group, Member
from reader.models import Artist, Author, Category, Chapter, Series

from ..response import JsonError, require_methods_api

if TYPE_CHECKING:  # pragma: no cover
    from datetime import datetime
    from typing import Union
    from django.http import HttpRequest
    Person = Union[Author, Artist]


def _latest(request: 'HttpRequest', slug: Optional[str] = None,
            vol: Optional[int] = None, num: Optional[float] = None
            ) -> 'Optional[datetime]':
    try:
        if slug is None:
            return Series.objects.only('modified').latest().modified
        if vol is None:
            return Series.objects.only('modified').get(slug=slug).modified
        if num is None:
            return Chapter.objects.only('modified').filter(
                series__slug=slug, volume=vol, published__lte=tz.now()
            ).latest().modified
        return Chapter.objects.only('modified').filter(
            series__slug=slug, volume=vol,
            number=num, published__lte=tz.now()
        ).latest().modified
    except ObjectDoesNotExist:
        return None


def _chapter_response(request: 'HttpRequest', _chapter: Chapter) -> Dict:
    url = request.build_absolute_uri(_chapter.get_absolute_url())
    return {
        'url': url,
        'title': _chapter.title,
        'full_title': str(_chapter),
        'pages_root': url.replace('/reader/', f'{settings.MEDIA_URL}series/'),
        'pages_list': [p._file_name for p in _chapter.pages.iterator()],
        'date': http_date(_chapter.published.timestamp()),
        'final': _chapter.final,
        'groups': list(_chapter.groups.values('id', 'name'))
    }


def _volume_response(request: 'HttpRequest',
                     chapters: Iterable[Chapter]) -> Dict:
    return {
        f'{c.number:g}': _chapter_response(request, c)
        for c in chapters
    }


def _series_response(request: 'HttpRequest', _series: Series) -> Dict:
    response = {
        'slug': _series.slug,
        'title': _series.title,
        'aliases': _series.aliases.names(),
        'url': request.build_absolute_uri(_series.get_absolute_url()),
        'description': _series.description,
        'authors': [],
        'artists': [],
        'categories': list(
            _series.categories.values('name', 'description')
        ),
        'cover': request.build_absolute_uri(_series.cover.url),
        'completed': _series.completed,
        'volumes': {},
    }
    chapters = _series.chapters.filter(published__lte=tz.now())
    for _chapter in chapters:
        if _chapter.volume not in response['volumes']:
            response['volumes'][_chapter.volume] = _volume_response(
                request, chapters.filter(volume=_chapter.volume)
            )
    for _author in _series.authors.prefetch_related('aliases').iterator():
        response['authors'].append([_author.name, *_author.aliases.names()])
    for _artist in _series.artists.prefetch_related('aliases').iterator():
        response['artists'].append([_artist.name, *_artist.aliases.names()])
    return response


def _person_response(request: 'HttpRequest', _person: 'Person') -> Dict:
    response = {
        'id': _person.id,
        'name': _person.name,
        'aliases': _person.aliases.names(),
        'series': [],
    }
    for _series in _person.series_set.prefetch_related('aliases').iterator():
        response['series'].append({
            'slug': _series.slug,
            'title': _series.title,
            'aliases': _series.aliases.names(),
        })
    return response


def _member_response(request: 'HttpRequest', _member: Member) -> Dict:
    return {
        'id': _member.id,
        'name': _member.name,
        'roles': [r.get_role_display() for r in _member.roles.iterator()],
        'twitter': _member.twitter,
        'discord': _member.discord,
    }


def _group_response(request: 'HttpRequest', _group: Group) -> Dict:
    logo = ''
    if _group.logo:
        logo = request.build_absolute_uri(_group.logo.url)
    response = {
        'id': _group.id,
        'name': _group.name,
        'description': _group.description,
        'website': _group.website,
        'discord': _group.discord,
        'twitter': _group.twitter,
        'logo': logo,
        'members': [
            _member_response(request, m) for m
            in _group.members.distinct().iterator()
        ],
        'series': [],
    }
    _series = []
    for _chapter in _group.releases.prefetch_related('series__aliases') \
            .filter(published__lte=tz.now()).iterator():
        if _chapter.series_id not in _series:
            response['series'].append({
                'slug': _chapter.series.slug,
                'title': _chapter.series.title,
                'aliases': _chapter.series.aliases.names()
            })
            _series.append(_chapter.series_id)
    return response


[docs]@csrf_exempt @require_methods_api() @last_modified(_latest) @cache_control(public=True, max_age=600, must_revalidate=True) def all_releases(request: 'HttpRequest') -> JsonResponse: """ View that serves all the releases in a JSON array. :param request: The original request. :return: A JSON-formatted response with the releases. """ response = [] for s in Series.objects.prefetch_related('chapters').iterator(): series_res = { 'slug': s.slug, 'title': s.title, 'url': request.build_absolute_uri(s.get_absolute_url()), 'cover': request.build_absolute_uri(s.cover.url), 'latest_chapter': {}, } try: series_res['latest_chapter'] = s.chapters.filter( published__lte=tz.now() ).values('title', 'volume', 'number', 'published').latest() except ObjectDoesNotExist: pass else: series_res['latest_chapter']['date'] = http_date( series_res['latest_chapter'].pop('published').timestamp() ) response.append(series_res) return JsonResponse(response, safe=False)
[docs]@csrf_exempt @require_methods_api() @last_modified(_latest) @cache_control(public=True, max_age=600, must_revalidate=True) def all_series(request: 'HttpRequest') -> JsonResponse: """ View that serves all the series in a JSON array. :param request: The original request. :return: A JSON-formatted response with the series. """ return JsonResponse([ _series_response(request, s) for s in get_response(request) ], safe=False)
[docs]@csrf_exempt @require_methods_api() @last_modified(_latest) @cache_control(public=True, max_age=600, must_revalidate=True) def series(request: 'HttpRequest', slug: str) -> JsonResponse: """ View that serves a single series as a JSON object. :param request: The original request. :param slug: The slug of the series. :return: A JSON-formatted response with the series. """ try: _series = Series.objects.get(slug=slug) except ObjectDoesNotExist: return JsonError('Not found', 404) return JsonResponse(_series_response(request, _series))
[docs]@csrf_exempt @require_methods_api() @last_modified(_latest) @cache_control(public=True, max_age=600, must_revalidate=True) def volume(request: 'HttpRequest', slug: str, vol: int) -> JsonResponse: """ View that serves a single volume as a JSON object. :param request: The original request. :param slug: The slug of the series. :param vol: The number of the volume. :return: A JSON-formatted response with the volume. """ try: _series = Series.objects \ .prefetch_related('chapters__pages').get(slug=slug) except ObjectDoesNotExist: return JsonError('Not found', 404) chapters = _series.chapters.filter(volume=vol, published__lte=tz.now()) if not chapters: return JsonError('Not found', 404) return JsonResponse(_volume_response(request, chapters))
[docs]@csrf_exempt @require_methods_api() @last_modified(_latest) @cache_control(public=True, max_age=600, must_revalidate=True) def chapter(request: 'HttpRequest', slug: str, vol: int, num: float) -> JsonResponse: """ View that serves a single chapter as a JSON object. :param request: The original request. :param slug: The slug of the series. :param vol: The number of the volume. :param num: The number of the chapter. :return: A JSON-formatted response with the chapter. """ try: _chapter = Chapter.objects.prefetch_related('pages').get( series__slug=slug, volume=vol, number=num, published__lte=tz.now() ) except ObjectDoesNotExist: return JsonError('Not found', 404) return JsonResponse(_chapter_response(request, _chapter))
def _is_author(request: 'HttpRequest') -> bool: return request.path[:16] == '/api/v1/authors'
[docs]@csrf_exempt @require_methods_api() @cache_control(public=True, max_age=1800, must_revalidate=True) def all_people(request: 'HttpRequest') -> JsonResponse: """ View that serves all the authors/artists in a JSON array. :param request: The original request. :return: A JSON-formatted response with the authors/artists. """ _type = Author if _is_author(request) else Artist return JsonResponse([ _person_response(request, p) for p in _type.objects.prefetch_related( 'aliases', 'series_set__aliases' ).iterator() ], safe=False)
[docs]@csrf_exempt @require_methods_api() @cache_control(public=True, max_age=1800, must_revalidate=True) def person(request: 'HttpRequest', p_id: int) -> JsonResponse: """ View that serves a single author/artist as a JSON object. :param request: The original request. :param p_id: The ID of the author/artist. :return: A JSON-formatted response with the author/artist. """ try: _type = Author if _is_author(request) else Artist _person = _type.objects.prefetch_related( 'aliases', 'series_set__aliases' ).get(id=p_id) except ObjectDoesNotExist: return JsonError('Not found', 404) return JsonResponse(_person_response(request, _person))
[docs]@csrf_exempt @require_methods_api() @cache_control(public=True, max_age=1800, must_revalidate=True) def all_groups(request: 'HttpRequest') -> JsonResponse: """ View that serves all the groups in a JSON array. :param request: The original request. :return: A JSON-formatted response with the groups. """ return JsonResponse([ _group_response(request, g) for g in Group.objects.prefetch_related( 'releases__series', 'roles__member' ).iterator() ], safe=False)
[docs]@csrf_exempt @require_methods_api() @cache_control(public=True, max_age=1800, must_revalidate=True) def group(request: 'HttpRequest', g_id: int) -> JsonResponse: """ View that serves a single group as a JSON object. :param request: The original request. :param g_id: The ID of the group. :return: A JSON-formatted response with the group. """ try: _group = Group.objects.prefetch_related( 'releases__series', 'roles__member' ).get(id=g_id) except ObjectDoesNotExist: return JsonError('Not found', 404) return JsonResponse(_group_response(request, _group))
[docs]@csrf_exempt @require_methods_api() @cache_control(public=True, max_age=1800, must_revalidate=True) def categories(request: 'HttpRequest') -> JsonResponse: """ View that serves all the categories in a JSON array. :param request: The original request. :return: A JSON-formatted response with the categories. """ return JsonResponse(list(Category.objects.values()), safe=False)
[docs]@csrf_exempt @require_methods_api() def invalid_endpoint(request: 'HttpRequest') -> JsonError: """ View that serves a :status:`501` error as a JSON object. :param request: The original request. :return: A JSON-formatted response with the error. """ return JsonError('Invalid API endpoint', 501)
__all__ = [ 'all_releases', 'all_series', 'series', 'volume', 'chapter', 'all_people', 'person', 'all_groups', 'group', 'categories', 'invalid_endpoint' ]