Source code for reader.admin

"""Admin models for the reader app."""

from hashlib import blake2b
from typing import Optional, Tuple

from django.contrib import admin
from django.db.models.query import Q, QuerySet
from django.forms.models import BaseInlineFormSet, ModelForm
# XXX: Forward reference warning when under TYPE_CHECKING
from django.http import HttpRequest
from django.utils import timezone as tz
from django.utils.html import mark_safe

from MangAdventure import filters, utils

from .models import (
    Artist, ArtistAlias, Author, AuthorAlias,
    Category, Chapter, Page, Series, SeriesAlias
)


class DateFilter(admin.DateFieldListFilter):
    """Admin interface filter for dates."""
    def __init__(self, *args, **kwargs):  # pragma: no cover
        super().__init__(*args, **kwargs)
        self.title = 'date'
        self.links += (('Scheduled', {
            self.lookup_kwarg_since: tz.now()
        }),)


[docs]class SeriesAliasInline(admin.StackedInline): """Inline admin model for :class:`~reader.models.SeriesAlias`.""" model = SeriesAlias extra = 1
[docs]class AuthorAliasInline(admin.StackedInline): """Inline admin model for :class:`~reader.models.AuthorAlias`.""" model = AuthorAlias extra = 1
[docs]class ArtistAliasInline(admin.StackedInline): """Inline admin model for :class:`~reader.models.ArtistAlias`.""" model = ArtistAlias extra = 1
class PageFormset(BaseInlineFormSet): """Formset for :class:`~reader.admin.PageInline`.""" def clean(self): # pragma: no cover """Ensure that page numbers don't have duplicates.""" super().clean() numbers = [] for form in self.forms: num = form.cleaned_data.get('number') if num in numbers: form._errors['number'] = \ self.error_class([self.get_form_error()]) del form.cleaned_data['number'] if not form.cleaned_data.get('DELETE'): numbers.append(num) def save_existing(self, form: ModelForm, instance: Page, commit: bool = True) -> Page: """Replace an existing chapter page.""" with form.instance.image.open('rb') as img: dgst = blake2b(img.read(), digest_size=16) ext = form.instance.image.name.split(".")[-1] path = form.instance.chapter.get_directory() name = str(path / f'{dgst.hexdigest()}.{ext}') form.instance.image.name = name return form.save(commit=commit) def save_new(self, form: ModelForm, commit: bool = True) -> Page: """Add a new page to the chapter.""" setattr(form.instance, self.fk.name, self.instance) return self.save_existing(form, self.instance, commit) class PageInline(admin.TabularInline): """ Inline admin model for :class:`~reader.models.Page`. .. admonition:: TODO :class: warning Add a way to delete all the pages. """ model = Page extra = 1 formset = PageFormset fields = ('image', 'preview', 'number') readonly_fields = ('preview',) def preview(self, obj: Page) -> str: """ Get the image of the page as an HTML ``<img>``. :param obj: A ``Page`` model instance. :return: An ``<img>`` tag with the page image. """ return utils.img_tag(obj.image, 'preview', height=150) preview.short_description = ''
[docs]class ChapterAdmin(admin.ModelAdmin): """Admin model for :class:`~reader.models.Chapter`.""" inlines = (PageInline,) date_hierarchy = 'published' list_display = ( 'preview', 'title', 'volume', '_number', 'published', 'modified', 'final' ) list_display_links = ('title',) ordering = ('-modified',) search_fields = ('title', 'series__title') list_filter = ( ('series', admin.RelatedFieldListFilter), ('groups', filters.related_filter('group')), filters.boolean_filter( 'status', 'final', ('Final', 'Not final') ), ('published', DateFilter), ) actions = ('toggle_final',) empty_value_display = 'N/A' def _number(self, obj: Chapter) -> str: return f'{obj.number:g}' _number.short_description = 'number' _number.admin_order_field = 'number'
[docs] def preview(self, obj: Chapter) -> str: """ Get the first image of the chapter as an HTML ``<img>``. :param obj: A ``Chapter`` model instance. :return: An ``<img>`` tag with the chapter preview. """ page = obj.pages.only('image').first() if page is None: return '' return utils.img_tag(page.image, 'preview', height=50)
[docs] def toggle_final(self, request: 'HttpRequest', queryset: 'QuerySet'): """ Toggle the status of the selected chapters. :param request: The original request. :param queryset: The original queryset. """ queryset.update(final=Q(final=False))
toggle_final.short_description = 'Toggle status of selected chapters'
class SeriesForm(ModelForm): """Admin form for :class:`~reader.models.Series`.""" def __init__(self, *args, **kwargs): # pragma: no cover super().__init__(*args, **kwargs) self.fields['format'].help_text = mark_safe('<br>'.join(( 'The format used to render the chapter names.' ' The following variables are available:', '<b>{title}</b>: The title of the chapter.', '<b>{volume}</b>: The volume of the chapter.', '<b>{number}</b>: The number of the chapter.', '<b>{date}</b>: The chapter\'s upload date (YYYY-MM-DD).', '<b>{series}</b>: The title of the series.' ))) class Meta: model = Series fields = '__all__'
[docs]class SeriesAdmin(admin.ModelAdmin): """Admin model for :class:`~reader.models.Series`.""" form = SeriesForm inlines = (SeriesAliasInline,) list_display = ('cover_image', 'title', 'created', 'modified', 'completed') list_display_links = ('title',) date_hierarchy = 'created' ordering = ('-modified',) search_fields = ('title',) autocomplete_fields = ('categories',) list_filter = ( ('authors', filters.related_filter('author')), ('artists', filters.related_filter('artist')), ('categories', filters.related_filter('category')), filters.boolean_filter( 'status', 'completed', ('Completed', 'Ongoing') ) ) actions = ('toggle_completed',) empty_value_display = 'N/A'
[docs] def cover_image(self, obj: Series) -> str: """ Get the cover of the series as an HTML ``<img>``. :param obj: A ``Series`` model instance. :return: An ``<img>`` tag with the series cover. """ return utils.img_tag(obj.cover, 'cover', height=75)
cover_image.short_description = 'cover'
[docs] def toggle_completed(self, request: 'HttpRequest', queryset: 'QuerySet'): """ Toggle the status of the selected series. :param request: The original request. :param queryset: The original queryset. """ queryset.update(completed=Q(completed=False))
toggle_completed.short_description = 'Toggle status of selected series'
[docs]class AuthorAdmin(admin.ModelAdmin): """Admin model for :class:`~reader.models.Author`.""" inlines = (AuthorAliasInline,) list_display = ('name', 'aliases') search_fields = ('name', 'aliases__alias')
[docs] def aliases(self, obj: Author) -> str: """ Get the author's aliases as a string. :param obj: An ``Author`` model instance. :return: A comma-separated list of aliases. """ return ', '.join(obj.aliases.values_list('alias', flat=True))
[docs]class ArtistAdmin(admin.ModelAdmin): """Admin model for :class:`~reader.models.Artist`.""" inlines = (ArtistAliasInline,) list_display = ('name', 'aliases') search_fields = ('name', 'aliases__alias')
[docs] def aliases(self, obj: Artist) -> str: """ Get the artist's aliases as a string. :param obj: An ``Artist`` model instance. :return: A comma-separated list of aliases. """ return ', '.join(obj.aliases.values_list('alias', flat=True))
[docs]class CategoryAdmin(admin.ModelAdmin): """Admin model for :class:`~reader.models.Category`.""" exclude = ('id',) list_display = ('name', 'description') search_fields = ('name', 'description')
[docs] def get_readonly_fields(self, request: 'HttpRequest', obj: Optional[Category] = None) -> Tuple: """ Return the fields that cannot be changed. Once a ``Category`` object has been created, its :attr:`~reader.models.Category.name` cannot be altered. :param request: The original request. :param obj: A ``Category`` model instance. :return: A tuple of readonly fields. """ return ('name',) if obj else ()
admin.site.register(Chapter, ChapterAdmin) admin.site.register(Series, SeriesAdmin) admin.site.register(Author, AuthorAdmin) admin.site.register(Artist, ArtistAdmin) admin.site.register(Category, CategoryAdmin) __all__ = [ 'SeriesAliasInline', 'AuthorAliasInline', 'ArtistAliasInline', 'ChapterAdmin', 'SeriesAdmin', 'AuthorAdmin', 'ArtistAdmin', 'CategoryAdmin' ]