new data model: journal models
This commit is contained in:
parent
8a668eca1f
commit
727254f3ce
16 changed files with 474 additions and 13 deletions
|
@ -24,6 +24,7 @@ from .utils import *
|
|||
|
||||
|
||||
class Edition(Item):
|
||||
category = ItemCategory.Book
|
||||
isbn = PrimaryLookupIdDescriptor(IdType.ISBN)
|
||||
asin = PrimaryLookupIdDescriptor(IdType.ASIN)
|
||||
cubn = PrimaryLookupIdDescriptor(IdType.CUBN)
|
||||
|
@ -58,18 +59,14 @@ class Edition(Item):
|
|||
|
||||
|
||||
class Work(Item):
|
||||
# douban_work = PrimaryLookupIdDescriptor(IdType.DoubanBook_Work)
|
||||
# goodreads_work = PrimaryLookupIdDescriptor(IdType.Goodreads_Work)
|
||||
editions = models.ManyToManyField(Edition, related_name='works') # , through='WorkEdition'
|
||||
|
||||
# def __str__(self):
|
||||
# return self.title
|
||||
|
||||
# class Meta:
|
||||
# proxy = True
|
||||
category = ItemCategory.Book
|
||||
douban_work = PrimaryLookupIdDescriptor(IdType.DoubanBook_Work)
|
||||
goodreads_work = PrimaryLookupIdDescriptor(IdType.Goodreads_Work)
|
||||
editions = models.ManyToManyField(Edition, related_name='works')
|
||||
|
||||
|
||||
class Series(Item):
|
||||
category = ItemCategory.Book
|
||||
# douban_serie = LookupIdDescriptor(IdType.DoubanBook_Serie)
|
||||
# goodreads_serie = LookupIdDescriptor(IdType.Goodreads_Serie)
|
||||
|
||||
|
|
|
@ -11,6 +11,11 @@ class BookTestCase(TestCase):
|
|||
hyperion.save()
|
||||
# hyperion.isbn10 = '0553283685'
|
||||
|
||||
def test_url(self):
|
||||
hyperion = Edition.objects.get(title="Hyperion")
|
||||
hyperion2 = Edition.get_by_url(hyperion.url)
|
||||
self.assertEqual(hyperion, hyperion2)
|
||||
|
||||
def test_properties(self):
|
||||
hyperion = Edition.objects.get(title="Hyperion")
|
||||
self.assertEqual(hyperion.title, "Hyperion")
|
||||
|
|
|
@ -5,4 +5,4 @@ from .scrapers import *
|
|||
from . import jsondata
|
||||
|
||||
|
||||
__all__ = ('IdType', 'Item', 'ExternalResource', 'ResourceContent', 'ParseError', 'AbstractSite', 'SiteList', 'jsondata', 'PrimaryLookupIdDescriptor', 'LookupIdDescriptor', 'get_mock_mode', 'get_mock_file', 'use_local_response', 'RetryDownloader', 'BasicDownloader', 'ProxiedDownloader', 'BasicImageDownloader', 'RESPONSE_OK', 'RESPONSE_NETWORK_ERROR', 'RESPONSE_INVALID_CONTENT', 'RESPONSE_CENSORSHIP')
|
||||
__all__ = ('IdType', 'ItemCategory', 'Item', 'ExternalResource', 'ResourceContent', 'ParseError', 'AbstractSite', 'SiteList', 'jsondata', 'PrimaryLookupIdDescriptor', 'LookupIdDescriptor', 'get_mock_mode', 'get_mock_file', 'use_local_response', 'RetryDownloader', 'BasicDownloader', 'ProxiedDownloader', 'BasicImageDownloader', 'RESPONSE_OK', 'RESPONSE_NETWORK_ERROR', 'RESPONSE_INVALID_CONTENT', 'RESPONSE_CENSORSHIP')
|
||||
|
|
|
@ -5,6 +5,8 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.utils import timezone
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.baseconv import base62
|
||||
from simple_history.models import HistoricalRecords
|
||||
import uuid
|
||||
from .utils import DEFAULT_ITEM_COVER, item_cover_path
|
||||
# from django.conf import settings
|
||||
|
@ -66,6 +68,19 @@ class ItemType(models.TextChoices):
|
|||
Exhibition = 'exhibition', _('展览')
|
||||
|
||||
|
||||
class ItemCategory(models.TextChoices):
|
||||
Book = 'book', _('书')
|
||||
Movie = 'movie', _('电影')
|
||||
TV = 'tv', _('剧集')
|
||||
Music = 'music', _('音乐')
|
||||
Game = 'game', _('游戏')
|
||||
Boardgame = 'boardgame', _('桌游')
|
||||
Podcast = 'podcast', _('播客')
|
||||
FanFic = 'fanfic', _('网文')
|
||||
Performance = 'performance', _('演出')
|
||||
Exhibition = 'exhibition', _('展览')
|
||||
|
||||
|
||||
class SubItemType(models.TextChoices):
|
||||
Season = 'season', _('剧集分季')
|
||||
Episode = 'episode', _('剧集分集')
|
||||
|
@ -139,7 +154,9 @@ class LookupIdDescriptor(object): # TODO make it mixin of Field
|
|||
|
||||
|
||||
class Item(PolymorphicModel):
|
||||
uid = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
URL_PATH = None # subclass must specify this
|
||||
category = None # subclass must specify this
|
||||
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
||||
# item_type = models.CharField(_("类型"), choices=ItemType.choices, blank=False, max_length=50)
|
||||
title = models.CharField(_("title in primary language"), max_length=1000, default="")
|
||||
# title_ml = models.JSONField(_("title in different languages {['lang':'zh-cn', 'text':'', primary:True], ...}"), null=True, blank=True, default=list)
|
||||
|
@ -152,6 +169,9 @@ class Item(PolymorphicModel):
|
|||
cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True)
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
edited_time = models.DateTimeField(auto_now=True)
|
||||
is_deleted = models.BooleanField(default=False, db_index=True)
|
||||
history = HistoricalRecords()
|
||||
merged_to_item = models.ForeignKey('Item', null=True, on_delete=models.SET_NULL, default=None, related_name="merged_from_items")
|
||||
# parent_item = models.ForeignKey('Item', null=True, on_delete=models.SET_NULL, related_name='child_items')
|
||||
# identical_item = models.ForeignKey('Item', null=True, on_delete=models.SET_NULL, related_name='identical_items')
|
||||
# def get_lookup_id(self, id_type: str) -> str:
|
||||
|
@ -161,6 +181,15 @@ class Item(PolymorphicModel):
|
|||
class Meta:
|
||||
unique_together = [['polymorphic_ctype_id', 'primary_lookup_id_type', 'primary_lookup_id_value']]
|
||||
|
||||
def delete(self, using=None, soft=True, *args, **kwargs):
|
||||
if soft:
|
||||
self.primary_lookup_id_value = None
|
||||
self.primary_lookup_id_type = None
|
||||
self.is_deleted = True
|
||||
self.save(using=using)
|
||||
else:
|
||||
return super().delete(using=using, *args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id}{' ' + self.primary_lookup_id_type + ':' + self.primary_lookup_id_value if self.primary_lookup_id_value else ''} ({self.title})"
|
||||
|
||||
|
@ -178,6 +207,25 @@ class Item(PolymorphicModel):
|
|||
return t, lookup_ids[t]
|
||||
return list(lookup_ids.items())[0]
|
||||
|
||||
def merge(self, to_item):
|
||||
if to_item is None:
|
||||
raise(ValueError('cannot merge to an empty item'))
|
||||
elif to_item.merged_to_item is not None:
|
||||
raise(ValueError('cannot merge with an item aleady merged'))
|
||||
elif to_item.__class__ != self.__class__:
|
||||
raise(ValueError('cannot merge with an item in different class'))
|
||||
else:
|
||||
self.merged_to_item = to_item
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return f'/{self.URL_PATH}/{base62.encode(self.uid.int)}'
|
||||
|
||||
@classmethod
|
||||
def get_by_url(cls, url_or_b62):
|
||||
b62 = url_or_b62.split('/')[-1]
|
||||
return cls.objects.get(uid=uuid.UUID(int=base62.decode(b62)))
|
||||
|
||||
def update_lookup_ids(self, lookup_ids):
|
||||
# TODO
|
||||
# ll = set(lookup_ids)
|
||||
|
|
|
@ -2,6 +2,7 @@ from catalog.common import *
|
|||
|
||||
|
||||
class Game(Item):
|
||||
category = ItemCategory.Game
|
||||
igdb = PrimaryLookupIdDescriptor(IdType.IGDB)
|
||||
steam = PrimaryLookupIdDescriptor(IdType.Steam)
|
||||
douban_game = PrimaryLookupIdDescriptor(IdType.DoubanGame)
|
||||
|
|
|
@ -2,6 +2,7 @@ from catalog.common import *
|
|||
|
||||
|
||||
class Movie(Item):
|
||||
category = ItemCategory.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):
|
||||
category = ItemCategory.Music
|
||||
barcode = PrimaryLookupIdDescriptor(IdType.GTIN)
|
||||
douban_music = PrimaryLookupIdDescriptor(IdType.DoubanMusic)
|
||||
spotify_album = PrimaryLookupIdDescriptor(IdType.Spotify_Album)
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
|
||||
class Performance(Item):
|
||||
category = ItemCategory.Performance
|
||||
douban_drama = LookupIdDescriptor(IdType.DoubanDrama)
|
||||
versions = jsondata.ArrayField(_('版本'), null=False, blank=False, default=list)
|
||||
directors = jsondata.ArrayField(_('导演'), null=False, blank=False, default=list)
|
||||
|
|
|
@ -2,6 +2,7 @@ from catalog.common import *
|
|||
|
||||
|
||||
class Podcast(Item):
|
||||
category = ItemCategory.Podcast
|
||||
feed_url = PrimaryLookupIdDescriptor(IdType.Feed)
|
||||
apple_podcast = PrimaryLookupIdDescriptor(IdType.ApplePodcast)
|
||||
# ximalaya = LookupIdDescriptor(IdType.Ximalaya)
|
||||
|
|
|
@ -29,6 +29,7 @@ from django.db import models
|
|||
|
||||
|
||||
class TVShow(Item):
|
||||
category = ItemCategory.TV
|
||||
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
|
||||
tmdb_tv = PrimaryLookupIdDescriptor(IdType.TMDB_TV)
|
||||
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
|
||||
|
@ -36,6 +37,7 @@ class TVShow(Item):
|
|||
|
||||
|
||||
class TVSeason(Item):
|
||||
category = ItemCategory.TV
|
||||
douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie)
|
||||
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
|
||||
tmdb_tvseason = PrimaryLookupIdDescriptor(IdType.TMDB_TVSeason)
|
||||
|
@ -55,6 +57,7 @@ class TVSeason(Item):
|
|||
|
||||
|
||||
class TVEpisode(Item):
|
||||
category = ItemCategory.TV
|
||||
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()
|
||||
|
|
0
journal/__init__.py
Normal file
0
journal/__init__.py
Normal file
3
journal/admin.py
Normal file
3
journal/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
journal/apps.py
Normal file
6
journal/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class JournalConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'journal'
|
326
journal/models.py
Normal file
326
journal/models.py
Normal file
|
@ -0,0 +1,326 @@
|
|||
from django.db import models
|
||||
from polymorphic.models import PolymorphicModel
|
||||
from users.models import User
|
||||
from catalog.common.models import Item, ItemCategory
|
||||
from decimal import *
|
||||
from enum import Enum
|
||||
from markdownx.models import MarkdownxField
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
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
|
||||
|
||||
|
||||
class UserOwnedEntity(PolymorphicModel):
|
||||
class Meta:
|
||||
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
|
||||
metadata = models.JSONField(default=dict)
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
edited_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
def is_visible_to(self, viewer):
|
||||
if not viewer.is_authenticated:
|
||||
return self.visibility == 0
|
||||
owner = self.owner
|
||||
if owner == viewer:
|
||||
return True
|
||||
if not owner.is_active:
|
||||
return False
|
||||
if self.visibility == 2:
|
||||
return False
|
||||
if viewer.is_blocking(owner) or owner.is_blocking(viewer) or viewer.is_muting(owner):
|
||||
return False
|
||||
if self.visibility == 1:
|
||||
return viewer.is_following(owner)
|
||||
else:
|
||||
return True
|
||||
|
||||
def is_editable_by(self, viewer):
|
||||
return True if viewer.is_staff or viewer.is_superuser or viewer == self.owner else False
|
||||
|
||||
@classmethod
|
||||
def get_available(cls, entity, request_user, following_only=False):
|
||||
# e.g. SongMark.get_available(song, request.user)
|
||||
query_kwargs = {entity.__class__.__name__.lower(): entity}
|
||||
all_entities = cls.objects.filter(**query_kwargs).order_by("-created_time") # get all marks for song
|
||||
visible_entities = list(filter(lambda _entity: _entity.is_visible_to(request_user) and (_entity.owner.mastodon_username in request_user.mastodon_following if following_only else True), all_entities))
|
||||
return visible_entities
|
||||
|
||||
|
||||
class Content(UserOwnedEntity):
|
||||
target: models.ForeignKey(Item, on_delete=models.PROTECT)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id}({self.target})"
|
||||
|
||||
|
||||
class Note(Content):
|
||||
pass
|
||||
|
||||
|
||||
class Review(Content):
|
||||
warning = models.BooleanField(default=False)
|
||||
title = models.CharField(max_length=500, blank=False, null=True)
|
||||
body = MarkdownxField()
|
||||
pass
|
||||
|
||||
|
||||
class Rating(Content):
|
||||
grade = models.IntegerField(default=1, validators=[MaxValueValidator(10), MinValueValidator(0)])
|
||||
|
||||
|
||||
class Reply(Content):
|
||||
reply_to_content = models.ForeignKey(Content, on_delete=models.PROTECT, related_name='replies')
|
||||
title = models.CharField(max_length=500, null=True)
|
||||
body = MarkdownxField()
|
||||
pass
|
||||
|
||||
|
||||
"""
|
||||
List (abstract class)
|
||||
"""
|
||||
|
||||
|
||||
class List(UserOwnedEntity):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
MEMBER_CLASS = None # subclass must override this
|
||||
# subclass must add this:
|
||||
# items = models.ManyToManyField(Item, through='ListMember')
|
||||
|
||||
@property
|
||||
def ordered_members(self):
|
||||
return self.members.all().order_by('position', 'item_id')
|
||||
|
||||
@property
|
||||
def ordered_items(self):
|
||||
return self.items.all().order_by('collectionmember__position')
|
||||
|
||||
def has_item(self, item):
|
||||
return self.members.filter(item=item).count() > 0
|
||||
|
||||
def append_item(self, item, **params):
|
||||
if item is None or self.has_item(item):
|
||||
return None
|
||||
else:
|
||||
ml = self.ordered_members
|
||||
p = {self.__class__.__name__.lower(): self}
|
||||
p.update(params)
|
||||
i = self.MEMBER_CLASS.objects.create(position=ml.last().position + 1 if ml.count() else 1, item=item, **p)
|
||||
return i
|
||||
|
||||
def remove_item(self, item):
|
||||
member = self.members.all().filter(item=item).first()
|
||||
if member:
|
||||
member.delete()
|
||||
|
||||
def move_up_item(self, item):
|
||||
members = self.ordered_members
|
||||
member = members.filter(item=item).first()
|
||||
if member:
|
||||
other = members.filter(position__lt=member.position).last()
|
||||
if other:
|
||||
p = other.position
|
||||
other.position = member.position
|
||||
member.position = p
|
||||
other.save()
|
||||
member.save()
|
||||
|
||||
def move_down_item(self, item):
|
||||
members = self.ordered_members
|
||||
member = members.filter(item=item).first()
|
||||
if member:
|
||||
other = members.filter(position__gt=member.position).first()
|
||||
if other:
|
||||
p = other.position
|
||||
other.position = member.position
|
||||
member.position = p
|
||||
other.save()
|
||||
member.save()
|
||||
|
||||
|
||||
class ListMember(models.Model):
|
||||
# subclass must add this:
|
||||
# list = models.ForeignKey('ListClass', related_name='members', on_delete=models.CASCADE)
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
"""
|
||||
Queue
|
||||
"""
|
||||
|
||||
|
||||
class QueueType(models.TextChoices):
|
||||
WISHED = ('wished', '未开始')
|
||||
STARTED = ('started', '进行中')
|
||||
DONE = ('done', '完成')
|
||||
# DISCARDED = ('discarded', '放弃')
|
||||
|
||||
|
||||
QueueTypeNames = [
|
||||
[ItemCategory.Book, QueueType.WISHED, _('想读')],
|
||||
[ItemCategory.Book, QueueType.STARTED, _('在读')],
|
||||
[ItemCategory.Book, QueueType.DONE, _('读过')],
|
||||
[ItemCategory.Movie, QueueType.WISHED, _('想看')],
|
||||
[ItemCategory.Movie, QueueType.STARTED, _('在看')],
|
||||
[ItemCategory.Movie, QueueType.DONE, _('看过')],
|
||||
[ItemCategory.TV, QueueType.WISHED, _('想看')],
|
||||
[ItemCategory.TV, QueueType.STARTED, _('在看')],
|
||||
[ItemCategory.TV, QueueType.DONE, _('看过')],
|
||||
[ItemCategory.Music, QueueType.WISHED, _('想听')],
|
||||
[ItemCategory.Music, QueueType.STARTED, _('在听')],
|
||||
[ItemCategory.Music, QueueType.DONE, _('听过')],
|
||||
[ItemCategory.Game, QueueType.WISHED, _('想玩')],
|
||||
[ItemCategory.Game, QueueType.STARTED, _('在玩')],
|
||||
[ItemCategory.Game, QueueType.DONE, _('玩过')],
|
||||
# TODO add more combinations
|
||||
]
|
||||
|
||||
|
||||
class QueueMember(ListMember):
|
||||
queue = models.ForeignKey('Queue', related_name='members', on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Queue(List):
|
||||
class Meta:
|
||||
unique_together = [['owner', 'item_category', 'queue_type']]
|
||||
|
||||
MEMBER_CLASS = QueueMember
|
||||
items = models.ManyToManyField(Item, through='QueueMember', related_name=None)
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.id} {self.title}'
|
||||
|
||||
@cached_property
|
||||
def queue_type_name(self):
|
||||
return next(iter([n[2] for n in iter(QueueTypeNames) if n[0] == self.item_category and n[1] == self.queue_type]), self.queue_type)
|
||||
|
||||
@cached_property
|
||||
def title(self):
|
||||
q = _("{item_category} {queue_type_name} list").format(queue_type_name=self.queue_type_name, item_category=self.item_category)
|
||||
return _("{user}'s {queue_name}").format(user=self.owner.mastodon_username, queue_name=q)
|
||||
|
||||
|
||||
class QueueLogEntry(models.Model):
|
||||
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='%(class)ss')
|
||||
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)
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
edited_time = models.DateTimeField(auto_now=True)
|
||||
queued_time = models.DateTimeField(default=timezone.now)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.owner}:{self.queue}:{self.item}:{self.metadata}'
|
||||
|
||||
|
||||
class QueueManager:
|
||||
"""
|
||||
QueueManager
|
||||
|
||||
all queue operations should go thru this class so that QueueLogEntry can be properly populated
|
||||
QueueLogEntry can later be modified if user wish to change history
|
||||
"""
|
||||
|
||||
def __init__(self, user):
|
||||
self.owner = user
|
||||
|
||||
def initialize(self):
|
||||
for ic in ItemCategory:
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
def update_for_item(self, item, queue_type, metadata=None):
|
||||
# None means no change for metadata, comment
|
||||
if not item:
|
||||
raise ValueError('empty item')
|
||||
lastqm = self._queue_member_for_item(item)
|
||||
lastqmm = lastqm.metadata if lastqm else None
|
||||
lastq = lastqm.queue if lastqm else None
|
||||
lastqt = lastq.queue_type if lastq else None
|
||||
queue = self.get_queue(item.category, queue_type) if queue_type else None
|
||||
if lastq != queue:
|
||||
if lastq:
|
||||
lastq.remove_item(item)
|
||||
if queue:
|
||||
queue.append_item(item, metadata=metadata or {})
|
||||
elif metadata is not None:
|
||||
lastqm.metadata = metadata
|
||||
lastqm.save()
|
||||
elif lastqm:
|
||||
metadata = lastqm.metadata
|
||||
if lastqt != queue_type or (lastqt and metadata != lastqmm):
|
||||
QueueLogEntry.objects.create(owner=self.owner, queue=queue, item=item, metadata=metadata or {})
|
||||
|
||||
def get_log(self):
|
||||
return QueueLogEntry.objects.filter(owner=self.owner)
|
||||
|
||||
def get_log_for_item(self, item):
|
||||
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()
|
||||
|
||||
|
||||
"""
|
||||
Collection
|
||||
"""
|
||||
|
||||
|
||||
class CollectionMember(ListMember):
|
||||
collection = models.ForeignKey('Collection', related_name='members', on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Collection(Item, List):
|
||||
MEMBER_CLASS = CollectionMember
|
||||
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
|
||||
|
||||
@property
|
||||
def plain_description(self):
|
||||
html = markdown(self.description)
|
||||
return RE_HTML_TAG.sub(' ', html)
|
||||
|
||||
|
||||
"""
|
||||
Tag
|
||||
"""
|
||||
|
||||
|
||||
class TagMember(ListMember):
|
||||
tag = models.ForeignKey('Tag', related_name='members', on_delete=models.CASCADE)
|
||||
|
||||
|
||||
TagValidators = [RegexValidator(regex=r'\s+', inverse_match=True)]
|
||||
|
||||
|
||||
class Tag(List):
|
||||
MEMBER_CLASS = CollectionMember
|
||||
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']]
|
65
journal/tests.py
Normal file
65
journal/tests.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
from django.test import TestCase
|
||||
from .models import *
|
||||
from catalog.models import *
|
||||
from users.models import User
|
||||
|
||||
|
||||
class CollectionTest(TestCase):
|
||||
def setUp(self):
|
||||
self.book1 = Edition.objects.create(title="Hyperion")
|
||||
self.book2 = Edition.objects.create(title="Andymion")
|
||||
self.user = User.objects.create()
|
||||
pass
|
||||
|
||||
def test_collection(self):
|
||||
collection = Collection.objects.create(title="test", owner=self.user)
|
||||
collection.append_item(self.book1)
|
||||
collection.append_item(self.book2)
|
||||
self.assertEqual(list(collection.ordered_items), [self.book1, self.book2])
|
||||
collection.move_up_item(self.book1)
|
||||
self.assertEqual(list(collection.ordered_items), [self.book1, self.book2])
|
||||
collection.move_up_item(self.book2)
|
||||
self.assertEqual(list(collection.ordered_items), [self.book2, self.book1])
|
||||
|
||||
|
||||
class QueueTest(TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test_queue(self):
|
||||
user = User.objects.create(mastodon_site="site", username="name")
|
||||
queue_manager = QueueManager(user=user)
|
||||
queue_manager.initialize()
|
||||
self.assertEqual(user.queues.all().count(), 30)
|
||||
book1 = Edition.objects.create(title="Hyperion")
|
||||
book2 = Edition.objects.create(title="Andymion")
|
||||
q1 = queue_manager.get_queue(ItemCategory.Book, QueueType.WISHED)
|
||||
q2 = queue_manager.get_queue(ItemCategory.Book, QueueType.STARTED)
|
||||
self.assertIsNotNone(q1)
|
||||
self.assertIsNotNone(q2)
|
||||
self.assertEqual(q1.members.all().count(), 0)
|
||||
self.assertEqual(q2.members.all().count(), 0)
|
||||
queue_manager.update_for_item(book1, QueueType.WISHED)
|
||||
queue_manager.update_for_item(book2, QueueType.WISHED)
|
||||
self.assertEqual(q1.members.all().count(), 2)
|
||||
queue_manager.update_for_item(book1, QueueType.STARTED)
|
||||
self.assertEqual(q1.members.all().count(), 1)
|
||||
self.assertEqual(q2.members.all().count(), 1)
|
||||
queue_manager.update_for_item(book1, QueueType.STARTED, metadata={'progress': 1})
|
||||
self.assertEqual(q1.members.all().count(), 1)
|
||||
self.assertEqual(q2.members.all().count(), 1)
|
||||
log = queue_manager.get_log_for_item(book1)
|
||||
self.assertEqual(log.count(), 3)
|
||||
queue_manager.update_for_item(book1, QueueType.STARTED, metadata={'progress': 1})
|
||||
log = queue_manager.get_log_for_item(book1)
|
||||
self.assertEqual(log.count(), 3)
|
||||
queue_manager.update_for_item(book1, QueueType.STARTED, metadata={'progress': 10})
|
||||
log = queue_manager.get_log_for_item(book1)
|
||||
self.assertEqual(log.count(), 4)
|
||||
queue_manager.update_for_item(book1, QueueType.STARTED)
|
||||
log = queue_manager.get_log_for_item(book1)
|
||||
self.assertEqual(log.count(), 4)
|
||||
self.assertEqual(log.order_by('queued_time').last().metadata, {'progress': 10})
|
||||
queue_manager.update_for_item(book1, QueueType.STARTED, metadata={'progress': 100})
|
||||
log = queue_manager.get_log_for_item(book1)
|
||||
self.assertEqual(log.count(), 5)
|
3
journal/views.py
Normal file
3
journal/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
Loading…
Add table
Reference in a new issue