new data model: more tests
This commit is contained in:
parent
727254f3ce
commit
3511ac16a0
12 changed files with 133 additions and 22 deletions
|
@ -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)
|
||||
|
||||
|
|
6
catalog/collection/models.py
Normal file
6
catalog/collection/models.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from catalog.common import *
|
||||
|
||||
|
||||
class Collection(Item):
|
||||
category = ItemCategory.Collection
|
||||
url_path = 'collection'
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
Loading…
Add table
Reference in a new issue