Adds type checking for templatetags

This commit is contained in:
Mouse Reeve 2024-08-27 17:34:16 -07:00
parent ef30c7cf65
commit 676a6b4b7f
18 changed files with 93 additions and 68 deletions

View file

@ -1,8 +1,9 @@
""" database schema for books and shelves """
from __future__ import annotations
from itertools import chain
import re
from typing import Any, Dict, Optional, Iterable
from typing import Any, Dict, Optional, Iterable, TYPE_CHECKING
from typing_extensions import Self
from django.contrib.postgres.search import SearchVectorField
@ -33,6 +34,9 @@ from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
from .base_model import BookWyrmModel
from . import fields
if TYPE_CHECKING:
from bookwyrm.models import Author
class BookDataModel(ObjectMixin, BookWyrmModel):
"""fields shared between editable book data (books, works, authors)"""
@ -415,7 +419,7 @@ class Work(OrderedCollectionPageMixin, Book):
"""in case the default edition is not set"""
return self.editions.order_by("-edition_rank").first()
def author_edition(self, author):
def author_edition(self, author: Author) -> Any:
"""in case the default edition doesn't have the required author"""
return self.editions.filter(authors=author).order_by("-edition_rank").first()

View file

@ -1,5 +1,7 @@
""" template filters """
from typing import Any
from django import template
from django.db.models import QuerySet
from bookwyrm import models
@ -7,13 +9,13 @@ register = template.Library()
@register.filter(name="review_count")
def get_review_count(book):
def get_review_count(book: models.Edition) -> int:
"""how many reviews?"""
return models.Review.objects.filter(deleted=False, book=book).count()
@register.filter(name="book_description")
def get_book_description(book):
def get_book_description(book: models.Edition) -> Any:
"""use the work's text if the book doesn't have it"""
if book.description:
return book.description
@ -24,12 +26,12 @@ def get_book_description(book):
@register.simple_tag(takes_context=False)
def get_book_file_links(book):
def get_book_file_links(book: models.Edition) -> QuerySet[models.FileLink]:
"""links for a book"""
return book.file_links.filter(domain__status="approved")
@register.filter(name="author_edition")
def get_author_edition(book, author):
def get_author_edition(book: models.Work, author: models.Author) -> Any:
"""default edition for a book on the author page"""
return book.author_edition(author)

View file

@ -7,18 +7,18 @@ register = template.Library()
@register.filter(name="uptime")
def uptime(seconds):
def uptime(seconds: float) -> str:
"""Seconds uptime to a readable format"""
return str(datetime.timedelta(seconds=seconds))
@register.filter(name="runtime")
def runtime(timestamp):
def runtime(timestamp: float) -> datetime.timedelta:
"""How long has it been?"""
return datetime.datetime.now() - datetime.datetime.fromtimestamp(timestamp)
@register.filter(name="shortname")
def shortname(name):
def shortname(name: str) -> str:
"""removes bookwyrm.celery..."""
return ".".join(name.split(".")[-2:])

View file

@ -1,4 +1,6 @@
""" additional formatting of dates """
from typing import Any
from django import template
from django.template import defaultfilters
from django.contrib.humanize.templatetags.humanize import naturalday
@ -9,7 +11,7 @@ register = template.Library()
@register.filter(expects_localtime=True)
def naturalday_partial(date, arg=None):
def naturalday_partial(date: Any, arg: Any = None) -> str | None:
"""chooses appropriate precision if date is a PartialDate object
If arg is a Django-defined format such as "DATE_FORMAT", it will be adjusted

View file

@ -1,13 +1,15 @@
""" tags used on the feed pages """
from typing import Any
from django import template
from bookwyrm.views.feed import get_suggested_books
from bookwyrm import models
register = template.Library()
@register.filter(name="load_subclass")
def load_subclass(status):
def load_subclass(status: models.Status) -> models.Status:
"""sometimes you didn't select_subclass"""
if hasattr(status, "quotation"):
return status.quotation
@ -21,7 +23,7 @@ def load_subclass(status):
@register.simple_tag(takes_context=True)
def suggested_books(context):
def suggested_books(context: dict[str, Any]) -> Any:
"""get books for suggested books panel"""
# this happens here instead of in the view so that the template snippet can
# be cached in the template

View file

@ -7,21 +7,21 @@ register = template.Library()
@register.filter(name="has_groups")
def has_groups(user):
def has_groups(user: models.User) -> bool:
"""whether or not the user has a pending invitation to join this group"""
return models.GroupMember.objects.filter(user=user).exists()
@register.filter(name="is_member")
def is_member(group, user):
def is_member(group: models.Group, user: models.User) -> bool:
"""whether or not the user is a member of this group"""
return models.GroupMember.objects.filter(group=group, user=user).exists()
@register.filter(name="is_invited")
def is_invited(group, user):
def is_invited(group: models.Group, user: models.User) -> bool:
"""whether or not the user has a pending invitation to join this group"""
return models.GroupMemberInvitation.objects.filter(group=group, user=user).exists()

View file

@ -1,4 +1,5 @@
""" template filters for status interaction buttons """
from typing import Any
from django import template
from bookwyrm import models
@ -9,7 +10,7 @@ register = template.Library()
@register.filter(name="liked")
def get_user_liked(user, status):
def get_user_liked(user: models.User, status: models.Status) -> Any:
"""did the given user fav a status?"""
return get_or_set(
f"fav-{user.id}-{status.id}",
@ -21,7 +22,7 @@ def get_user_liked(user, status):
@register.filter(name="boosted")
def get_user_boosted(user, status):
def get_user_boosted(user: models.User, status: models.Status) -> Any:
"""did the given user fav a status?"""
return get_or_set(
f"boost-{user.id}-{status.id}",
@ -32,13 +33,13 @@ def get_user_boosted(user, status):
@register.filter(name="saved")
def get_user_saved_lists(user, book_list):
def get_user_saved_lists(user: models.User, book_list: models.List) -> bool:
"""did the user save a list"""
return user.saved_lists.filter(id=book_list.id).exists()
@register.simple_tag(takes_context=True)
def get_relationship(context, user_object):
def get_relationship(context: dict[str, Any], user_object: models.User) -> Any:
"""caches the relationship between the logged in user and another user"""
user = context["request"].user
return get_or_set(
@ -50,7 +51,9 @@ def get_relationship(context, user_object):
)
def get_relationship_name(user, user_object):
def get_relationship_name(
user: models.User, user_object: models.User
) -> dict[str, bool]:
"""returns the relationship type"""
types = {
"is_following": False,

View file

@ -1,6 +1,7 @@
""" template filters """
from typing import Optional
from django import template
from django.db.models import Avg, StdDev, Count, F, Q
from django.db.models import Avg, StdDev, Count, F, Q, QuerySet
from bookwyrm import models
@ -8,7 +9,7 @@ register = template.Library()
@register.simple_tag(takes_context=False)
def get_book_superlatives():
def get_book_superlatives() -> dict[str, Optional[models.Work]]:
"""get book stats for the about page"""
total_ratings = models.Review.objects.filter(local=True, deleted=False).count()
data = {}
@ -67,7 +68,7 @@ def get_book_superlatives():
@register.simple_tag(takes_context=False)
def get_landing_books():
def get_landing_books() -> list[QuerySet[models.Edition]]:
"""list of books for the landing page"""
return list(
set(

View file

@ -5,7 +5,7 @@ register = template.Library()
@register.simple_tag(takes_context=False)
def get_lang():
def get_lang() -> str:
"""get current language, strip to the first two letters"""
language = utils.translation.get_language()
return language[0 : language.find("-")]

View file

@ -1,4 +1,5 @@
""" template filters """
from typing import Any
from django import template
from bookwyrm.views.status import to_markdown
@ -7,7 +8,7 @@ register = template.Library()
@register.filter(name="to_markdown")
def get_markdown(content):
def get_markdown(content: str) -> Any:
"""convert markdown to html"""
if content:
return to_markdown(content)

View file

@ -1,5 +1,7 @@
""" tags used on the feed pages """
from typing import Optional
from django import template
from bookwyrm import models
from bookwyrm.templatetags.feed_page_tags import load_subclass
@ -7,7 +9,7 @@ register = template.Library()
@register.simple_tag(takes_context=False)
def related_status(notification):
def related_status(notification: models.Notification) -> Optional[models.Status]:
"""for notifications"""
if not notification.related_status:
return None
@ -15,6 +17,6 @@ def related_status(notification):
@register.simple_tag(takes_context=False)
def get_related_users(notification):
def get_related_users(notification: models.Notification) -> list[models.User]:
"""Who actually was it who liked your post"""
return list(reversed(list(notification.related_users.distinct())))[:10]

View file

@ -1,4 +1,5 @@
""" template filters """
from typing import Any
from django import template
from django.db.models import Avg
@ -10,7 +11,7 @@ register = template.Library()
@register.filter(name="rating")
def get_rating(book, user):
def get_rating(book: models.Edition, user: models.User) -> Any:
"""get the overall rating of a book"""
# this shouldn't happen, but it CAN
if not book.parent_work:
@ -29,7 +30,7 @@ def get_rating(book, user):
@register.filter(name="user_rating")
def get_user_rating(book, user):
def get_user_rating(book: models.Edition, user: models.User) -> Any:
"""get a user's rating of a book"""
rating = (
models.Review.objects.filter(

View file

@ -1,6 +1,8 @@
""" Filters and tags related to shelving books """
from typing import Any
from django import template
from django.utils.translation import gettext_lazy as _
from django_stubs_ext import StrPromise
from bookwyrm import models
from bookwyrm.utils import cache
@ -19,7 +21,7 @@ SHELF_NAMES = {
@register.filter(name="is_book_on_shelf")
def get_is_book_on_shelf(book, shelf):
def get_is_book_on_shelf(book: models.Edition, shelf: models.Shelf) -> Any:
"""is a book on a shelf"""
return cache.get_or_set(
f"book-on-shelf-{book.id}-{shelf.id}",
@ -31,7 +33,7 @@ def get_is_book_on_shelf(book, shelf):
@register.filter(name="next_shelf")
def get_next_shelf(current_shelf):
def get_next_shelf(current_shelf: str) -> str:
"""shelf you'd use to update reading progress"""
if current_shelf == "to-read":
return "reading"
@ -45,7 +47,7 @@ def get_next_shelf(current_shelf):
@register.filter(name="translate_shelf_name")
def get_translated_shelf_name(shelf):
def get_translated_shelf_name(shelf: models.Shelf | dict[str, str]) -> str | StrPromise:
"""produce translated shelf identifiername"""
if not shelf:
return ""
@ -59,7 +61,7 @@ def get_translated_shelf_name(shelf):
@register.simple_tag(takes_context=True)
def active_shelf(context, book):
def active_shelf(context: dict[str, Any], book: models.Edition) -> Any:
"""check what shelf a user has a book on, if any"""
user = context["request"].user
return cache.get_or_set(
@ -78,7 +80,7 @@ def active_shelf(context, book):
@register.simple_tag(takes_context=False)
def latest_read_through(book, user):
def latest_read_through(book: models.Edition, user: models.User) -> Any:
"""the most recent read activity"""
return cache.get_or_set(
f"latest_read_through-{user.id}-{book.id}",

View file

@ -6,6 +6,6 @@ register = template.Library()
@register.filter(name="half_star")
def get_half_star(value):
def get_half_star(value: str) -> str:
"""one of those things that's weirdly hard with templates"""
return f"{value}.5"

View file

@ -1,4 +1,5 @@
""" template filters """
from typing import Any, Optional
from dateutil.relativedelta import relativedelta
from django import template
from django.contrib.humanize.templatetags.humanize import naturaltime, naturalday
@ -12,7 +13,7 @@ register = template.Library()
@register.filter(name="mentions")
def get_mentions(status, user):
def get_mentions(status: models.Status, user: models.User) -> str:
"""people to @ in a reply: the parent and all mentions"""
mentions = set([status.user] + list(status.mention_users.all()))
return (
@ -21,7 +22,7 @@ def get_mentions(status, user):
@register.filter(name="replies")
def get_replies(status):
def get_replies(status: models.Status) -> Any:
"""get all direct replies to a status"""
# TODO: this limit could cause problems
return models.Status.objects.filter(
@ -31,7 +32,7 @@ def get_replies(status):
@register.filter(name="parent")
def get_parent(status):
def get_parent(status: models.Status) -> Any:
"""get the reply parent for a status"""
return (
models.Status.objects.filter(id=status.reply_parent_id)
@ -41,7 +42,7 @@ def get_parent(status):
@register.filter(name="boosted_status")
def get_boosted(boost):
def get_boosted(boost: models.Boost) -> Any:
"""load a boosted status. have to do this or it won't get foreign keys"""
return (
models.Status.objects.select_subclasses()
@ -52,7 +53,7 @@ def get_boosted(boost):
@register.filter(name="published_date")
def get_published_date(date):
def get_published_date(date: str) -> Any:
"""less verbose combo of humanize filters"""
if not date:
return ""
@ -66,7 +67,7 @@ def get_published_date(date):
@register.simple_tag()
def get_header_template(status):
def get_header_template(status: models.Status) -> Any:
"""get the path for the status template"""
if isinstance(status, models.Boost):
status = status.boosted_status
@ -82,6 +83,6 @@ def get_header_template(status):
@register.simple_tag(takes_context=False)
def load_book(status):
def load_book(status: models.Status) -> Optional[models.Book]:
"""how many users that you follow, follow them"""
return status.book if hasattr(status, "book") else status.mention_books.first()

View file

@ -1,12 +1,14 @@
""" template filters """
from typing import Any, Optional
from django import template
from bookwyrm import models
register = template.Library()
@register.simple_tag(takes_context=True)
def mutuals_count(context, user):
def mutuals_count(context: dict[str, Any], user: models.User) -> Optional[int]:
"""how many users that you follow, follow them"""
viewer = context["request"].user
if not viewer.is_authenticated:

View file

@ -1,46 +1,50 @@
""" template filters for really common utilities """
from typing import Any, Optional
import os
import re
from uuid import uuid4
from urllib.parse import urlparse
from django import template
from django.contrib.auth.models import Group
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.templatetags.static import static
from django_stubs_ext import StrPromise
from bookwyrm.models import User
from bookwyrm.models import Author, Edition, User
from bookwyrm.settings import INSTANCE_ACTOR_USERNAME
register = template.Library()
@register.filter(name="uuid")
def get_uuid(identifier):
def get_uuid(identifier: str) -> str:
"""for avoiding clashing ids when there are many forms"""
return f"{identifier}{uuid4()}"
@register.simple_tag(takes_context=False)
def join(*args):
def join(*args: tuple[Any]) -> str:
"""concatenate an arbitrary set of values"""
return "_".join(str(a) for a in args)
@register.filter(name="username")
def get_user_identifier(user):
def get_user_identifier(user: User) -> str:
"""use localname for local users, username for remote"""
return user.localname if user.localname else user.username
return user.localname if user.localname else user.username or ""
@register.filter(name="user_from_remote_id")
def get_user_identifier_from_remote_id(remote_id):
def get_user_identifier_from_remote_id(remote_id: str) -> Optional[User]:
"""get the local user id from their remote id"""
user = User.objects.get(remote_id=remote_id)
return user if user else None
@register.filter(name="book_title")
def get_title(book, too_short=5):
def get_title(book: Edition, too_short: int = 5) -> Any:
"""display the subtitle if the title is short"""
if not book:
return ""
@ -54,7 +58,7 @@ def get_title(book, too_short=5):
@register.simple_tag(takes_context=False)
def comparison_bool(str1, str2, reverse=False):
def comparison_bool(str1: str, str2: str, reverse: bool = False) -> bool:
"""idk why I need to write a tag for this, it returns a bool"""
if reverse:
return str1 != str2
@ -62,7 +66,7 @@ def comparison_bool(str1, str2, reverse=False):
@register.filter(is_safe=True)
def truncatepath(value, arg):
def truncatepath(value: Any, arg: Any) -> Any:
"""Truncate a path by removing all directories except the first and truncating"""
path = os.path.normpath(value.name)
path_list = path.split(os.sep)
@ -74,7 +78,9 @@ def truncatepath(value, arg):
@register.simple_tag(takes_context=False)
def get_book_cover_thumbnail(book, size="medium", ext="jpg"):
def get_book_cover_thumbnail(
book: Edition, size: str = "medium", ext: str = "jpg"
) -> Any:
"""Returns a book thumbnail at the specified size and extension,
with fallback if needed"""
if size == "":
@ -87,7 +93,7 @@ def get_book_cover_thumbnail(book, size="medium", ext="jpg"):
@register.filter(name="get_isni_bio")
def get_isni_bio(existing, author):
def get_isni_bio(existing: int, author: Author) -> str:
"""Returns the isni bio string if an existing author has an isni listed"""
auth_isni = re.sub(r"\D", "", str(author.isni))
if len(existing) == 0:
@ -101,7 +107,7 @@ def get_isni_bio(existing, author):
# pylint: disable=unused-argument
@register.filter(name="get_isni", needs_autoescape=True)
def get_isni(existing, author, autoescape=True):
def get_isni(existing: str, author: Author, autoescape: bool = True) -> str:
"""Returns the isni ID if an existing author has an ISNI listing"""
auth_isni = re.sub(r"\D", "", str(author.isni))
if len(existing) == 0:
@ -116,7 +122,7 @@ def get_isni(existing, author, autoescape=True):
@register.simple_tag(takes_context=False)
def id_to_username(user_id):
def id_to_username(user_id: str) -> str | StrPromise:
"""given an arbitrary remote id, return the username"""
if user_id:
url = urlparse(user_id)
@ -130,7 +136,7 @@ def id_to_username(user_id):
@register.filter(name="get_file_size")
def get_file_size(nbytes):
def get_file_size(nbytes: int) -> str:
"""display the size of a file in human readable terms"""
try:
@ -148,13 +154,13 @@ def get_file_size(nbytes):
@register.filter(name="get_user_permission")
def get_user_permission(user):
def get_user_permission(user: User) -> Group | str:
"""given a user, return their permission level"""
return user.groups.first() or "User"
@register.filter(name="is_instance_admin")
def is_instance_admin(localname):
def is_instance_admin(localname: str) -> bool:
"""Returns a boolean indicating whether the user is the instance admin account"""
return localname == INSTANCE_ACTOR_USERNAME

View file

@ -10,23 +10,19 @@ django_settings_module = "bookwyrm.settings"
ignore_errors = True
implicit_reexport = True
[mypy-bookwyrm.connectors.*]
ignore_errors = False
[mypy-bookwyrm.models.author]
ignore_errors = False
allow_untyped_calls = True
disable_error_code = import-untyped, assignment
[mypy-bookwyrm.connectors.*]
[mypy-bookwyrm.utils.*]
ignore_errors = False
[mypy-bookwyrm.importers.*]
ignore_errors = False
[mypy-bookwyrm.isbn.*]
ignore_errors = False
[mypy-celerywyrm.*]
ignore_errors = False
[mypy-bookwyrm.templatetags.*]
ignore_errors = False
allow_untyped_calls = True
disable_error_code = attr-defined, arg-type, misc