class MemberAdmin(admin.ModelAdmin): """Admin model for :class:`~groups.models.Member`.""" inlines = (MemberRoleInline,) ordering = (Lower('name'),) list_display = ('name', '_twitter', 'discord', 'irc', '_reddit') search_fields = ('name', 'twitter', 'discord', 'irc', 'reddit') list_filter = ( ('roles__group__name', filters.related_filter('group')), ('roles__role', filters.related_filter('role')), ) def _twitter(self, obj: Member) -> str: if not obj.twitter: return '' return format_html( '<a href="https://twitter.com/{0}" rel="noopener noreferrer"' ' target="_blank">@{0}</a>', obj.twitter ) _twitter.short_description = 'twitter' _twitter.admin_order_field = 'twitter' def _reddit(self, obj: Member) -> str: if not obj.reddit: return '' return format_html( '<a href="https://reddit.com/u/{0}" rel="noopener noreferrer"' ' target="_blank">/u/{0}</a>', obj.reddit ) _reddit.short_description = 'reddit' _reddit.admin_order_field = 'reddit'
class SeriesAdmin(admin.ModelAdmin): """Admin model for :class:`~reader.models.Series`.""" form = SeriesForm inlines = (alias_inline('series'), ) list_display = ('cover_image', 'title', 'created', 'modified', 'completed') list_display_links = ('title', ) date_hierarchy = 'created' ordering = ('-modified', ) search_fields = ('title', 'aliases__name') 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' 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' 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'
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' 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) 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 ChapterAdmin(admin.ModelAdmin): """Admin model for :class:`~reader.models.Chapter`.""" inlines = (PageInline,) date_hierarchy = 'published' list_display = ( 'preview', 'title', 'series', 'volume', '_number', 'published', 'modified', 'views', 'final' ) list_display_links = ('title',) ordering = ('-modified',) sortable_by = ( 'title', 'series', 'volume', 'number', 'published', 'modified', 'views' ) 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), ('series__manager', filters.related_filter('manager')), ) actions = ('toggle_final',) empty_value_display = 'N/A' @admin.display(ordering='number', description='number') def _number(self, obj: Chapter) -> str: return f'{obj.number:g}' 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._thumb, 'preview', height=50) @admin.display(description='Toggle status of selected chapters') 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)) def get_form(self, request: HttpRequest, obj: Optional[Chapter], **kwargs) -> ModelForm: # pragma: no cover form = super().get_form(request, obj, **kwargs) if 'series' in form.base_fields and not request.user.is_superuser: form.base_fields['series'].queryset = \ Series.objects.filter(manager_id=request.user.id) return form def has_change_permission(self, request: HttpRequest, obj: Optional[Chapter] = None) -> bool: """ Return ``True`` if editing the object is permitted. | Superusers can edit any chapter. | Scanlators can only edit chapters of series they manage. :param request: The original request. :param obj: A ``Chapter`` model instance. :return: ``True`` if the user is allowed to edit the chapter. """ if request.user.is_superuser or obj is None: return True return obj.series.manager_id == request.user.id def has_delete_permission(self, request: HttpRequest, obj: Optional[Chapter] = None) -> bool: """ Return ``True`` if deleting the object is permitted. | Superusers delete edit any chapter. | Scanlators can only delete chapters of series they manage. :param request: The original request. :param obj: A ``Chapter`` model instance. :return: ``True`` if the user is allowed to delete the chapter. """ if request.user.is_superuser or obj is None: return True return obj.series.manager_id == request.user.id
class SeriesAdmin(admin.ModelAdmin): """Admin model for :class:`~reader.models.Series`.""" inlines = (alias_inline('series'),) list_display = ( 'cover_image', 'title', 'manager', 'created', 'modified', 'views', 'completed', 'licensed' ) list_display_links = ('title',) date_hierarchy = 'created' ordering = ('-modified',) sortable_by = ('title', 'created', 'modified', 'views') search_fields = ('title', 'aliases__name') 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') ), ('manager', filters.related_filter('manager')), ) actions = ('toggle_completed', 'toggle_licensed') empty_value_display = 'N/A' def get_form(self, request: HttpRequest, obj: Optional[Series] = None, change: bool = False, **kwargs) -> ModelForm: form = super().get_form(request, obj, change, **kwargs) if 'format' in form.base_fields: form.base_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.' ))) if 'manager' in form.base_fields: form.base_fields['manager'].initial = request.user.id if request.user.is_superuser: # pragma: no cover form.base_fields['manager'].required = False else: # pragma: no cover form.base_fields['manager'].widget = HiddenInput() return form def get_queryset(self, request: HttpRequest) -> QuerySet: return super().get_queryset(request).annotate( views=Sum('chapters__views', distinct=True) ) @admin.display(ordering='views') def views(self, obj: Series) -> int: """ Get the total views of all chapters of the series. :param obj: A ``Series`` model instance. :return: The sum of chapter views. """ return getattr(obj, 'views') or 0 @admin.display(description='cover') 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) @admin.display(description='Toggle status of selected series') def toggle_completed(self, request: HttpRequest, queryset: QuerySet): """ Toggle the publication status of the selected series. :param request: The original request. :param queryset: The original queryset. """ queryset.update(completed=Q(completed=False)) @admin.display(description='Toggle licensing of selected series') def toggle_licensed(self, request: HttpRequest, queryset: QuerySet): """ Toggle the licensing status of the selected series. :param request: The original request. :param queryset: The original queryset. """ queryset.update(licensed=Q(licensed=False)) def has_change_permission(self, request: HttpRequest, obj: Optional[Series] = None) -> bool: """ Return ``True`` if editing the object is permitted. | Superusers can edit any series. | Scanlators can only edit series they manage. :param request: The original request. :param obj: A ``Series`` model instance. :return: ``True`` if the user is allowed to edit the series. """ if request.user.is_superuser or obj is None: return True return obj.manager_id == request.user.id def has_delete_permission(self, request: HttpRequest, obj: Optional[Series] = None) -> bool: """ Return ``True`` if deleting the object is permitted. | Superusers can delete any series. | Scanlators can only delete series they manage. :param request: The original request. :param obj: A ``Series`` model instance. :return: ``True`` if the user is allowed to delete the series. """ if request.user.is_superuser or obj is None: return True return obj.manager_id == request.user.id
class GroupAdmin(admin.ModelAdmin): """Admin model for :class:`~groups.models.Group`.""" exclude = ('id',) ordering = (Lower('name'),) list_display = ('image', 'name', '_website', 'manager', 'description') search_fields = ('name', 'website', 'description') list_display_links = ('name',) list_filter = ( ('manager', filters.related_filter('manager')), ) empty_value_display = 'N/A' def get_form(self, request: 'HttpRequest', obj: Optional[Group] = None, change: bool = False, **kwargs) -> 'ModelForm': form = super().get_form(request, obj, change, **kwargs) if 'manager' in form.base_fields: form.base_fields['manager'].initial = request.user.id if not request.user.is_superuser: # pragma: no cover form.base_fields['manager'].widget = HiddenInput() return form def image(self, obj: Group) -> str: """ Get the logo of the group as an HTML ``<img>``. :param obj: A ``Group`` model instance. :return: An ``<img>`` tag with the group's logo. """ return utils.img_tag(obj.logo, 'logo', height=25) image.short_description = 'logo' def _website(self, obj: Group) -> str: if not obj.website: return '' return format_html( '<a href="{0}" rel="noopener noreferrer"' ' target="_blank">{0}</a>', obj.website ) _website.short_description = 'website' _website.admin_order_field = 'website' def has_change_permission(self, request: 'HttpRequest', obj: Optional[Group] = None) -> bool: """ Return ``True`` if editing the object is permitted. | Superusers can edit any group. | Scanlators can only edit groups they manage. :param request: The original request. :param obj: A ``Group`` model instance. :return: ``True`` if the user is allowed to edit the group. """ if request.user.is_superuser or obj is None: return True return obj.manager_id == request.user.id def has_delete_permission(self, request: 'HttpRequest', obj: Optional[Group] = None) -> bool: """ Return ``True`` if deleting the object is permitted. | Superusers can delete any group. | Scanlators can only delete groups they manage. :param request: The original request. :param obj: A ``Group`` model instance. :return: ``True`` if the user is allowed to delete the group. """ if request.user.is_superuser or obj is None: return True return obj.manager_id == request.user.id