new data model: more tests

This commit is contained in:
Your Name 2022-12-12 16:46:37 +00:00
parent 727254f3ce
commit 3511ac16a0
12 changed files with 133 additions and 22 deletions

View file

@ -25,6 +25,7 @@ from .utils import *
class Edition(Item):
category = ItemCategory.Book
url_path = 'book'
isbn = PrimaryLookupIdDescriptor(IdType.ISBN)
asin = PrimaryLookupIdDescriptor(IdType.ASIN)
cubn = PrimaryLookupIdDescriptor(IdType.CUBN)
@ -60,6 +61,7 @@ class Edition(Item):
class Work(Item):
category = ItemCategory.Book
url_path = 'book/work'
douban_work = PrimaryLookupIdDescriptor(IdType.DoubanBook_Work)
goodreads_work = PrimaryLookupIdDescriptor(IdType.Goodreads_Work)
editions = models.ManyToManyField(Edition, related_name='works')
@ -67,6 +69,7 @@ class Work(Item):
class Series(Item):
category = ItemCategory.Book
url_path = 'book/series'
# douban_serie = LookupIdDescriptor(IdType.DoubanBook_Serie)
# goodreads_serie = LookupIdDescriptor(IdType.Goodreads_Serie)

View file

@ -0,0 +1,6 @@
from catalog.common import *
class Collection(Item):
category = ItemCategory.Collection
url_path = 'collection'

View file

@ -66,6 +66,7 @@ class ItemType(models.TextChoices):
FanFic = 'fanfic', _('网文')
Performance = 'performance', _('演出')
Exhibition = 'exhibition', _('展览')
Collection = 'collection', _('收藏单')
class ItemCategory(models.TextChoices):
@ -79,6 +80,7 @@ class ItemCategory(models.TextChoices):
FanFic = 'fanfic', _('网文')
Performance = 'performance', _('演出')
Exhibition = 'exhibition', _('展览')
Collection = 'collection', _('收藏单')
class SubItemType(models.TextChoices):

View file

@ -3,6 +3,7 @@ from catalog.common import *
class Game(Item):
category = ItemCategory.Game
url_path = 'game'
igdb = PrimaryLookupIdDescriptor(IdType.IGDB)
steam = PrimaryLookupIdDescriptor(IdType.Steam)
douban_game = PrimaryLookupIdDescriptor(IdType.DoubanGame)

View file

@ -5,6 +5,7 @@ from .music.models import Album
from .game.models import Game
from .podcast.models import Podcast
from .performance.models import Performance
from .collection.models import Collection as CatalogCollection
# class Exhibition(Item):

View file

@ -3,6 +3,7 @@ from catalog.common import *
class Movie(Item):
category = ItemCategory.Movie
url_path = 'movie'
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_movie = PrimaryLookupIdDescriptor(IdType.TMDB_Movie)
douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie)

View file

@ -2,6 +2,7 @@ from catalog.common import *
class Album(Item):
url_path = 'album'
category = ItemCategory.Music
barcode = PrimaryLookupIdDescriptor(IdType.GTIN)
douban_music = PrimaryLookupIdDescriptor(IdType.DoubanMusic)

View file

@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
class Performance(Item):
category = ItemCategory.Performance
url_path = 'performance'
douban_drama = LookupIdDescriptor(IdType.DoubanDrama)
versions = jsondata.ArrayField(_('版本'), null=False, blank=False, default=list)
directors = jsondata.ArrayField(_('导演'), null=False, blank=False, default=list)

View file

@ -3,6 +3,7 @@ from catalog.common import *
class Podcast(Item):
category = ItemCategory.Podcast
url_path = 'podcast'
feed_url = PrimaryLookupIdDescriptor(IdType.Feed)
apple_podcast = PrimaryLookupIdDescriptor(IdType.ApplePodcast)
# ximalaya = LookupIdDescriptor(IdType.Ximalaya)

View file

@ -30,6 +30,7 @@ from django.db import models
class TVShow(Item):
category = ItemCategory.TV
url_path = 'tv'
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_tv = PrimaryLookupIdDescriptor(IdType.TMDB_TV)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
@ -38,6 +39,7 @@ class TVShow(Item):
class TVSeason(Item):
category = ItemCategory.TV
url_path = 'tv/season'
douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_tvseason = PrimaryLookupIdDescriptor(IdType.TMDB_TVSeason)
@ -58,6 +60,7 @@ class TVSeason(Item):
class TVEpisode(Item):
category = ItemCategory.TV
url_path = 'tv/episode'
show = models.ForeignKey(TVShow, null=True, on_delete=models.SET_NULL, related_name='episodes')
season = models.ForeignKey(TVSeason, null=True, on_delete=models.SET_NULL, related_name='episodes')
episode_number = models.PositiveIntegerField()

View file

@ -2,6 +2,7 @@ from django.db import models
from polymorphic.models import PolymorphicModel
from users.models import User
from catalog.common.models import Item, ItemCategory
from catalog.collection.models import Collection as CatalogCollection
from decimal import *
from enum import Enum
from markdownx.models import MarkdownxField
@ -11,17 +12,27 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.utils.translation import gettext_lazy as _
from django.core.validators import RegexValidator
from functools import cached_property
from django.db.models import Count
class UserOwnedEntity(PolymorphicModel):
class Meta:
abstract = True
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='%(class)ss')
class Piece(PolymorphicModel):
owner = models.ForeignKey(User, on_delete=models.PROTECT)
visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only
metadata = models.JSONField(default=dict)
created_time = models.DateTimeField(auto_now_add=True)
edited_time = models.DateTimeField(auto_now=True)
is_deleted = models.BooleanField(default=False, db_index=True)
def clear(self):
pass
def delete(self, using=None, soft=True, *args, **kwargs):
if soft:
self.clear()
self.is_deleted = True
self.save(using=using)
else:
return super().delete(using=using, *args, **kwargs)
def is_visible_to(self, viewer):
if not viewer.is_authenticated:
@ -52,7 +63,7 @@ class UserOwnedEntity(PolymorphicModel):
return visible_entities
class Content(UserOwnedEntity):
class Content(Piece):
target: models.ForeignKey(Item, on_delete=models.PROTECT)
def __str__(self):
@ -71,7 +82,7 @@ class Review(Content):
class Rating(Content):
grade = models.IntegerField(default=1, validators=[MaxValueValidator(10), MinValueValidator(0)])
grade = models.IntegerField(default=0, validators=[MaxValueValidator(10), MinValueValidator(0)])
class Reply(Content):
@ -86,10 +97,16 @@ List (abstract class)
"""
class List(UserOwnedEntity):
class List(Piece):
class Meta:
abstract = True
_owner = models.ForeignKey(User, on_delete=models.PROTECT) # duplicated owner field to make unique key possible for subclasses
def save(self, *args, **kwargs):
self._owner = self.owner
super().save(*args, **kwargs)
MEMBER_CLASS = None # subclass must override this
# subclass must add this:
# items = models.ManyToManyField(Item, through='ListMember')
@ -100,7 +117,7 @@ class List(UserOwnedEntity):
@property
def ordered_items(self):
return self.items.all().order_by('collectionmember__position')
return self.items.all().order_by(self.MEMBER_CLASS.__name__.lower() + '__position')
def has_item(self, item):
return self.members.filter(item=item).count() > 0
@ -151,7 +168,7 @@ class ListMember(models.Model):
item = models.ForeignKey(Item, on_delete=models.PROTECT)
position = models.PositiveIntegerField()
metadata = models.JSONField(default=dict)
comment = models.ForeignKey(Review, on_delete=models.SET_NULL, null=True)
comment = models.ForeignKey(Review, on_delete=models.PROTECT, null=True)
class Meta:
abstract = True
@ -195,10 +212,10 @@ class QueueMember(ListMember):
class Queue(List):
class Meta:
unique_together = [['owner', 'item_category', 'queue_type']]
unique_together = [['_owner', 'item_category', 'queue_type']]
MEMBER_CLASS = QueueMember
items = models.ManyToManyField(Item, through='QueueMember', related_name=None)
items = models.ManyToManyField(Item, through='QueueMember', related_name="+")
item_category = models.CharField(choices=ItemCategory.choices, max_length=100, null=False, blank=False)
queue_type = models.CharField(choices=QueueType.choices, max_length=100, null=False, blank=False)
@ -216,7 +233,7 @@ class Queue(List):
class QueueLogEntry(models.Model):
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='%(class)ss')
owner = models.ForeignKey(User, on_delete=models.PROTECT)
queue = models.ForeignKey(Queue, on_delete=models.PROTECT, related_name='entries', null=True) # None means removed from any queue
item = models.ForeignKey(Item, on_delete=models.PROTECT)
metadata = models.JSONField(default=dict)
@ -241,16 +258,17 @@ class QueueManager:
def initialize(self):
for ic in ItemCategory:
for qt in QueueType:
Queue.objects.create(owner=self.owner, item_category=ic, queue_type=qt)
if ic != ItemCategory.Collection:
for qt in QueueType:
Queue.objects.create(owner=self.owner, item_category=ic, queue_type=qt)
def _queue_member_for_item(self, item):
return QueueMember.objects.filter(item=item, queue__in=self.owner.queues.all()).first()
return QueueMember.objects.filter(item=item, queue__in=self.owner.queue_set.all()).first()
def _queue_for_item_and_type(item, queue_type):
if not item or not queue_type:
return None
return self.owner.queues.all().filter(item_category=item.category, queue_type=queue_type)
return self.owner.queue_set.all().filter(item_category=item.category, queue_type=queue_type)
def update_for_item(self, item, queue_type, metadata=None):
# None means no change for metadata, comment
@ -281,7 +299,7 @@ class QueueManager:
return QueueLogEntry.objects.filter(owner=self.owner, item=item)
def get_queue(self, item_category, queue_type):
return self.owner.queues.all().filter(item_category=item_category, queue_type=queue_type).first()
return self.owner.queue_set.all().filter(item_category=item_category, queue_type=queue_type).first()
"""
@ -293,8 +311,11 @@ class CollectionMember(ListMember):
collection = models.ForeignKey('Collection', related_name='members', on_delete=models.CASCADE)
class Collection(Item, List):
class Collection(List):
MEMBER_CLASS = CollectionMember
catalog_item = models.OneToOneField(CatalogCollection, on_delete=models.PROTECT)
title = models.CharField(_("title in primary language"), max_length=1000, default="")
brief = models.TextField(_("简介"), blank=True, default="")
items = models.ManyToManyField(Item, through='CollectionMember', related_name="collections")
collaborative = models.PositiveSmallIntegerField(default=0) # 0: Editable by owner only / 1: Editable by bi-direction followers
@ -303,6 +324,15 @@ class Collection(Item, List):
html = markdown(self.description)
return RE_HTML_TAG.sub(' ', html)
def save(self, *args, **kwargs):
if getattr(self, 'catalog_item', None) is None:
self.catalog_item = CatalogCollection()
if self.catalog_item.title != self.title or self.catalog_item.brief != self.brief:
self.catalog_item.title = self.title
self.catalog_item.brief = self.brief
self.catalog_item.save()
super().save(*args, **kwargs)
"""
Tag
@ -317,10 +347,37 @@ TagValidators = [RegexValidator(regex=r'\s+', inverse_match=True)]
class Tag(List):
MEMBER_CLASS = CollectionMember
MEMBER_CLASS = TagMember
items = models.ManyToManyField(Item, through='TagMember')
title = models.CharField(max_length=100, null=False, blank=False, validators=TagValidators)
# TODO case convert and space removal on save
# TODO check on save
class Meta:
unique_together = [['owner', 'title']]
unique_together = [['_owner', 'title']]
@staticmethod
def cleanup_title(title):
return title.strip().lower()
@staticmethod
def public_tags_for_item(item):
tags = item.tag_set.all().filter(visibility=0).values('title').annotate(frequency=Count('owner')).order_by('-frequency')
return list(map(lambda t: t['title'], tags))
@staticmethod
def all_tags_for_user(user):
tags = user.tag_set.all().values('title').annotate(frequency=Count('members')).order_by('-frequency')
return list(map(lambda t: t['title'], tags))
@staticmethod
def add_tag_by_user(item, tag_title, user, default_visibility=0):
title = Tag.cleanup_title(tag_title)
tag = Tag.objects.filter(owner=user, title=title).first()
if not tag:
tag = Tag.objects.create(owner=user, title=title, visibility=default_visibility)
tag.append_item(item)
Item.tags = property(Tag.public_tags_for_item)
User.tags = property(Tag.all_tags_for_user)

View file

@ -13,6 +13,8 @@ class CollectionTest(TestCase):
def test_collection(self):
collection = Collection.objects.create(title="test", owner=self.user)
collection = Collection.objects.filter(title="test", owner=self.user).first()
self.assertEqual(collection.catalog_item.title, "test")
collection.append_item(self.book1)
collection.append_item(self.book2)
self.assertEqual(list(collection.ordered_items), [self.book1, self.book2])
@ -30,7 +32,7 @@ class QueueTest(TestCase):
user = User.objects.create(mastodon_site="site", username="name")
queue_manager = QueueManager(user=user)
queue_manager.initialize()
self.assertEqual(user.queues.all().count(), 30)
self.assertEqual(user.queue_set.all().count(), 30)
book1 = Edition.objects.create(title="Hyperion")
book2 = Edition.objects.create(title="Andymion")
q1 = queue_manager.get_queue(ItemCategory.Book, QueueType.WISHED)
@ -63,3 +65,35 @@ class QueueTest(TestCase):
queue_manager.update_for_item(book1, QueueType.STARTED, metadata={'progress': 100})
log = queue_manager.get_log_for_item(book1)
self.assertEqual(log.count(), 5)
class TagTest(TestCase):
def setUp(self):
self.book1 = Edition.objects.create(title="Hyperion")
self.book2 = Edition.objects.create(title="Andymion")
self.movie1 = Edition.objects.create(title="Hyperion, The Movie")
self.user1 = User.objects.create(mastodon_site="site", username="name")
self.user2 = User.objects.create(mastodon_site="site2", username="name2")
self.user3 = User.objects.create(mastodon_site="site2", username="name3")
pass
def test_tag(self):
t1 = 'sci-fi'
t2 = 'private'
t3 = 'public'
Tag.add_tag_by_user(self.book1, t3, self.user2)
Tag.add_tag_by_user(self.book1, t1, self.user1)
Tag.add_tag_by_user(self.book1, t1, self.user2)
Tag.add_tag_by_user(self.book1, t2, self.user1, default_visibility=2)
self.assertEqual(self.book1.tags, [t1, t3])
Tag.add_tag_by_user(self.book1, t3, self.user1)
Tag.add_tag_by_user(self.book1, t3, self.user3)
self.assertEqual(self.book1.tags, [t3, t1])
Tag.add_tag_by_user(self.book1, t3, self.user3)
Tag.add_tag_by_user(self.book1, t3, self.user3)
self.assertEqual(Tag.objects.count(), 6)
Tag.add_tag_by_user(self.book2, t1, self.user2)
self.assertEqual(self.user2.tags, [t1, t3])
Tag.add_tag_by_user(self.book2, t3, self.user2)
Tag.add_tag_by_user(self.movie1, t3, self.user2)
self.assertEqual(self.user2.tags, [t3, t1])