more typehints
wip wip
This commit is contained in:
parent
bc79c957de
commit
a2ffff1760
29 changed files with 247 additions and 88 deletions
|
@ -18,6 +18,7 @@ work data seems asymmetric (a book links to a work, but may not listed in that w
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from os.path import exists
|
from os.path import exists
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -62,6 +63,8 @@ class EditionSchema(EditionInSchema, BaseSchema):
|
||||||
|
|
||||||
|
|
||||||
class Edition(Item):
|
class Edition(Item):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
works: "models.ManyToManyField[Work, Edition]"
|
||||||
category = ItemCategory.Book
|
category = ItemCategory.Book
|
||||||
url_path = "book"
|
url_path = "book"
|
||||||
|
|
||||||
|
@ -164,17 +167,17 @@ class Edition(Item):
|
||||||
return detect_isbn_asin(lookup_id_value)
|
return detect_isbn_asin(lookup_id_value)
|
||||||
return super().lookup_id_cleanup(lookup_id_type, lookup_id_value)
|
return super().lookup_id_cleanup(lookup_id_type, lookup_id_value)
|
||||||
|
|
||||||
def merge_to(self, to_item: "Edition | None"):
|
def merge_to(self, to_item: "Edition | None"): # type: ignore[reportIncompatibleMethodOverride]
|
||||||
super().merge_to(to_item)
|
super().merge_to(to_item)
|
||||||
if to_item:
|
if to_item:
|
||||||
for work in self.works.all():
|
for work in self.works.all():
|
||||||
to_item.works.add(work)
|
to_item.works.add(work)
|
||||||
self.works.clear()
|
self.works.clear()
|
||||||
|
|
||||||
def delete(self, using=None, soft=True, *args, **kwargs):
|
def delete(self, using=None, keep_parents=False, soft=True, *args, **kwargs):
|
||||||
if soft:
|
if soft:
|
||||||
self.works.clear()
|
self.works.clear()
|
||||||
return super().delete(using, soft, *args, **kwargs)
|
return super().delete(using, soft, keep_parents, *args, **kwargs)
|
||||||
|
|
||||||
def update_linked_items_from_external_resource(self, resource):
|
def update_linked_items_from_external_resource(self, resource):
|
||||||
"""add Work from resource.metadata['work'] if not yet"""
|
"""add Work from resource.metadata['work'] if not yet"""
|
||||||
|
@ -279,7 +282,7 @@ class Work(Item):
|
||||||
]
|
]
|
||||||
return [(i.value, i.label) for i in id_types]
|
return [(i.value, i.label) for i in id_types]
|
||||||
|
|
||||||
def merge_to(self, to_item: "Work | None"):
|
def merge_to(self, to_item: "Work | None"): # type: ignore[reportIncompatibleMethodOverride]
|
||||||
super().merge_to(to_item)
|
super().merge_to(to_item)
|
||||||
if to_item:
|
if to_item:
|
||||||
for edition in self.editions.all():
|
for edition in self.editions.all():
|
||||||
|
@ -293,10 +296,10 @@ class Work(Item):
|
||||||
to_item.other_title += [self.title] # type: ignore
|
to_item.other_title += [self.title] # type: ignore
|
||||||
to_item.save()
|
to_item.save()
|
||||||
|
|
||||||
def delete(self, using=None, soft=True, *args, **kwargs):
|
def delete(self, using=None, keep_parents=False, soft=True, *args, **kwargs):
|
||||||
if soft:
|
if soft:
|
||||||
self.editions.clear()
|
self.editions.clear()
|
||||||
return super().delete(using, soft, *args, **kwargs)
|
return super().delete(using, keep_parents, soft, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Series(Item):
|
class Series(Item):
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
from catalog.common import *
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from catalog.common import Item, ItemCategory
|
||||||
|
|
||||||
|
|
||||||
class Collection(Item):
|
class Collection(Item):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from journal.models import Collection as JournalCollection
|
||||||
|
|
||||||
|
journal_item: "JournalCollection"
|
||||||
category = ItemCategory.Collection
|
category = ItemCategory.Collection
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -13,10 +13,11 @@ class SoftDeleteMixin:
|
||||||
def clear(self):
|
def clear(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def delete(self, using=None, soft=True, *args, **kwargs):
|
def delete(self, using=None, keep_parents=False, soft=True, *args, **kwargs):
|
||||||
if soft:
|
if soft:
|
||||||
self.clear()
|
self.clear()
|
||||||
self.is_deleted = True
|
self.is_deleted = True
|
||||||
self.save(using=using) # type: ignore
|
self.save(using=using) # type: ignore
|
||||||
|
return 0, {}
|
||||||
else:
|
else:
|
||||||
return super().delete(using=using, *args, **kwargs) # type: ignore
|
return super().delete(using=using, keep_parents=keep_parents, *args, **kwargs) # type: ignore
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import TYPE_CHECKING, Any, Iterable, Type, cast
|
from typing import TYPE_CHECKING, Any, Iterable, Self, Type, cast
|
||||||
|
|
||||||
from auditlog.context import disable_auditlog
|
from auditlog.context import disable_auditlog
|
||||||
from auditlog.models import AuditlogHistoryField, LogEntry
|
from auditlog.models import AuditlogHistoryField, LogEntry
|
||||||
|
@ -9,7 +9,8 @@ from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.db import connection, models
|
from django.db import connection, models
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet, Value
|
||||||
|
from django.template.defaultfilters import default
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.baseconv import base62
|
from django.utils.baseconv import base62
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -19,12 +20,14 @@ from polymorphic.models import PolymorphicModel
|
||||||
|
|
||||||
from catalog.common import jsondata
|
from catalog.common import jsondata
|
||||||
|
|
||||||
from .mixins import SoftDeleteMixin
|
|
||||||
from .utils import DEFAULT_ITEM_COVER, item_cover_path, resource_cover_path
|
from .utils import DEFAULT_ITEM_COVER, item_cover_path, resource_cover_path
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from journal.models import Collection
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
|
from .sites import ResourceContent
|
||||||
|
|
||||||
|
|
||||||
class SiteName(models.TextChoices):
|
class SiteName(models.TextChoices):
|
||||||
Unknown = "unknown", _("Unknown")
|
Unknown = "unknown", _("Unknown")
|
||||||
|
@ -76,9 +79,9 @@ class IdType(models.TextChoices):
|
||||||
Bandcamp = "bandcamp", _("Bandcamp")
|
Bandcamp = "bandcamp", _("Bandcamp")
|
||||||
Spotify_Album = "spotify_album", _("Spotify Album")
|
Spotify_Album = "spotify_album", _("Spotify Album")
|
||||||
Spotify_Show = "spotify_show", _("Spotify Podcast")
|
Spotify_Show = "spotify_show", _("Spotify Podcast")
|
||||||
Discogs_Release = "discogs_release", ("Discogs Release")
|
Discogs_Release = "discogs_release", _("Discogs Release")
|
||||||
Discogs_Master = "discogs_master", ("Discogs Master")
|
Discogs_Master = "discogs_master", _("Discogs Master")
|
||||||
MusicBrainz = "musicbrainz", ("MusicBrainz ID")
|
MusicBrainz = "musicbrainz", _("MusicBrainz ID")
|
||||||
# DoubanBook_Author = "doubanbook_author", _("Douban Book Author")
|
# DoubanBook_Author = "doubanbook_author", _("Douban Book Author")
|
||||||
# DoubanCelebrity = "doubanmovie_celebrity", _("Douban Movie Celebrity")
|
# DoubanCelebrity = "doubanmovie_celebrity", _("Douban Movie Celebrity")
|
||||||
# Goodreads_Author = "goodreads_author", _("Goodreads Author")
|
# Goodreads_Author = "goodreads_author", _("Goodreads Author")
|
||||||
|
@ -168,14 +171,16 @@ class PrimaryLookupIdDescriptor(object): # TODO make it mixin of Field
|
||||||
def __init__(self, id_type: IdType):
|
def __init__(self, id_type: IdType):
|
||||||
self.id_type = id_type
|
self.id_type = id_type
|
||||||
|
|
||||||
def __get__(self, instance, cls=None):
|
def __get__(
|
||||||
|
self, instance: "Item | None", cls: type[Any] | None = None
|
||||||
|
) -> str | Self | None:
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return self
|
return self
|
||||||
if self.id_type != instance.primary_lookup_id_type:
|
if self.id_type != instance.primary_lookup_id_type:
|
||||||
return None
|
return None
|
||||||
return instance.primary_lookup_id_value
|
return instance.primary_lookup_id_value
|
||||||
|
|
||||||
def __set__(self, instance, id_value):
|
def __set__(self, instance: "Item", id_value: str | None):
|
||||||
if id_value:
|
if id_value:
|
||||||
instance.primary_lookup_id_type = self.id_type
|
instance.primary_lookup_id_type = self.id_type
|
||||||
instance.primary_lookup_id_value = id_value
|
instance.primary_lookup_id_value = id_value
|
||||||
|
@ -246,12 +251,16 @@ class ItemSchema(BaseSchema, ItemInSchema):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Item(PolymorphicModel, SoftDeleteMixin):
|
class Item(PolymorphicModel):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
external_resources: QuerySet["ExternalResource"]
|
||||||
|
collections: QuerySet["Collection"]
|
||||||
|
merged_from_items: QuerySet["Item"]
|
||||||
|
merged_to_item_id: int
|
||||||
|
category: ItemCategory # subclass must specify this
|
||||||
url_path = "item" # subclass must specify this
|
url_path = "item" # subclass must specify this
|
||||||
type = None # subclass must specify this
|
|
||||||
child_class = None # subclass may specify this to allow link to parent item
|
child_class = None # subclass may specify this to allow link to parent item
|
||||||
parent_class = None # subclass may specify this to allow create child item
|
parent_class = None # subclass may specify this to allow create child item
|
||||||
category: ItemCategory # subclass must specify this
|
|
||||||
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
||||||
title = models.CharField(_("title"), max_length=1000, default="")
|
title = models.CharField(_("title"), max_length=1000, default="")
|
||||||
brief = models.TextField(_("description"), blank=True, default="")
|
brief = models.TextField(_("description"), blank=True, default="")
|
||||||
|
@ -288,6 +297,24 @@ class Item(PolymorphicModel, SoftDeleteMixin):
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def delete(
|
||||||
|
self,
|
||||||
|
using: Any = None,
|
||||||
|
keep_parents: bool = False,
|
||||||
|
soft: bool = True,
|
||||||
|
*args: tuple[Any, ...],
|
||||||
|
**kwargs: dict[str, Any],
|
||||||
|
) -> tuple[int, dict[str, int]]:
|
||||||
|
if soft:
|
||||||
|
self.clear()
|
||||||
|
self.is_deleted = True
|
||||||
|
self.save(using=using)
|
||||||
|
return 0, {}
|
||||||
|
else:
|
||||||
|
return super().delete(
|
||||||
|
using=using, keep_parents=keep_parents, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def history(self):
|
def history(self):
|
||||||
# can't use AuditlogHistoryField bc it will only return history with current content type
|
# can't use AuditlogHistoryField bc it will only return history with current content type
|
||||||
|
@ -324,7 +351,7 @@ class Item(PolymorphicModel, SoftDeleteMixin):
|
||||||
return lookup_id_type, lookup_id_value.strip()
|
return lookup_id_type, lookup_id_value.strip()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_best_lookup_id(cls, lookup_ids: dict[IdType, str]) -> tuple[IdType, str]:
|
def get_best_lookup_id(cls, lookup_ids: dict[str, str]) -> tuple[str, str]:
|
||||||
"""get best available lookup id, ideally commonly used"""
|
"""get best available lookup id, ideally commonly used"""
|
||||||
for t in IdealIdTypes:
|
for t in IdealIdTypes:
|
||||||
if lookup_ids.get(t):
|
if lookup_ids.get(t):
|
||||||
|
@ -406,7 +433,7 @@ class Item(PolymorphicModel, SoftDeleteMixin):
|
||||||
res.item = to_item
|
res.item = to_item
|
||||||
res.save()
|
res.save()
|
||||||
|
|
||||||
def recast_to(self, model: "type[Item]") -> "Item":
|
def recast_to(self, model: "type[Any]") -> "Item":
|
||||||
logger.warning(f"recast item {self} to {model}")
|
logger.warning(f"recast item {self} to {model}")
|
||||||
if isinstance(self, model):
|
if isinstance(self, model):
|
||||||
return self
|
return self
|
||||||
|
@ -453,7 +480,7 @@ class Item(PolymorphicModel, SoftDeleteMixin):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_url(cls, url_or_b62: str) -> "Item | None":
|
def get_by_url(cls, url_or_b62: str) -> "Self | None":
|
||||||
b62 = url_or_b62.strip().split("/")[-1]
|
b62 = url_or_b62.strip().split("/")[-1]
|
||||||
if len(b62) not in [21, 22]:
|
if len(b62) not in [21, 22]:
|
||||||
r = re.search(r"[A-Za-z0-9]{21,22}", url_or_b62)
|
r = re.search(r"[A-Za-z0-9]{21,22}", url_or_b62)
|
||||||
|
@ -469,7 +496,7 @@ class Item(PolymorphicModel, SoftDeleteMixin):
|
||||||
# prefix = id_type.strip().lower() + ':'
|
# prefix = id_type.strip().lower() + ':'
|
||||||
# return next((x[len(prefix):] for x in self.lookup_ids if x.startswith(prefix)), None)
|
# return next((x[len(prefix):] for x in self.lookup_ids if x.startswith(prefix)), None)
|
||||||
|
|
||||||
def update_lookup_ids(self, lookup_ids):
|
def update_lookup_ids(self, lookup_ids: list[tuple[str, str]]):
|
||||||
for t, v in lookup_ids:
|
for t, v in lookup_ids:
|
||||||
if t in IdealIdTypes and self.primary_lookup_id_type not in IdealIdTypes:
|
if t in IdealIdTypes and self.primary_lookup_id_type not in IdealIdTypes:
|
||||||
self.primary_lookup_id_type = t
|
self.primary_lookup_id_type = t
|
||||||
|
@ -484,25 +511,25 @@ class Item(PolymorphicModel, SoftDeleteMixin):
|
||||||
] # list of metadata keys to copy from resource to item
|
] # list of metadata keys to copy from resource to item
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy_metadata(cls, metadata):
|
def copy_metadata(cls, metadata: dict[str, Any]) -> dict[str, Any]:
|
||||||
return dict(
|
return dict(
|
||||||
(k, v)
|
(k, v)
|
||||||
for k, v in metadata.items()
|
for k, v in metadata.items()
|
||||||
if k in cls.METADATA_COPY_LIST and v is not None
|
if k in cls.METADATA_COPY_LIST and v is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
def has_cover(self):
|
def has_cover(self) -> bool:
|
||||||
return self.cover and self.cover != DEFAULT_ITEM_COVER
|
return bool(self.cover) and self.cover != DEFAULT_ITEM_COVER
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cover_image_url(self):
|
def cover_image_url(self) -> str | None:
|
||||||
return (
|
return (
|
||||||
f"{settings.SITE_INFO['site_url']}{self.cover.url}"
|
f"{settings.SITE_INFO['site_url']}{self.cover.url}" # type:ignore
|
||||||
if self.cover and self.cover != DEFAULT_ITEM_COVER
|
if self.cover and self.cover != DEFAULT_ITEM_COVER
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def merge_data_from_external_resources(self, ignore_existing_content=False):
|
def merge_data_from_external_resources(self, ignore_existing_content: bool = False):
|
||||||
"""Subclass may override this"""
|
"""Subclass may override this"""
|
||||||
lookup_ids = []
|
lookup_ids = []
|
||||||
for p in self.external_resources.all():
|
for p in self.external_resources.all():
|
||||||
|
@ -517,7 +544,7 @@ class Item(PolymorphicModel, SoftDeleteMixin):
|
||||||
self.cover = p.cover
|
self.cover = p.cover
|
||||||
self.update_lookup_ids(list(set(lookup_ids)))
|
self.update_lookup_ids(list(set(lookup_ids)))
|
||||||
|
|
||||||
def update_linked_items_from_external_resource(self, resource):
|
def update_linked_items_from_external_resource(self, resource: "ExternalResource"):
|
||||||
"""Subclass should override this"""
|
"""Subclass should override this"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -575,6 +602,9 @@ class ItemLookupId(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class ExternalResource(models.Model):
|
class ExternalResource(models.Model):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
required_resources: list[dict[str, str]]
|
||||||
|
related_resources: list[dict[str, str]]
|
||||||
item = models.ForeignKey(
|
item = models.ForeignKey(
|
||||||
Item, null=True, on_delete=models.SET_NULL, related_name="external_resources"
|
Item, null=True, on_delete=models.SET_NULL, related_name="external_resources"
|
||||||
)
|
)
|
||||||
|
@ -598,15 +628,21 @@ class ExternalResource(models.Model):
|
||||||
scraped_time = models.DateTimeField(null=True)
|
scraped_time = models.DateTimeField(null=True)
|
||||||
created_time = models.DateTimeField(auto_now_add=True)
|
created_time = models.DateTimeField(auto_now_add=True)
|
||||||
edited_time = models.DateTimeField(auto_now=True)
|
edited_time = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
required_resources = jsondata.ArrayField(
|
required_resources = jsondata.ArrayField(
|
||||||
models.CharField(), null=False, blank=False, default=list
|
models.CharField(), null=False, blank=False, default=list
|
||||||
) # links required to generate Item from this resource, e.g. parent TVShow of TVSeason
|
) # type: ignore
|
||||||
|
""" links required to generate Item from this resource, e.g. parent TVShow of TVSeason """
|
||||||
|
|
||||||
related_resources = jsondata.ArrayField(
|
related_resources = jsondata.ArrayField(
|
||||||
models.CharField(), null=False, blank=False, default=list
|
models.CharField(), null=False, blank=False, default=list
|
||||||
) # links related to this resource which may be fetched later, e.g. sub TVSeason of TVShow
|
) # type: ignore
|
||||||
|
"""links related to this resource which may be fetched later, e.g. sub TVSeason of TVShow"""
|
||||||
|
|
||||||
prematched_resources = jsondata.ArrayField(
|
prematched_resources = jsondata.ArrayField(
|
||||||
models.CharField(), null=False, blank=False, default=list
|
models.CharField(), null=False, blank=False, default=list
|
||||||
) # links to help match an existing Item from this resource
|
)
|
||||||
|
"""links to help match an existing Item from this resource"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [["id_type", "id_value"]]
|
unique_together = [["id_type", "id_value"]]
|
||||||
|
@ -645,7 +681,7 @@ class ExternalResource(models.Model):
|
||||||
return n or domain
|
return n or domain
|
||||||
return self.site_name.label
|
return self.site_name.label
|
||||||
|
|
||||||
def update_content(self, resource_content):
|
def update_content(self, resource_content: "ResourceContent"):
|
||||||
self.other_lookup_ids = resource_content.lookup_ids
|
self.other_lookup_ids = resource_content.lookup_ids
|
||||||
self.metadata = resource_content.metadata
|
self.metadata = resource_content.metadata
|
||||||
if resource_content.cover_image and resource_content.cover_image_extention:
|
if resource_content.cover_image and resource_content.cover_image_extention:
|
||||||
|
@ -662,13 +698,15 @@ class ExternalResource(models.Model):
|
||||||
def ready(self):
|
def ready(self):
|
||||||
return bool(self.metadata and self.scraped_time)
|
return bool(self.metadata and self.scraped_time)
|
||||||
|
|
||||||
def get_all_lookup_ids(self):
|
def get_all_lookup_ids(self) -> dict[str, str]:
|
||||||
d = self.other_lookup_ids.copy()
|
d = self.other_lookup_ids.copy()
|
||||||
d[self.id_type] = self.id_value
|
d[self.id_type] = self.id_value
|
||||||
d = {k: v for k, v in d.items() if bool(v)}
|
d = {k: v for k, v in d.items() if bool(v)}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def get_lookup_ids(self, default_model):
|
def get_lookup_ids(
|
||||||
|
self, default_model: type[Item] | None = None
|
||||||
|
) -> list[tuple[str, str]]:
|
||||||
lookup_ids = self.get_all_lookup_ids()
|
lookup_ids = self.get_all_lookup_ids()
|
||||||
model = self.get_item_model(default_model)
|
model = self.get_item_model(default_model)
|
||||||
bt, bv = model.get_best_lookup_id(lookup_ids)
|
bt, bv = model.get_best_lookup_id(lookup_ids)
|
||||||
|
@ -677,23 +715,30 @@ class ExternalResource(models.Model):
|
||||||
ids = [(bt, bv)] + ids
|
ids = [(bt, bv)] + ids
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
def get_item_model(self, default_model: type[Item]) -> type[Item]:
|
def get_item_model(self, default_model: type[Item] | None) -> type[Item]:
|
||||||
model = self.metadata.get("preferred_model")
|
model = self.metadata.get("preferred_model")
|
||||||
if model:
|
if model:
|
||||||
m = ContentType.objects.filter(
|
m = ContentType.objects.filter(
|
||||||
app_label="catalog", model=model.lower()
|
app_label="catalog", model=model.lower()
|
||||||
).first()
|
).first()
|
||||||
if m:
|
if m:
|
||||||
return cast(Item, m).model_class()
|
mc: type[Item] | None = m.model_class() # type: ignore
|
||||||
|
if not mc:
|
||||||
|
raise ValueError(
|
||||||
|
f"preferred model {model} does not exist in ContentType"
|
||||||
|
)
|
||||||
|
return mc
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"preferred model {model} does not exist")
|
raise ValueError(f"preferred model {model} does not exist")
|
||||||
|
if not default_model:
|
||||||
|
raise ValueError("no default preferred model specified")
|
||||||
return default_model
|
return default_model
|
||||||
|
|
||||||
|
|
||||||
_CONTENT_TYPE_LIST = None
|
_CONTENT_TYPE_LIST = None
|
||||||
|
|
||||||
|
|
||||||
def item_content_types():
|
def item_content_types() -> dict[type[Item], int]:
|
||||||
global _CONTENT_TYPE_LIST
|
global _CONTENT_TYPE_LIST
|
||||||
if _CONTENT_TYPE_LIST is None:
|
if _CONTENT_TYPE_LIST is None:
|
||||||
_CONTENT_TYPE_LIST = {}
|
_CONTENT_TYPE_LIST = {}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Count, F
|
from django.db.models import Count, F
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models import Count, F
|
from django.db.models import Count, F
|
||||||
|
|
||||||
|
@ -111,12 +112,14 @@ class Command(BaseCommand):
|
||||||
else:
|
else:
|
||||||
self.stdout.write(f"! no season {i} : {i.absolute_url}?skipcheck=1")
|
self.stdout.write(f"! no season {i} : {i.absolute_url}?skipcheck=1")
|
||||||
if self.fix:
|
if self.fix:
|
||||||
i.recast_to(i.merged_to_item.__class__)
|
i.recast_to(i.merged_to_item.__class__) # type:ignore
|
||||||
|
|
||||||
self.stdout.write(f"Checking TVSeason is child of other class...")
|
self.stdout.write(f"Checking TVSeason is child of other class...")
|
||||||
for i in TVSeason.objects.filter(show__isnull=False).exclude(
|
for i in TVSeason.objects.filter(show__isnull=False).exclude(
|
||||||
show__polymorphic_ctype_id=tvshow_ct_id
|
show__polymorphic_ctype_id=tvshow_ct_id
|
||||||
):
|
):
|
||||||
|
if not i.show:
|
||||||
|
continue
|
||||||
self.stdout.write(f"! {i.show} : {i.show.absolute_url}?skipcheck=1")
|
self.stdout.write(f"! {i.show} : {i.show.absolute_url}?skipcheck=1")
|
||||||
if self.fix:
|
if self.fix:
|
||||||
i.show = None
|
i.show = None
|
||||||
|
@ -124,6 +127,8 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
self.stdout.write(f"Checking deleted item with child TV Season...")
|
self.stdout.write(f"Checking deleted item with child TV Season...")
|
||||||
for i in TVSeason.objects.filter(show__is_deleted=True):
|
for i in TVSeason.objects.filter(show__is_deleted=True):
|
||||||
|
if not i.show:
|
||||||
|
continue
|
||||||
self.stdout.write(f"! {i.show} : {i.show.absolute_url}?skipcheck=1")
|
self.stdout.write(f"! {i.show} : {i.show.absolute_url}?skipcheck=1")
|
||||||
if self.fix:
|
if self.fix:
|
||||||
i.show.is_deleted = False
|
i.show.is_deleted = False
|
||||||
|
@ -131,6 +136,8 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
self.stdout.write(f"Checking merged item with child TV Season...")
|
self.stdout.write(f"Checking merged item with child TV Season...")
|
||||||
for i in TVSeason.objects.filter(show__merged_to_item__isnull=False):
|
for i in TVSeason.objects.filter(show__merged_to_item__isnull=False):
|
||||||
|
if not i.show:
|
||||||
|
continue
|
||||||
self.stdout.write(f"! {i.show} : {i.show.absolute_url}?skipcheck=1")
|
self.stdout.write(f"! {i.show} : {i.show.absolute_url}?skipcheck=1")
|
||||||
if self.fix:
|
if self.fix:
|
||||||
i.show = i.show.merged_to_item
|
i.show = i.show.merged_to_item
|
||||||
|
|
|
@ -98,3 +98,52 @@ def init_catalog_audit_log():
|
||||||
)
|
)
|
||||||
|
|
||||||
# logger.debug(f"Catalog audit log initialized for {item_content_types().values()}")
|
# logger.debug(f"Catalog audit log initialized for {item_content_types().values()}")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Edition",
|
||||||
|
"EditionInSchema",
|
||||||
|
"EditionSchema",
|
||||||
|
"Series",
|
||||||
|
"Work",
|
||||||
|
"CatalogCollection",
|
||||||
|
"AvailableItemCategory",
|
||||||
|
"ExternalResource",
|
||||||
|
"IdType",
|
||||||
|
"Item",
|
||||||
|
"ItemCategory",
|
||||||
|
"ItemInSchema",
|
||||||
|
"ItemSchema",
|
||||||
|
"ItemType",
|
||||||
|
"SiteName",
|
||||||
|
"item_categories",
|
||||||
|
"item_content_types",
|
||||||
|
"Game",
|
||||||
|
"GameInSchema",
|
||||||
|
"GameSchema",
|
||||||
|
"Movie",
|
||||||
|
"MovieInSchema",
|
||||||
|
"MovieSchema",
|
||||||
|
"Album",
|
||||||
|
"AlbumInSchema",
|
||||||
|
"AlbumSchema",
|
||||||
|
"Performance",
|
||||||
|
"PerformanceProduction",
|
||||||
|
"PerformanceProductionSchema",
|
||||||
|
"PerformanceSchema",
|
||||||
|
"Podcast",
|
||||||
|
"PodcastEpisode",
|
||||||
|
"PodcastInSchema",
|
||||||
|
"PodcastSchema",
|
||||||
|
"TVEpisode",
|
||||||
|
"TVEpisodeSchema",
|
||||||
|
"TVSeason",
|
||||||
|
"TVSeasonInSchema",
|
||||||
|
"TVSeasonSchema",
|
||||||
|
"TVShow",
|
||||||
|
"TVShowInSchema",
|
||||||
|
"TVShowSchema",
|
||||||
|
"Indexer",
|
||||||
|
"init_catalog_search_models",
|
||||||
|
"init_catalog_audit_log",
|
||||||
|
]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -100,6 +101,8 @@ def _crew_by_role(crew):
|
||||||
|
|
||||||
|
|
||||||
class Performance(Item):
|
class Performance(Item):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
productions: models.QuerySet["PerformanceProduction"]
|
||||||
type = ItemType.Performance
|
type = ItemType.Performance
|
||||||
child_class = "PerformanceProduction"
|
child_class = "PerformanceProduction"
|
||||||
category = ItemCategory.Performance
|
category = ItemCategory.Performance
|
||||||
|
@ -371,10 +374,10 @@ class PerformanceProduction(Item):
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent_item(self):
|
def parent_item(self) -> Performance | None: # type:ignore
|
||||||
return self.show
|
return self.show
|
||||||
|
|
||||||
def set_parent_item(self, value):
|
def set_parent_item(self, value: Performance | None): # type:ignore
|
||||||
self.show = value
|
self.show = value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -389,23 +392,23 @@ class PerformanceProduction(Item):
|
||||||
return f"{self.show.title if self.show else '♢'} {self.title}"
|
return f"{self.show.title if self.show else '♢'} {self.title}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cover_image_url(self):
|
def cover_image_url(self) -> str | None:
|
||||||
return (
|
return (
|
||||||
self.cover.url
|
self.cover.url # type:ignore
|
||||||
if self.cover and self.cover != DEFAULT_ITEM_COVER
|
if self.cover and self.cover != DEFAULT_ITEM_COVER
|
||||||
else self.show.cover_image_url
|
else self.show.cover_image_url
|
||||||
if self.show
|
if self.show
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_linked_items_from_external_resource(self, resource):
|
def update_linked_items_from_external_resource(self, resource: ExternalResource):
|
||||||
for r in resource.required_resources:
|
for r in resource.required_resources:
|
||||||
if r["model"] == "Performance":
|
if r["model"] == "Performance":
|
||||||
resource = ExternalResource.objects.filter(
|
res = ExternalResource.objects.filter(
|
||||||
id_type=r["id_type"], id_value=r["id_value"]
|
id_type=r["id_type"], id_value=r["id_value"]
|
||||||
).first()
|
).first()
|
||||||
if resource and resource.item:
|
if res and res.item:
|
||||||
self.show = resource.item
|
self.show = res.item
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def crew_by_role(self):
|
def crew_by_role(self):
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
@ -26,6 +28,8 @@ class PodcastSchema(PodcastInSchema, BaseSchema):
|
||||||
|
|
||||||
|
|
||||||
class Podcast(Item):
|
class Podcast(Item):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
episodes: models.QuerySet["PodcastEpisode"]
|
||||||
category = ItemCategory.Podcast
|
category = ItemCategory.Podcast
|
||||||
child_class = "PodcastEpisode"
|
child_class = "PodcastEpisode"
|
||||||
url_path = "podcast"
|
url_path = "podcast"
|
||||||
|
@ -107,10 +111,10 @@ class PodcastEpisode(Item):
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent_item(self):
|
def parent_item(self) -> Podcast | None: # type:ignore
|
||||||
return self.program
|
return self.program
|
||||||
|
|
||||||
def set_parent_item(self, value):
|
def set_parent_item(self, value: Podcast | None): # type:ignore
|
||||||
self.program = value
|
self.program = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -123,7 +127,7 @@ class PodcastEpisode(Item):
|
||||||
self.program.cover_image_url if self.program else None
|
self.program.cover_image_url if self.program else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_url_with_position(self, position=None):
|
def get_url_with_position(self, position: int | str | None = None):
|
||||||
return (
|
return (
|
||||||
self.url
|
self.url
|
||||||
if position is None or position == ""
|
if position is None or position == ""
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ from datetime import datetime
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
import podcastparser
|
import podcastparser
|
||||||
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
|
|
|
@ -26,9 +26,13 @@ For now, we follow Douban convention, but keep an eye on it in case it breaks it
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from typing import TYPE_CHECKING, overload
|
||||||
|
|
||||||
|
from auditlog.diff import ForeignKey
|
||||||
|
from auditlog.models import QuerySet
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
from catalog.common import (
|
from catalog.common import (
|
||||||
BaseSchema,
|
BaseSchema,
|
||||||
|
@ -90,6 +94,8 @@ class TVEpisodeSchema(ItemSchema):
|
||||||
|
|
||||||
|
|
||||||
class TVShow(Item):
|
class TVShow(Item):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
seasons: QuerySet["TVSeason"]
|
||||||
type = ItemType.TVShow
|
type = ItemType.TVShow
|
||||||
child_class = "TVSeason"
|
child_class = "TVSeason"
|
||||||
category = ItemCategory.TV
|
category = ItemCategory.TV
|
||||||
|
@ -249,6 +255,8 @@ class TVShow(Item):
|
||||||
|
|
||||||
|
|
||||||
class TVSeason(Item):
|
class TVSeason(Item):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
episodes: models.QuerySet["TVEpisode"]
|
||||||
type = ItemType.TVSeason
|
type = ItemType.TVSeason
|
||||||
category = ItemCategory.TV
|
category = ItemCategory.TV
|
||||||
url_path = "tv/season"
|
url_path = "tv/season"
|
||||||
|
@ -424,10 +432,10 @@ class TVSeason(Item):
|
||||||
return self.episodes.all().order_by("episode_number")
|
return self.episodes.all().order_by("episode_number")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent_item(self):
|
def parent_item(self) -> TVShow | None: # type:ignore
|
||||||
return self.show
|
return self.show
|
||||||
|
|
||||||
def set_parent_item(self, value):
|
def set_parent_item(self, value: TVShow | None): # type:ignore
|
||||||
self.show = value
|
self.show = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -462,10 +470,10 @@ class TVEpisode(Item):
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent_item(self):
|
def parent_item(self) -> TVSeason | None: # type:ignore
|
||||||
return self.season
|
return self.season
|
||||||
|
|
||||||
def set_parent_item(self, value):
|
def set_parent_item(self, value: TVSeason | None): # type:ignore
|
||||||
self.season = value
|
self.season = value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -476,7 +484,7 @@ class TVEpisode(Item):
|
||||||
]
|
]
|
||||||
return [(i.value, i.label) for i in id_types]
|
return [(i.value, i.label) for i in id_types]
|
||||||
|
|
||||||
def update_linked_items_from_external_resource(self, resource):
|
def update_linked_items_from_external_resource(self, resource: ExternalResource):
|
||||||
for w in resource.required_resources:
|
for w in resource.required_resources:
|
||||||
if w["model"] == "TVSeason":
|
if w["model"] == "TVSeason":
|
||||||
p = ExternalResource.objects.filter(
|
p = ExternalResource.objects.filter(
|
||||||
|
|
|
@ -129,7 +129,7 @@ def retrieve(request, item_path, item_uuid):
|
||||||
|
|
||||||
|
|
||||||
def episode_data(request, item_uuid):
|
def episode_data(request, item_uuid):
|
||||||
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
item = get_object_or_404(Podcast, uid=get_uuid_or_404(item_uuid))
|
||||||
qs = item.episodes.all().order_by("-pub_date")
|
qs = item.episodes.all().order_by("-pub_date")
|
||||||
if request.GET.get("last"):
|
if request.GET.get("last"):
|
||||||
qs = qs.filter(pub_date__lt=request.GET.get("last"))
|
qs = qs.filter(pub_date__lt=request.GET.get("last"))
|
||||||
|
|
|
@ -224,13 +224,13 @@ def assign_parent(request, item_path, item_uuid):
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def remove_unused_seasons(request, item_path, item_uuid):
|
def remove_unused_seasons(request, item_path, item_uuid):
|
||||||
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
item = get_object_or_404(TVShow, uid=get_uuid_or_404(item_uuid))
|
||||||
sl = list(item.seasons.all())
|
sl = list(item.seasons.all())
|
||||||
for s in sl:
|
for s in sl:
|
||||||
if not s.journal_exists():
|
if not s.journal_exists():
|
||||||
s.delete()
|
s.delete()
|
||||||
ol = [s.id for s in sl]
|
ol = [s.pk for s in sl]
|
||||||
nl = [s.id for s in item.seasons.all()]
|
nl = [s.pk for s in item.seasons.all()]
|
||||||
discord_send(
|
discord_send(
|
||||||
"audit",
|
"audit",
|
||||||
f"{item.absolute_url}\n{ol} ➡ {nl}\nby [@{request.user.username}]({request.user.absolute_url})",
|
f"{item.absolute_url}\n{ol} ➡ {nl}\nby [@{request.user.username}]({request.user.absolute_url})",
|
||||||
|
@ -244,7 +244,7 @@ def remove_unused_seasons(request, item_path, item_uuid):
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def fetch_tvepisodes(request, item_path, item_uuid):
|
def fetch_tvepisodes(request, item_path, item_uuid):
|
||||||
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
item = get_object_or_404(TVSeason, uid=get_uuid_or_404(item_uuid))
|
||||||
if item.class_name != "tvseason" or not item.imdb or item.season_number is None:
|
if item.class_name != "tvseason" or not item.imdb or item.season_number is None:
|
||||||
raise BadRequest(_("TV Season with IMDB id and season number required."))
|
raise BadRequest(_("TV Season with IMDB id and season number required."))
|
||||||
item.log_action({"!fetch_tvepisodes": ["", ""]})
|
item.log_action({"!fetch_tvepisodes": ["", ""]})
|
||||||
|
@ -257,7 +257,7 @@ def fetch_tvepisodes(request, item_path, item_uuid):
|
||||||
|
|
||||||
def fetch_episodes_for_season_task(item_uuid, user):
|
def fetch_episodes_for_season_task(item_uuid, user):
|
||||||
with set_actor(user):
|
with set_actor(user):
|
||||||
season = Item.get_by_url(item_uuid)
|
season = TVSeason.get_by_url(item_uuid)
|
||||||
if not season:
|
if not season:
|
||||||
return
|
return
|
||||||
episodes = season.episode_uuids
|
episodes = season.episode_uuids
|
||||||
|
@ -313,8 +313,8 @@ def merge(request, item_path, item_uuid):
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def link_edition(request, item_path, item_uuid):
|
def link_edition(request, item_path, item_uuid):
|
||||||
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
item = get_object_or_404(Edition, uid=get_uuid_or_404(item_uuid))
|
||||||
new_item = Item.get_by_url(request.POST.get("target_item_url"))
|
new_item = Edition.get_by_url(request.POST.get("target_item_url"))
|
||||||
if (
|
if (
|
||||||
not new_item
|
not new_item
|
||||||
or new_item.is_deleted
|
or new_item.is_deleted
|
||||||
|
@ -325,7 +325,7 @@ def link_edition(request, item_path, item_uuid):
|
||||||
if item.class_name != "edition" or new_item.class_name != "edition":
|
if item.class_name != "edition" or new_item.class_name != "edition":
|
||||||
raise BadRequest(_("Cannot link items other than editions"))
|
raise BadRequest(_("Cannot link items other than editions"))
|
||||||
if request.POST.get("sure", 0) != "1":
|
if request.POST.get("sure", 0) != "1":
|
||||||
new_item = Item.get_by_url(request.POST.get("target_item_url"))
|
new_item = Edition.get_by_url(request.POST.get("target_item_url")) # type: ignore
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"catalog_merge.html",
|
"catalog_merge.html",
|
||||||
|
@ -345,7 +345,7 @@ def link_edition(request, item_path, item_uuid):
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def unlink_works(request, item_path, item_uuid):
|
def unlink_works(request, item_path, item_uuid):
|
||||||
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
item = get_object_or_404(Edition, uid=get_uuid_or_404(item_uuid))
|
||||||
if not request.user.is_staff and item.journal_exists():
|
if not request.user.is_staff and item.journal_exists():
|
||||||
raise PermissionDenied(_("Insufficient permission"))
|
raise PermissionDenied(_("Insufficient permission"))
|
||||||
item.unlink_from_all_works()
|
item.unlink_from_all_works()
|
||||||
|
|
|
@ -47,7 +47,7 @@ class Command(BaseCommand):
|
||||||
{
|
{
|
||||||
"title": member.item.title,
|
"title": member.item.title,
|
||||||
"url": member.item.absolute_url,
|
"url": member.item.absolute_url,
|
||||||
"note": member.note,
|
"note": member.note, # type:ignore
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
print(json.dumps(data, indent=2))
|
print(json.dumps(data, indent=2))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import re
|
import re
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -40,6 +41,8 @@ class CollectionMember(ListMember):
|
||||||
|
|
||||||
|
|
||||||
class Collection(List):
|
class Collection(List):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
members: models.QuerySet[CollectionMember]
|
||||||
url_path = "collection"
|
url_path = "collection"
|
||||||
MEMBER_CLASS = CollectionMember
|
MEMBER_CLASS = CollectionMember
|
||||||
catalog_item = models.OneToOneField(
|
catalog_item = models.OneToOneField(
|
||||||
|
|
|
@ -21,6 +21,8 @@ from .mixins import UserOwnedObjectMixin
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from takahe.models import Post
|
from takahe.models import Post
|
||||||
|
|
||||||
|
from .like import Like
|
||||||
|
|
||||||
|
|
||||||
class VisibilityType(models.IntegerChoices):
|
class VisibilityType(models.IntegerChoices):
|
||||||
Public = 0, _("Public")
|
Public = 0, _("Public")
|
||||||
|
@ -112,6 +114,8 @@ def q_item_in_category(item_category: ItemCategory):
|
||||||
|
|
||||||
|
|
||||||
class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
likes: models.QuerySet["Like"]
|
||||||
url_path = "p" # subclass must specify this
|
url_path = "p" # subclass must specify this
|
||||||
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
||||||
local = models.BooleanField(default=True)
|
local = models.BooleanField(default=True)
|
||||||
|
@ -257,7 +261,7 @@ class Content(Piece):
|
||||||
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
||||||
visibility = models.PositiveSmallIntegerField(
|
visibility = models.PositiveSmallIntegerField(
|
||||||
default=0
|
default=0
|
||||||
) # 0: Public / 1: Follower only / 2: Self only
|
) # 0: Public / 1: Follower only / 2: Self only # type:ignore
|
||||||
created_time = models.DateTimeField(default=timezone.now)
|
created_time = models.DateTimeField(default=timezone.now)
|
||||||
edited_time = models.DateTimeField(auto_now=True)
|
edited_time = models.DateTimeField(auto_now=True)
|
||||||
metadata = models.JSONField(default=dict)
|
metadata = models.JSONField(default=dict)
|
||||||
|
@ -275,7 +279,7 @@ class Debris(Content):
|
||||||
class_name = CharField(max_length=50)
|
class_name = CharField(max_length=50)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from_piece(cls, c: Piece):
|
def create_from_piece(cls, c: Content):
|
||||||
return cls.objects.create(
|
return cls.objects.create(
|
||||||
class_name=c.__class__.__name__,
|
class_name=c.__class__.__name__,
|
||||||
owner=c.owner,
|
owner=c.owner,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import django.dispatch
|
import django.dispatch
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -18,10 +19,14 @@ class List(Piece):
|
||||||
List (abstract model)
|
List (abstract model)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
MEMBER_CLASS: "type[ListMember]"
|
||||||
|
members: "models.QuerySet[ListMember]"
|
||||||
|
items: "models.ManyToManyField[Item, List]"
|
||||||
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
||||||
visibility = models.PositiveSmallIntegerField(
|
visibility = models.PositiveSmallIntegerField(
|
||||||
default=0
|
default=0
|
||||||
) # 0: Public / 1: Follower only / 2: Self only
|
) # 0: Public / 1: Follower only / 2: Self only # type:ignore
|
||||||
created_time = models.DateTimeField(default=timezone.now)
|
created_time = models.DateTimeField(default=timezone.now)
|
||||||
edited_time = models.DateTimeField(auto_now=True)
|
edited_time = models.DateTimeField(auto_now=True)
|
||||||
metadata = models.JSONField(default=dict)
|
metadata = models.JSONField(default=dict)
|
||||||
|
@ -29,7 +34,6 @@ class List(Piece):
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
MEMBER_CLASS: Piece
|
|
||||||
# MEMBER_CLASS = None # subclass must override this
|
# MEMBER_CLASS = None # subclass must override this
|
||||||
# subclass must add this:
|
# subclass must add this:
|
||||||
# items = models.ManyToManyField(Item, through='ListMember')
|
# items = models.ManyToManyField(Item, through='ListMember')
|
||||||
|
@ -76,9 +80,10 @@ class List(Piece):
|
||||||
ml = self.ordered_members
|
ml = self.ordered_members
|
||||||
p = {"parent": self}
|
p = {"parent": self}
|
||||||
p.update(params)
|
p.update(params)
|
||||||
|
lm = ml.last()
|
||||||
member = self.MEMBER_CLASS.objects.create(
|
member = self.MEMBER_CLASS.objects.create(
|
||||||
owner=self.owner,
|
owner=self.owner,
|
||||||
position=ml.last().position + 1 if ml.count() else 1,
|
position=lm.position + 1 if lm else 1,
|
||||||
item=item,
|
item=item,
|
||||||
**p,
|
**p,
|
||||||
)
|
)
|
||||||
|
@ -96,7 +101,7 @@ class List(Piece):
|
||||||
def update_member_order(self, ordered_member_ids):
|
def update_member_order(self, ordered_member_ids):
|
||||||
for m in self.members.all():
|
for m in self.members.all():
|
||||||
try:
|
try:
|
||||||
i = ordered_member_ids.index(m.id)
|
i = ordered_member_ids.index(m.pk)
|
||||||
if m.position != i + 1:
|
if m.position != i + 1:
|
||||||
m.position = i + 1
|
m.position = i + 1
|
||||||
m.save()
|
m.save()
|
||||||
|
@ -142,10 +147,12 @@ class ListMember(Piece):
|
||||||
parent = models.ForeignKey('List', related_name='members', on_delete=models.CASCADE)
|
parent = models.ForeignKey('List', related_name='members', on_delete=models.CASCADE)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
parent: models.ForeignKey["ListMember", "List"]
|
||||||
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
||||||
visibility = models.PositiveSmallIntegerField(
|
visibility = models.PositiveSmallIntegerField(
|
||||||
default=0
|
default=0
|
||||||
) # 0: Public / 1: Follower only / 2: Self only
|
) # 0: Public / 1: Follower only / 2: Self only # type:ignore[reportAssignmentType]
|
||||||
created_time = models.DateTimeField(default=timezone.now)
|
created_time = models.DateTimeField(default=timezone.now)
|
||||||
edited_time = models.DateTimeField(auto_now=True)
|
edited_time = models.DateTimeField(auto_now=True)
|
||||||
metadata = models.JSONField(default=dict)
|
metadata = models.JSONField(default=dict)
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Like(Piece): # TODO remove
|
||||||
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
||||||
visibility = models.PositiveSmallIntegerField(
|
visibility = models.PositiveSmallIntegerField(
|
||||||
default=0
|
default=0
|
||||||
) # 0: Public / 1: Follower only / 2: Self only
|
) # 0: Public / 1: Follower only / 2: Self only # type: ignore
|
||||||
created_time = models.DateTimeField(default=timezone.now)
|
created_time = models.DateTimeField(default=timezone.now)
|
||||||
edited_time = models.DateTimeField(auto_now=True)
|
edited_time = models.DateTimeField(auto_now=True)
|
||||||
target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name="likes")
|
target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name="likes")
|
||||||
|
|
|
@ -42,7 +42,7 @@ class Mark:
|
||||||
self.item = item
|
self.item = item
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def shelfmember(self) -> ShelfMember:
|
def shelfmember(self) -> ShelfMember | None:
|
||||||
return self.owner.shelf_manager.locate_item(self.item)
|
return self.owner.shelf_manager.locate_item(self.item)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -198,7 +198,7 @@ class Mark:
|
||||||
last_visibility = self.visibility if last_shelf_type else None
|
last_visibility = self.visibility if last_shelf_type else None
|
||||||
if shelf_type is None: # TODO change this use case to DEFERRED status
|
if shelf_type is None: # TODO change this use case to DEFERRED status
|
||||||
# take item off shelf
|
# take item off shelf
|
||||||
if last_shelf_type:
|
if self.shelfmember:
|
||||||
Takahe.delete_posts(self.shelfmember.all_post_ids)
|
Takahe.delete_posts(self.shelfmember.all_post_ids)
|
||||||
self.shelfmember.log_and_delete()
|
self.shelfmember.log_and_delete()
|
||||||
if self.comment:
|
if self.comment:
|
||||||
|
@ -207,7 +207,7 @@ class Mark:
|
||||||
self.rating.delete()
|
self.rating.delete()
|
||||||
return
|
return
|
||||||
# create/update shelf member and shelf log if necessary
|
# create/update shelf member and shelf log if necessary
|
||||||
if last_shelf_type == shelf_type:
|
if self.shelfmember and last_shelf_type == shelf_type:
|
||||||
shelfmember_changed = False
|
shelfmember_changed = False
|
||||||
log_entry = self.shelfmember.ensure_log_entry()
|
log_entry = self.shelfmember.ensure_log_entry()
|
||||||
if metadata is not None and metadata != self.shelfmember.metadata:
|
if metadata is not None and metadata != self.shelfmember.metadata:
|
||||||
|
|
|
@ -18,7 +18,9 @@ class UserOwnedObjectMixin:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
owner: ForeignKey[APIdentity, Piece]
|
owner: ForeignKey[Piece, APIdentity]
|
||||||
|
# owner: ForeignKey[APIdentity, Piece]
|
||||||
|
owner_id: int
|
||||||
visibility: int
|
visibility: int
|
||||||
|
|
||||||
def is_visible_to(
|
def is_visible_to(
|
||||||
|
|
|
@ -272,6 +272,9 @@ _SHELF_LABELS = [
|
||||||
|
|
||||||
|
|
||||||
class ShelfMember(ListMember):
|
class ShelfMember(ListMember):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
parent: models.ForeignKey["ShelfMember", "Shelf"]
|
||||||
|
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
"Shelf", related_name="members", on_delete=models.CASCADE
|
"Shelf", related_name="members", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
@ -375,6 +378,9 @@ class Shelf(List):
|
||||||
Shelf
|
Shelf
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
members: models.QuerySet[ShelfMember]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [["owner", "shelf_type"]]
|
unique_together = [["owner", "shelf_type"]]
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import re
|
import re
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
||||||
from django.db import connection, models
|
from django.db import connection, models
|
||||||
|
@ -14,6 +15,8 @@ from .itemlist import List, ListMember
|
||||||
|
|
||||||
|
|
||||||
class TagMember(ListMember):
|
class TagMember(ListMember):
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
parent: models.ForeignKey["TagMember", "Tag"]
|
||||||
parent = models.ForeignKey("Tag", related_name="members", on_delete=models.CASCADE)
|
parent = models.ForeignKey("Tag", related_name="members", on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -247,7 +247,7 @@ def collection_update_item_note(request: AuthedHttpRequest, collection_uuid, ite
|
||||||
)
|
)
|
||||||
return collection_retrieve_items(request, collection_uuid, True)
|
return collection_retrieve_items(request, collection_uuid, True)
|
||||||
else:
|
else:
|
||||||
member = collection.get_member_for_item(item)
|
member: CollectionMember = collection.get_member_for_item(item) # type:ignore
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"collection_update_item_note.html",
|
"collection_update_item_note.html",
|
||||||
|
|
|
@ -99,7 +99,7 @@ def render_list(
|
||||||
if year:
|
if year:
|
||||||
year = int(year)
|
year = int(year)
|
||||||
queryset = queryset.filter(created_time__year=year)
|
queryset = queryset.filter(created_time__year=year)
|
||||||
paginator = Paginator(queryset, PAGE_SIZE)
|
paginator = Paginator(queryset, PAGE_SIZE) # type:ignore
|
||||||
page_number = int(request.GET.get("page", default=1))
|
page_number = int(request.GET.get("page", default=1))
|
||||||
members = paginator.get_page(page_number)
|
members = paginator.get_page(page_number)
|
||||||
pagination = PageLinksGenerator(page_number, paginator.num_pages, request.GET)
|
pagination = PageLinksGenerator(page_number, paginator.num_pages, request.GET)
|
||||||
|
|
|
@ -291,16 +291,17 @@ def get_related_acct_list(site, token, api):
|
||||||
)
|
)
|
||||||
url = None
|
url = None
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
|
r: list[dict[str, str]] = response.json()
|
||||||
results.extend(
|
results.extend(
|
||||||
map(
|
map(
|
||||||
lambda u: (
|
lambda u: ( # type: ignore
|
||||||
u["acct"]
|
u["acct"]
|
||||||
if u["acct"].find("@") != -1
|
if u["acct"].find("@") != -1
|
||||||
else u["acct"] + "@" + site
|
else u["acct"] + "@" + site
|
||||||
)
|
)
|
||||||
if "acct" in u
|
if "acct" in u
|
||||||
else u,
|
else u,
|
||||||
response.json(),
|
r,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if "Link" in response.headers:
|
if "Link" in response.headers:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
exclude = [ "media", ".venv", ".git", "playground", "catalog/*/tests.py", "neodb", "**/migrations", "**/sites/douban_*", "neodb-takahe" ]
|
exclude = [ "media", ".venv", ".git", "playground", "catalog/*/tests.py", "journal/tests.py", "neodb", "**/migrations", "**/sites/douban_*", "neodb-takahe" ]
|
||||||
reportIncompatibleVariableOverride = false
|
reportIncompatibleVariableOverride = false
|
||||||
reportUnusedImport = false
|
reportUnusedImport = false
|
||||||
reportUnknownVariableType = false
|
reportUnknownVariableType = false
|
||||||
|
@ -10,6 +10,8 @@ reportUnknownArgumentType = false
|
||||||
reportAny = false
|
reportAny = false
|
||||||
reportImplicitOverride = false
|
reportImplicitOverride = false
|
||||||
reportUninitializedInstanceVariable = false
|
reportUninitializedInstanceVariable = false
|
||||||
|
reportMissingTypeStubs = false
|
||||||
|
reportIgnoreCommentWithoutRule = false
|
||||||
|
|
||||||
[tool.djlint]
|
[tool.djlint]
|
||||||
ignore="T002,T003,H005,H006,H019,H020,H021,H023,H030,H031,D018"
|
ignore="T002,T003,H005,H006,H019,H020,H021,H023,H030,H031,D018"
|
||||||
|
|
|
@ -869,8 +869,9 @@ class Post(models.Model):
|
||||||
A post (status, toot) that is either local or remote.
|
A post (status, toot) that is either local or remote.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
interactions: "models.QuerySet[PostInteraction]"
|
if TYPE_CHECKING:
|
||||||
attachments: "models.QuerySet[PostAttachment]"
|
interactions: "models.QuerySet[PostInteraction]"
|
||||||
|
attachments: "models.QuerySet[PostAttachment]"
|
||||||
|
|
||||||
class Visibilities(models.IntegerChoices):
|
class Visibilities(models.IntegerChoices):
|
||||||
public = 0
|
public = 0
|
||||||
|
|
|
@ -533,7 +533,8 @@ class Takahe:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def post_collection(collection: "Collection"):
|
def post_collection(collection: "Collection"):
|
||||||
existing_post = collection.latest_post
|
existing_post = collection.latest_post
|
||||||
user = collection.owner.user
|
owner: APIdentity = collection.owner
|
||||||
|
user = owner.user
|
||||||
if not user:
|
if not user:
|
||||||
raise ValueError(f"Cannot find user for collection {collection}")
|
raise ValueError(f"Cannot find user for collection {collection}")
|
||||||
visibility = Takahe.visibility_n2t(
|
visibility = Takahe.visibility_n2t(
|
||||||
|
|
Loading…
Add table
Reference in a new issue