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): class Edition(Item):
category = ItemCategory.Book category = ItemCategory.Book
url_path = 'book'
isbn = PrimaryLookupIdDescriptor(IdType.ISBN) isbn = PrimaryLookupIdDescriptor(IdType.ISBN)
asin = PrimaryLookupIdDescriptor(IdType.ASIN) asin = PrimaryLookupIdDescriptor(IdType.ASIN)
cubn = PrimaryLookupIdDescriptor(IdType.CUBN) cubn = PrimaryLookupIdDescriptor(IdType.CUBN)
@ -60,6 +61,7 @@ class Edition(Item):
class Work(Item): class Work(Item):
category = ItemCategory.Book category = ItemCategory.Book
url_path = 'book/work'
douban_work = PrimaryLookupIdDescriptor(IdType.DoubanBook_Work) douban_work = PrimaryLookupIdDescriptor(IdType.DoubanBook_Work)
goodreads_work = PrimaryLookupIdDescriptor(IdType.Goodreads_Work) goodreads_work = PrimaryLookupIdDescriptor(IdType.Goodreads_Work)
editions = models.ManyToManyField(Edition, related_name='works') editions = models.ManyToManyField(Edition, related_name='works')
@ -67,6 +69,7 @@ class Work(Item):
class Series(Item): class Series(Item):
category = ItemCategory.Book category = ItemCategory.Book
url_path = 'book/series'
# douban_serie = LookupIdDescriptor(IdType.DoubanBook_Serie) # douban_serie = LookupIdDescriptor(IdType.DoubanBook_Serie)
# goodreads_serie = LookupIdDescriptor(IdType.Goodreads_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', _('网文') FanFic = 'fanfic', _('网文')
Performance = 'performance', _('演出') Performance = 'performance', _('演出')
Exhibition = 'exhibition', _('展览') Exhibition = 'exhibition', _('展览')
Collection = 'collection', _('收藏单')
class ItemCategory(models.TextChoices): class ItemCategory(models.TextChoices):
@ -79,6 +80,7 @@ class ItemCategory(models.TextChoices):
FanFic = 'fanfic', _('网文') FanFic = 'fanfic', _('网文')
Performance = 'performance', _('演出') Performance = 'performance', _('演出')
Exhibition = 'exhibition', _('展览') Exhibition = 'exhibition', _('展览')
Collection = 'collection', _('收藏单')
class SubItemType(models.TextChoices): class SubItemType(models.TextChoices):

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
class Performance(Item): class Performance(Item):
category = ItemCategory.Performance category = ItemCategory.Performance
url_path = 'performance'
douban_drama = LookupIdDescriptor(IdType.DoubanDrama) douban_drama = LookupIdDescriptor(IdType.DoubanDrama)
versions = jsondata.ArrayField(_('版本'), null=False, blank=False, default=list) versions = jsondata.ArrayField(_('版本'), null=False, blank=False, default=list)
directors = 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): class Podcast(Item):
category = ItemCategory.Podcast category = ItemCategory.Podcast
url_path = 'podcast'
feed_url = PrimaryLookupIdDescriptor(IdType.Feed) feed_url = PrimaryLookupIdDescriptor(IdType.Feed)
apple_podcast = PrimaryLookupIdDescriptor(IdType.ApplePodcast) apple_podcast = PrimaryLookupIdDescriptor(IdType.ApplePodcast)
# ximalaya = LookupIdDescriptor(IdType.Ximalaya) # ximalaya = LookupIdDescriptor(IdType.Ximalaya)

View file

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

View file

@ -2,6 +2,7 @@ from django.db import models
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from users.models import User from users.models import User
from catalog.common.models import Item, ItemCategory from catalog.common.models import Item, ItemCategory
from catalog.collection.models import Collection as CatalogCollection
from decimal import * from decimal import *
from enum import Enum from enum import Enum
from markdownx.models import MarkdownxField 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.utils.translation import gettext_lazy as _
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from functools import cached_property from functools import cached_property
from django.db.models import Count
class UserOwnedEntity(PolymorphicModel): class Piece(PolymorphicModel):
class Meta: owner = models.ForeignKey(User, on_delete=models.PROTECT)
abstract = True
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='%(class)ss')
visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only
metadata = models.JSONField(default=dict) metadata = models.JSONField(default=dict)
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)
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): def is_visible_to(self, viewer):
if not viewer.is_authenticated: if not viewer.is_authenticated:
@ -52,7 +63,7 @@ class UserOwnedEntity(PolymorphicModel):
return visible_entities return visible_entities
class Content(UserOwnedEntity): class Content(Piece):
target: models.ForeignKey(Item, on_delete=models.PROTECT) target: models.ForeignKey(Item, on_delete=models.PROTECT)
def __str__(self): def __str__(self):
@ -71,7 +82,7 @@ class Review(Content):
class Rating(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): class Reply(Content):
@ -86,10 +97,16 @@ List (abstract class)
""" """
class List(UserOwnedEntity): class List(Piece):
class Meta: class Meta:
abstract = True 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 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')
@ -100,7 +117,7 @@ class List(UserOwnedEntity):
@property @property
def ordered_items(self): 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): def has_item(self, item):
return self.members.filter(item=item).count() > 0 return self.members.filter(item=item).count() > 0
@ -151,7 +168,7 @@ class ListMember(models.Model):
item = models.ForeignKey(Item, on_delete=models.PROTECT) item = models.ForeignKey(Item, on_delete=models.PROTECT)
position = models.PositiveIntegerField() position = models.PositiveIntegerField()
metadata = models.JSONField(default=dict) 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: class Meta:
abstract = True abstract = True
@ -195,10 +212,10 @@ class QueueMember(ListMember):
class Queue(List): class Queue(List):
class Meta: class Meta:
unique_together = [['owner', 'item_category', 'queue_type']] unique_together = [['_owner', 'item_category', 'queue_type']]
MEMBER_CLASS = QueueMember 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) 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) 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): 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 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) item = models.ForeignKey(Item, on_delete=models.PROTECT)
metadata = models.JSONField(default=dict) metadata = models.JSONField(default=dict)
@ -241,16 +258,17 @@ class QueueManager:
def initialize(self): def initialize(self):
for ic in ItemCategory: for ic in ItemCategory:
for qt in QueueType: if ic != ItemCategory.Collection:
Queue.objects.create(owner=self.owner, item_category=ic, queue_type=qt) for qt in QueueType:
Queue.objects.create(owner=self.owner, item_category=ic, queue_type=qt)
def _queue_member_for_item(self, item): 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): def _queue_for_item_and_type(item, queue_type):
if not item or not queue_type: if not item or not queue_type:
return None 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): def update_for_item(self, item, queue_type, metadata=None):
# None means no change for metadata, comment # None means no change for metadata, comment
@ -281,7 +299,7 @@ class QueueManager:
return QueueLogEntry.objects.filter(owner=self.owner, item=item) return QueueLogEntry.objects.filter(owner=self.owner, item=item)
def get_queue(self, item_category, queue_type): 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) collection = models.ForeignKey('Collection', related_name='members', on_delete=models.CASCADE)
class Collection(Item, List): class Collection(List):
MEMBER_CLASS = CollectionMember 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") 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 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) html = markdown(self.description)
return RE_HTML_TAG.sub(' ', html) 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 Tag
@ -317,10 +347,37 @@ TagValidators = [RegexValidator(regex=r'\s+', inverse_match=True)]
class Tag(List): 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) title = models.CharField(max_length=100, null=False, blank=False, validators=TagValidators)
# TODO case convert and space removal on save # TODO case convert and space removal on save
# TODO check on save # TODO check on save
class Meta: 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): def test_collection(self):
collection = Collection.objects.create(title="test", owner=self.user) 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.book1)
collection.append_item(self.book2) collection.append_item(self.book2)
self.assertEqual(list(collection.ordered_items), [self.book1, 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") user = User.objects.create(mastodon_site="site", username="name")
queue_manager = QueueManager(user=user) queue_manager = QueueManager(user=user)
queue_manager.initialize() queue_manager.initialize()
self.assertEqual(user.queues.all().count(), 30) self.assertEqual(user.queue_set.all().count(), 30)
book1 = Edition.objects.create(title="Hyperion") book1 = Edition.objects.create(title="Hyperion")
book2 = Edition.objects.create(title="Andymion") book2 = Edition.objects.create(title="Andymion")
q1 = queue_manager.get_queue(ItemCategory.Book, QueueType.WISHED) 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}) queue_manager.update_for_item(book1, QueueType.STARTED, metadata={'progress': 100})
log = queue_manager.get_log_for_item(book1) log = queue_manager.get_log_for_item(book1)
self.assertEqual(log.count(), 5) 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])