2023-07-19 11:12:58 -04:00
import re
import uuid
2022-12-11 23:20:28 +00:00
from functools import cached_property
2023-07-19 11:12:58 -04:00
2022-12-13 18:12:43 +00:00
import django . dispatch
2023-07-19 11:12:58 -04:00
from django . conf import settings
from django . contrib . contenttypes . models import ContentType
from django . core . exceptions import PermissionDenied
from django . core . validators import MaxValueValidator , MinValueValidator , RegexValidator
from django . db import connection , models
from django . db . models import Avg , Count , Q
from django . utils import timezone
2022-12-21 14:34:36 -05:00
from django . utils . baseconv import base62
2023-07-19 11:12:58 -04:00
from django . utils . translation import gettext_lazy as _
from markdownx . models import MarkdownxField
from polymorphic . models import PolymorphicModel
from catalog . collection . models import Collection as CatalogCollection
from catalog . common import jsondata
from catalog . common . models import Item , ItemCategory
from catalog . common . utils import DEFAULT_ITEM_COVER , piece_cover_path
2022-12-27 14:52:03 -05:00
from catalog . models import *
2023-07-19 11:12:58 -04:00
from mastodon . api import share_review
from users . models import User
from . mixins import UserOwnedObjectMixin
2023-01-31 10:24:57 -05:00
from . renderers import render_md , render_text
2023-01-30 17:12:25 -05:00
2023-01-07 00:35:30 -05:00
_logger = logging . getLogger ( __name__ )
2022-12-23 00:08:42 -05:00
2022-12-25 13:45:24 -05:00
class VisibilityType ( models . IntegerChoices ) :
2022-12-29 14:30:31 -05:00
Public = 0 , _ ( " 公开 " )
Follower_Only = 1 , _ ( " 仅关注者 " )
Private = 2 , _ ( " 仅自己 " )
2022-12-25 13:45:24 -05:00
2022-12-28 01:09:55 -05:00
def q_visible_to ( viewer , owner ) :
if viewer == owner :
return Q ( )
# elif viewer.is_blocked_by(owner):
# return Q(pk__in=[])
2023-01-01 23:50:57 -05:00
elif viewer . is_authenticated and viewer . is_following ( owner ) :
2023-01-09 01:16:10 -05:00
return Q ( visibility__in = [ 0 , 1 ] )
2022-12-28 01:09:55 -05:00
else :
return Q ( visibility = 0 )
2023-04-20 13:36:12 -04:00
def max_visiblity_to ( viewer , owner ) :
if viewer == owner :
return 2
# elif viewer.is_blocked_by(owner):
# return Q(pk__in=[])
elif viewer . is_authenticated and viewer . is_following ( owner ) :
return 1
else :
return 0
2022-12-23 00:08:42 -05:00
def query_visible ( user ) :
2022-12-29 14:30:31 -05:00
return (
2023-07-07 18:33:34 -04:00
(
Q ( visibility = 0 )
| Q ( owner_id__in = user . following , visibility = 1 )
| Q ( owner_id = user . id )
)
& ~ Q ( owner_id__in = user . ignoring )
if user . is_authenticated
else Q ( visibility = 0 )
)
2022-12-11 23:20:28 +00:00
2022-12-24 01:28:24 -05:00
def query_following ( user ) :
return Q ( owner_id__in = user . following , visibility__lt = 2 ) | Q ( owner_id = user . id )
2022-12-27 14:52:03 -05:00
def query_item_category ( item_category ) :
2023-06-19 12:57:45 -04:00
classes = item_categories ( ) [ item_category ]
2022-12-27 14:52:03 -05:00
# q = Q(item__instance_of=classes[0])
# for cls in classes[1:]:
# q = q | Q(instance_of=cls)
# return q
2023-06-19 12:57:45 -04:00
ct = item_content_types ( )
2022-12-28 10:24:07 -05:00
contenttype_ids = [ ct [ cls ] for cls in classes ]
2022-12-29 23:49:28 -05:00
return Q ( item__polymorphic_ctype__in = contenttype_ids )
2022-12-27 14:52:03 -05:00
2022-12-31 00:20:20 -05:00
# class ImportStatus(Enum):
# QUEUED = 0
# PROCESSING = 1
# FINISHED = 2
# class ImportSession(models.Model):
# owner = models.ForeignKey(User, on_delete=models.CASCADE)
# status = models.PositiveSmallIntegerField(default=ImportStatus.QUEUED)
# importer = models.CharField(max_length=50)
# file = models.CharField()
# default_visibility = models.PositiveSmallIntegerField()
# total = models.PositiveIntegerField()
# processed = models.PositiveIntegerField()
# skipped = models.PositiveIntegerField()
# imported = models.PositiveIntegerField()
# failed = models.PositiveIntegerField()
# logs = models.JSONField(default=list)
# created_time = models.DateTimeField(auto_now_add=True)
# edited_time = models.DateTimeField(auto_now=True)
# class Meta:
# indexes = [
# models.Index(fields=["owner", "importer", "created_time"]),
# ]
2022-12-13 06:44:29 +00:00
class Piece ( PolymorphicModel , UserOwnedObjectMixin ) :
2023-01-23 16:31:30 -05:00
url_path = " p " # subclass must specify this
2022-12-17 16:18:16 -05:00
uid = models . UUIDField ( default = uuid . uuid4 , editable = False , db_index = True )
2022-12-13 06:44:29 +00:00
2022-12-21 14:34:36 -05:00
@property
def uuid ( self ) :
return base62 . encode ( self . uid . int )
2022-12-24 01:28:24 -05:00
@property
def url ( self ) :
2022-12-29 14:30:31 -05:00
return f " / { self . url_path } / { self . uuid } " if self . url_path else None
2022-12-24 01:28:24 -05:00
@property
def absolute_url ( self ) :
2023-07-10 15:29:29 -04:00
return ( settings . SITE_INFO [ " site_url " ] + self . url ) if self . url_path else None
2022-12-24 01:28:24 -05:00
@property
def api_url ( self ) :
2022-12-29 16:20:33 -05:00
return f " /api/ { self . url } " if self . url_path else None
2022-12-24 01:28:24 -05:00
2023-02-11 20:40:00 -05:00
@property
def like_count ( self ) :
return self . likes . all ( ) . count ( )
2023-02-03 00:02:43 -05:00
@classmethod
def get_by_url ( cls , url_or_b62 ) :
b62 = url_or_b62 . strip ( ) . split ( " / " ) [ - 1 ]
if len ( b62 ) not in [ 21 , 22 ] :
r = re . search ( r " [A-Za-z0-9] { 21,22} " , url_or_b62 )
if r :
b62 = r [ 0 ]
try :
obj = cls . objects . get ( uid = uuid . UUID ( int = base62 . decode ( b62 ) ) )
except :
obj = None
return obj
2022-12-13 06:44:29 +00:00
2022-12-15 17:29:35 -05:00
class Content ( Piece ) :
2022-12-29 16:20:33 -05:00
owner = models . ForeignKey ( User , on_delete = models . PROTECT )
visibility = models . PositiveSmallIntegerField (
default = 0
) # 0: Public / 1: Follower only / 2: Self only
2023-01-11 09:20:14 -05:00
created_time = models . DateTimeField ( default = timezone . now )
2022-12-29 16:20:33 -05:00
edited_time = models . DateTimeField (
default = timezone . now
) # auto_now=True FIXME revert this after migration
metadata = models . JSONField ( default = dict )
2022-12-14 21:12:37 -05:00
item = models . ForeignKey ( Item , on_delete = models . PROTECT )
2022-12-11 23:20:28 +00:00
def __str__ ( self ) :
2022-12-25 13:45:24 -05:00
return f " { self . uuid } @ { self . item } "
2022-12-11 23:20:28 +00:00
2022-12-15 17:29:35 -05:00
class Meta :
abstract = True
2022-12-11 23:20:28 +00:00
2022-12-20 11:32:44 -05:00
class Like ( Piece ) :
2022-12-29 16:20:33 -05:00
owner = models . ForeignKey ( User , on_delete = models . PROTECT )
visibility = models . PositiveSmallIntegerField (
default = 0
) # 0: Public / 1: Follower only / 2: Self only
created_time = models . DateTimeField (
default = timezone . now
) # auto_now_add=True FIXME revert this after migration
edited_time = models . DateTimeField (
default = timezone . now
) # auto_now=True FIXME revert this after migration
2022-12-29 14:30:31 -05:00
target = models . ForeignKey ( Piece , on_delete = models . CASCADE , related_name = " likes " )
2022-12-20 11:32:44 -05:00
2022-12-29 16:20:33 -05:00
@staticmethod
def user_liked_piece ( user , piece ) :
2023-02-11 20:40:00 -05:00
return Like . objects . filter ( owner = user , target = piece ) . exists ( )
2022-12-29 16:20:33 -05:00
2022-12-21 14:34:36 -05:00
@staticmethod
def user_like_piece ( user , piece ) :
2023-02-11 20:40:00 -05:00
if not piece :
2022-12-21 14:34:36 -05:00
return
like = Like . objects . filter ( owner = user , target = piece ) . first ( )
if not like :
like = Like . objects . create ( owner = user , target = piece )
return like
2022-12-29 14:30:31 -05:00
@staticmethod
def user_unlike_piece ( user , piece ) :
if not piece :
return
Like . objects . filter ( owner = user , target = piece ) . delete ( )
2022-12-29 16:20:33 -05:00
@staticmethod
def user_likes_by_class ( user , cls ) :
ctype_id = ContentType . objects . get_for_model ( cls )
return Like . objects . filter ( owner = user , target__polymorphic_ctype = ctype_id )
2022-12-20 11:32:44 -05:00
2022-12-29 14:30:31 -05:00
class Memo ( Content ) :
2022-12-11 23:20:28 +00:00
pass
2022-12-15 17:29:35 -05:00
class Comment ( Content ) :
text = models . TextField ( blank = False , null = False )
2023-01-31 10:24:57 -05:00
@property
def html ( self ) :
return render_text ( self . text )
2023-05-20 11:01:18 -04:00
@cached_property
def rating_grade ( self ) :
return Rating . get_item_rating_by_user ( self . item , self . owner )
@cached_property
def mark ( self ) :
m = Mark ( self . owner , self . item )
m . comment = self
return m
2023-02-01 22:40:34 -05:00
@property
def item_url ( self ) :
2023-07-12 17:55:48 -04:00
if self . metadata . get ( " position " ) :
return self . item . get_absolute_url_with_position ( self . metadata [ " position " ] )
2023-02-01 22:40:34 -05:00
else :
return self . item . url
2022-12-15 17:29:35 -05:00
@staticmethod
2023-06-02 21:54:48 -04:00
def comment_item_by_user ( item , user , text , visibility = 0 , created_time = None ) :
2023-07-12 17:55:48 -04:00
comment = Comment . objects . filter ( owner = user , item = item ) . first ( )
2022-12-17 16:18:16 -05:00
if not text :
2022-12-15 17:29:35 -05:00
if comment is not None :
comment . delete ( )
comment = None
elif comment is None :
2022-12-29 14:30:31 -05:00
comment = Comment . objects . create (
2023-06-02 21:54:48 -04:00
owner = user ,
item = item ,
text = text ,
visibility = visibility ,
created_time = created_time or timezone . now ( ) ,
2022-12-29 14:30:31 -05:00
)
2022-12-17 16:18:16 -05:00
elif comment . text != text or comment . visibility != visibility :
2022-12-15 17:29:35 -05:00
comment . text = text
comment . visibility = visibility
2023-06-02 21:54:48 -04:00
if created_time :
comment . created_time = created_time
2022-12-15 17:29:35 -05:00
comment . save ( )
return comment
2022-12-11 23:20:28 +00:00
class Review ( Content ) :
2022-12-29 14:30:31 -05:00
url_path = " review "
2022-12-15 17:29:35 -05:00
title = models . CharField ( max_length = 500 , blank = False , null = False )
2022-12-11 23:20:28 +00:00
body = MarkdownxField ( )
2022-12-24 01:28:24 -05:00
@property
def html_content ( self ) :
2023-01-30 17:12:25 -05:00
return render_md ( self . body )
2022-12-24 01:28:24 -05:00
2023-05-20 11:01:18 -04:00
@property
def plain_content ( self ) :
html = render_md ( self . body )
return _RE_HTML_TAG . sub (
" " , _RE_SPOILER_TAG . sub ( " *** " , html . replace ( " \n " , " " ) )
)
@cached_property
def mark ( self ) :
m = Mark ( self . owner , self . item )
m . review = self
return m
2022-12-26 14:56:39 -05:00
@cached_property
def rating_grade ( self ) :
return Rating . get_item_rating_by_user ( self . item , self . owner )
2023-06-03 01:22:19 -04:00
@classmethod
def review_item_by_user (
cls ,
item ,
user ,
title ,
body ,
visibility = 0 ,
created_time = None ,
share_to_mastodon = False ,
) :
2022-12-15 17:29:35 -05:00
if title is None :
2023-06-03 01:22:19 -04:00
review = Review . objects . filter ( owner = user , item = item ) . first ( )
2022-12-15 17:29:35 -05:00
if review is not None :
review . delete ( )
2023-06-03 01:22:19 -04:00
return None
defaults = {
" title " : title ,
" body " : body ,
" visibility " : visibility ,
}
if created_time :
defaults [ " created_time " ] = (
created_time if created_time < timezone . now ( ) else timezone . now ( )
)
review , created = cls . objects . update_or_create (
item = item , owner = user , defaults = defaults
)
if share_to_mastodon :
share_review ( review )
2022-12-15 17:29:35 -05:00
return review
2022-12-11 23:20:28 +00:00
2023-05-20 11:01:18 -04:00
MIN_RATING_COUNT = 5
2023-06-16 17:47:22 -04:00
RATING_INCLUDES_CHILD_ITEMS = [ " tvshow " , " performance " ]
2023-05-20 11:01:18 -04:00
2022-12-15 17:29:35 -05:00
class Rating ( Content ) :
2023-01-12 16:08:10 -05:00
class Meta :
unique_together = [ [ " owner " , " item " ] ]
2023-01-07 00:35:30 -05:00
2022-12-29 14:30:31 -05:00
grade = models . PositiveSmallIntegerField (
default = 0 , validators = [ MaxValueValidator ( 10 ) , MinValueValidator ( 1 ) ] , null = True
)
2022-12-11 23:20:28 +00:00
2022-12-29 14:30:31 -05:00
@staticmethod
2022-12-14 21:12:37 -05:00
def get_rating_for_item ( item ) :
2023-06-16 17:47:22 -04:00
stat = Rating . objects . filter ( grade__isnull = False )
if item . class_name in RATING_INCLUDES_CHILD_ITEMS :
stat = stat . filter ( item_id__in = item . child_item_ids + [ item . id ] )
else :
stat = stat . filter ( item = item )
stat = stat . aggregate ( average = Avg ( " grade " ) , count = Count ( " item " ) )
2023-05-20 11:01:18 -04:00
return round ( stat [ " average " ] , 1 ) if stat [ " count " ] > = MIN_RATING_COUNT else None
2022-12-14 21:12:37 -05:00
2022-12-29 14:30:31 -05:00
@staticmethod
2022-12-14 21:12:37 -05:00
def get_rating_count_for_item ( item ) :
2023-06-16 17:47:22 -04:00
stat = Rating . objects . filter ( grade__isnull = False )
if item . class_name in RATING_INCLUDES_CHILD_ITEMS :
stat = stat . filter ( item_id__in = item . child_item_ids + [ item . id ] )
else :
stat = stat . filter ( item = item )
stat = stat . aggregate ( count = Count ( " item " ) )
2022-12-29 14:30:31 -05:00
return stat [ " count " ]
2022-12-14 21:12:37 -05:00
2023-06-16 17:47:22 -04:00
@staticmethod
def get_rating_distribution_for_item ( item ) :
stat = Rating . objects . filter ( grade__isnull = False )
if item . class_name in RATING_INCLUDES_CHILD_ITEMS :
stat = stat . filter ( item_id__in = item . child_item_ids + [ item . id ] )
else :
stat = stat . filter ( item = item )
stat = stat . values ( " grade " ) . annotate ( count = Count ( " grade " ) ) . order_by ( " grade " )
g = [ 0 ] * 11
t = 0
for s in stat :
g [ s [ " grade " ] ] = s [ " count " ]
t + = s [ " count " ]
if t < MIN_RATING_COUNT :
return [ 0 ] * 5
r = [
100 * ( g [ 1 ] + g [ 2 ] ) / / t ,
100 * ( g [ 3 ] + g [ 4 ] ) / / t ,
100 * ( g [ 5 ] + g [ 6 ] ) / / t ,
100 * ( g [ 7 ] + g [ 8 ] ) / / t ,
100 * ( g [ 9 ] + g [ 10 ] ) / / t ,
]
return r
2022-12-29 14:30:31 -05:00
@staticmethod
2022-12-17 16:18:16 -05:00
def rate_item_by_user ( item , user , rating_grade , visibility = 0 ) :
2022-12-17 17:18:08 -05:00
if rating_grade and ( rating_grade < 1 or rating_grade > 10 ) :
2022-12-29 14:30:31 -05:00
raise ValueError ( f " Invalid rating grade: { rating_grade } " )
2022-12-15 17:29:35 -05:00
rating = Rating . objects . filter ( owner = user , item = item ) . first ( )
2022-12-17 16:18:16 -05:00
if not rating_grade :
if rating :
rating . delete ( )
rating = None
elif rating is None :
2022-12-29 14:30:31 -05:00
rating = Rating . objects . create (
owner = user , item = item , grade = rating_grade , visibility = visibility
)
2022-12-17 16:18:16 -05:00
elif rating . grade != rating_grade or rating . visibility != visibility :
2022-12-15 17:29:35 -05:00
rating . visibility = visibility
rating . grade = rating_grade
rating . save ( )
2022-12-17 16:18:16 -05:00
return rating
2022-12-15 17:29:35 -05:00
2022-12-29 14:30:31 -05:00
@staticmethod
2022-12-15 17:29:35 -05:00
def get_item_rating_by_user ( item , user ) :
rating = Rating . objects . filter ( owner = user , item = item ) . first ( )
2023-06-02 21:54:48 -04:00
return ( rating . grade or None ) if rating else None
2022-12-15 17:29:35 -05:00
2022-12-14 21:12:37 -05:00
2022-12-15 17:29:35 -05:00
Item . rating = property ( Rating . get_rating_for_item )
Item . rating_count = property ( Rating . get_rating_count_for_item )
2023-05-20 11:01:18 -04:00
Item . rating_dist = property ( Rating . get_rating_distribution_for_item )
2022-12-14 21:12:37 -05:00
2022-12-20 11:32:44 -05:00
class Reply ( Piece ) :
2022-12-29 14:30:31 -05:00
reply_to_content = models . ForeignKey (
Piece , on_delete = models . SET_NULL , related_name = " replies " , null = True
)
2022-12-11 23:20:28 +00:00
title = models . CharField ( max_length = 500 , null = True )
body = MarkdownxField ( )
"""
List ( abstract class )
"""
2022-12-13 18:12:43 +00:00
list_add = django . dispatch . Signal ( )
list_remove = django . dispatch . Signal ( )
2022-12-11 23:20:28 +00:00
2022-12-12 16:46:37 +00:00
class List ( Piece ) :
2022-12-29 16:20:33 -05:00
owner = models . ForeignKey ( User , on_delete = models . PROTECT )
visibility = models . PositiveSmallIntegerField (
default = 0
) # 0: Public / 1: Follower only / 2: Self only
created_time = models . DateTimeField (
default = timezone . now
) # auto_now_add=True FIXME revert this after migration
edited_time = models . DateTimeField (
default = timezone . now
) # auto_now=True FIXME revert this after migration
metadata = models . JSONField ( default = dict )
2022-12-11 23:20:28 +00:00
class Meta :
abstract = True
2022-12-13 06:44:29 +00:00
# MEMBER_CLASS = None # subclass must override this
2022-12-11 23:20:28 +00:00
# subclass must add this:
# items = models.ManyToManyField(Item, through='ListMember')
2022-12-28 01:09:55 -05:00
@property
2022-12-11 23:20:28 +00:00
def ordered_members ( self ) :
2022-12-29 14:30:31 -05:00
return self . members . all ( ) . order_by ( " position " )
2022-12-11 23:20:28 +00:00
2022-12-28 01:09:55 -05:00
@property
2022-12-11 23:20:28 +00:00
def ordered_items ( self ) :
2022-12-29 14:30:31 -05:00
return self . items . all ( ) . order_by (
self . MEMBER_CLASS . __name__ . lower ( ) + " __position "
)
2022-12-11 23:20:28 +00:00
2022-12-28 01:09:55 -05:00
@property
def recent_items ( self ) :
2022-12-29 14:30:31 -05:00
return self . items . all ( ) . order_by (
" - " + self . MEMBER_CLASS . __name__ . lower ( ) + " __created_time "
)
2022-12-28 01:09:55 -05:00
@property
def recent_members ( self ) :
2022-12-29 14:30:31 -05:00
return self . members . all ( ) . order_by ( " -created_time " )
2022-12-28 01:09:55 -05:00
2022-12-29 14:30:31 -05:00
def get_member_for_item ( self , item ) :
return self . members . filter ( item = item ) . first ( )
2022-12-11 23:20:28 +00:00
2023-01-16 14:03:27 -05:00
def get_summary ( self ) :
summary = { k : 0 for k in ItemCategory . values }
for c in self . recent_items :
summary [ c . category ] + = 1
return summary
2022-12-11 23:20:28 +00:00
def append_item ( self , item , * * params ) :
2023-01-08 22:10:48 -05:00
"""
named metadata fields should be specified directly , not in metadata dict !
e . g . collection . append_item ( item , note = " abc " ) works , but collection . append_item ( item , metadata = { " note " : " abc " } ) doesn ' t
"""
2023-01-16 14:03:27 -05:00
if item is None :
2022-12-11 23:20:28 +00:00
return None
2023-01-16 14:03:27 -05:00
member = self . get_member_for_item ( item )
if member :
2022-12-13 18:12:43 +00:00
return member
2023-01-16 14:03:27 -05:00
ml = self . ordered_members
p = { " parent " : self }
p . update ( params )
member = self . MEMBER_CLASS . objects . create (
owner = self . owner ,
position = ml . last ( ) . position + 1 if ml . count ( ) else 1 ,
item = item ,
* * p ,
)
list_add . send ( sender = self . __class__ , instance = self , item = item , member = member )
return member
2022-12-11 23:20:28 +00:00
def remove_item ( self , item ) :
2022-12-29 14:30:31 -05:00
member = self . get_member_for_item ( item )
2022-12-11 23:20:28 +00:00
if member :
2022-12-29 14:30:31 -05:00
list_remove . send (
sender = self . __class__ , instance = self , item = item , member = member
)
2022-12-11 23:20:28 +00:00
member . delete ( )
2023-05-20 11:01:18 -04:00
def update_member_order ( self , ordered_member_ids ) :
members = self . ordered_members
for m in self . members . all ( ) :
try :
i = ordered_member_ids . index ( m . id )
if m . position != i + 1 :
m . position = i + 1
m . save ( )
except ValueError :
pass
2022-12-11 23:20:28 +00:00
def move_up_item ( self , item ) :
members = self . ordered_members
2022-12-29 14:30:31 -05:00
member = self . get_member_for_item ( item )
2022-12-11 23:20:28 +00:00
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
2022-12-29 14:30:31 -05:00
member = self . get_member_for_item ( item )
2022-12-11 23:20:28 +00:00
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 ( )
2022-12-29 14:30:31 -05:00
def update_item_metadata ( self , item , metadata ) :
member = self . get_member_for_item ( item )
if member :
member . metadata = metadata
member . save ( )
2022-12-11 23:20:28 +00:00
2022-12-13 06:44:29 +00:00
class ListMember ( Piece ) :
"""
ListMember - List class ' s member class
It ' s an abstract class, subclass must add this:
2022-12-27 14:52:03 -05:00
parent = models . ForeignKey ( ' List ' , related_name = ' members ' , on_delete = models . CASCADE )
2022-12-13 06:44:29 +00:00
"""
2022-12-29 14:30:31 -05:00
2022-12-29 16:20:33 -05:00
owner = models . ForeignKey ( User , on_delete = models . PROTECT )
visibility = models . PositiveSmallIntegerField (
default = 0
) # 0: Public / 1: Follower only / 2: Self only
2023-01-11 09:20:14 -05:00
created_time = models . DateTimeField ( default = timezone . now )
2022-12-29 16:20:33 -05:00
edited_time = models . DateTimeField (
default = timezone . now
) # auto_now=True FIXME revert this after migration
metadata = models . JSONField ( default = dict )
2022-12-11 23:20:28 +00:00
item = models . ForeignKey ( Item , on_delete = models . PROTECT )
position = models . PositiveIntegerField ( )
2022-12-27 14:52:03 -05:00
@cached_property
def mark ( self ) :
m = Mark ( self . owner , self . item )
return m
2022-12-11 23:20:28 +00:00
class Meta :
abstract = True
2022-12-13 18:12:43 +00:00
def __str__ ( self ) :
2022-12-29 14:30:31 -05:00
return f " { self . id } : { self . position } ( { self . item } ) "
2022-12-13 18:12:43 +00:00
2022-12-11 23:20:28 +00:00
"""
2022-12-13 18:12:43 +00:00
Shelf
2022-12-11 23:20:28 +00:00
"""
2022-12-13 18:12:43 +00:00
class ShelfType ( models . TextChoices ) :
2022-12-29 14:30:31 -05:00
WISHLIST = ( " wishlist " , " 未开始 " )
PROGRESS = ( " progress " , " 进行中 " )
COMPLETE = ( " complete " , " 完成 " )
2022-12-11 23:20:28 +00:00
# DISCARDED = ('discarded', '放弃')
2022-12-13 18:12:43 +00:00
ShelfTypeNames = [
2022-12-29 14:30:31 -05:00
[ ItemCategory . Book , ShelfType . WISHLIST , _ ( " 想读 " ) ] ,
[ ItemCategory . Book , ShelfType . PROGRESS , _ ( " 在读 " ) ] ,
[ ItemCategory . Book , ShelfType . COMPLETE , _ ( " 读过 " ) ] ,
[ ItemCategory . Movie , ShelfType . WISHLIST , _ ( " 想看 " ) ] ,
[ ItemCategory . Movie , ShelfType . PROGRESS , _ ( " 在看 " ) ] ,
[ ItemCategory . Movie , ShelfType . COMPLETE , _ ( " 看过 " ) ] ,
[ ItemCategory . TV , ShelfType . WISHLIST , _ ( " 想看 " ) ] ,
[ ItemCategory . TV , ShelfType . PROGRESS , _ ( " 在看 " ) ] ,
[ ItemCategory . TV , ShelfType . COMPLETE , _ ( " 看过 " ) ] ,
[ ItemCategory . Music , ShelfType . WISHLIST , _ ( " 想听 " ) ] ,
[ ItemCategory . Music , ShelfType . PROGRESS , _ ( " 在听 " ) ] ,
[ ItemCategory . Music , ShelfType . COMPLETE , _ ( " 听过 " ) ] ,
[ ItemCategory . Game , ShelfType . WISHLIST , _ ( " 想玩 " ) ] ,
[ ItemCategory . Game , ShelfType . PROGRESS , _ ( " 在玩 " ) ] ,
[ ItemCategory . Game , ShelfType . COMPLETE , _ ( " 玩过 " ) ] ,
2023-01-29 20:05:30 -05:00
[ ItemCategory . Podcast , ShelfType . WISHLIST , _ ( " 想听 " ) ] ,
[ ItemCategory . Podcast , ShelfType . PROGRESS , _ ( " 在听 " ) ] ,
[ ItemCategory . Podcast , ShelfType . COMPLETE , _ ( " 听过 " ) ] ,
2023-06-16 17:47:22 -04:00
# disable all shelves for PodcastEpisode
2023-02-15 23:45:12 -05:00
[ ItemCategory . Performance , ShelfType . WISHLIST , _ ( " 想看 " ) ] ,
2023-06-16 17:47:22 -04:00
# disable progress shelf for Performance
2023-02-15 23:45:12 -05:00
[ ItemCategory . Performance , ShelfType . PROGRESS , _ ( " " ) ] ,
[ ItemCategory . Performance , ShelfType . COMPLETE , _ ( " 看过 " ) ] ,
2022-12-11 23:20:28 +00:00
]
2022-12-13 18:12:43 +00:00
class ShelfMember ( ListMember ) :
2022-12-29 14:30:31 -05:00
parent = models . ForeignKey (
" Shelf " , related_name = " members " , on_delete = models . CASCADE
)
2022-12-11 23:20:28 +00:00
2023-01-12 16:08:10 -05:00
class Meta :
2023-01-23 16:31:30 -05:00
unique_together = [ [ " owner " , " item " ] ]
2023-04-21 18:27:49 -04:00
indexes = [
models . Index ( fields = [ " parent_id " , " visibility " , " created_time " ] ) ,
]
2023-01-07 12:00:09 -05:00
2023-01-01 01:07:32 -05:00
@cached_property
def mark ( self ) :
m = Mark ( self . owner , self . item )
m . shelfmember = self
return m
2023-06-02 21:54:48 -04:00
@property
def shelf_label ( self ) :
return ShelfManager . get_label ( self . parent . shelf_type , self . item . category )
@property
def shelf_type ( self ) :
return self . parent . shelf_type
@property
def rating_grade ( self ) :
return self . mark . rating_grade
@property
def comment_text ( self ) :
return self . mark . comment_text
@property
def tags ( self ) :
return self . mark . tags
2022-12-11 23:20:28 +00:00
2022-12-13 18:12:43 +00:00
class Shelf ( List ) :
2022-12-11 23:20:28 +00:00
class Meta :
2022-12-29 23:49:28 -05:00
unique_together = [ [ " owner " , " shelf_type " ] ]
2022-12-11 23:20:28 +00:00
2022-12-13 18:12:43 +00:00
MEMBER_CLASS = ShelfMember
2022-12-29 14:30:31 -05:00
items = models . ManyToManyField ( Item , through = " ShelfMember " , related_name = " + " )
shelf_type = models . CharField (
choices = ShelfType . choices , max_length = 100 , null = False , blank = False
)
2022-12-11 23:20:28 +00:00
def __str__ ( self ) :
2022-12-29 23:49:28 -05:00
return f " { self . id } [ { self . owner } { self . shelf_type } list] "
2022-12-11 23:20:28 +00:00
2022-12-13 18:12:43 +00:00
class ShelfLogEntry ( models . Model ) :
2022-12-12 16:46:37 +00:00
owner = models . ForeignKey ( User , on_delete = models . PROTECT )
2023-01-11 00:57:58 -05:00
shelf_type = models . CharField ( choices = ShelfType . choices , max_length = 100 , null = True )
2022-12-11 23:20:28 +00:00
item = models . ForeignKey ( Item , on_delete = models . PROTECT )
2023-01-10 22:36:13 -05:00
timestamp = models . DateTimeField ( ) # this may later be changed by user
2022-12-11 23:20:28 +00:00
metadata = models . JSONField ( default = dict )
created_time = models . DateTimeField ( auto_now_add = True )
edited_time = models . DateTimeField ( auto_now = True )
def __str__ ( self ) :
2023-01-12 11:02:14 -05:00
return f " { self . owner } : { self . shelf_type } : { self . item . uuid } : { self . timestamp } : { self . metadata } "
2022-12-11 23:20:28 +00:00
2023-01-10 22:36:13 -05:00
@property
def action_label ( self ) :
2023-01-12 11:02:14 -05:00
if self . shelf_type :
return ShelfManager . get_action_label ( self . shelf_type , self . item . category )
2023-01-10 22:36:13 -05:00
else :
return _ ( " 移除标记 " )
2022-12-11 23:20:28 +00:00
2022-12-13 18:12:43 +00:00
class ShelfManager :
2022-12-11 23:20:28 +00:00
"""
2022-12-13 18:12:43 +00:00
ShelfManager
2022-12-11 23:20:28 +00:00
2022-12-13 18:12:43 +00:00
all shelf operations should go thru this class so that ShelfLogEntry can be properly populated
ShelfLogEntry can later be modified if user wish to change history
2022-12-11 23:20:28 +00:00
"""
def __init__ ( self , user ) :
self . owner = user
2022-12-29 23:49:28 -05:00
qs = Shelf . objects . filter ( owner = self . owner )
self . shelf_list = { v . shelf_type : v for v in qs }
if len ( self . shelf_list ) == 0 :
self . initialize ( )
2022-12-11 23:20:28 +00:00
def initialize ( self ) :
2022-12-29 23:49:28 -05:00
for qt in ShelfType :
self . shelf_list [ qt ] = Shelf . objects . create ( owner = self . owner , shelf_type = qt )
2022-12-11 23:20:28 +00:00
2023-07-19 11:12:58 -04:00
def locate_item ( self , item ) - > ShelfMember | None :
2023-01-24 19:02:56 -05:00
return ShelfMember . objects . filter ( item = item , owner = self . owner ) . first ( )
2022-12-11 23:20:28 +00:00
2023-06-17 16:45:47 +02:00
def move_item ( self , item , shelf_type , visibility = 0 , metadata = None , silence = False ) :
2022-12-13 18:12:43 +00:00
# shelf_type=None means remove from current shelf
# metadata=None means no change
2023-06-17 16:45:47 +02:00
# silence=False means move_item is logged.
2022-12-11 23:20:28 +00:00
if not item :
2022-12-29 14:30:31 -05:00
raise ValueError ( " empty item " )
2022-12-17 16:18:16 -05:00
new_shelfmember = None
2022-12-29 23:49:28 -05:00
last_shelfmember = self . locate_item ( item )
2022-12-21 14:34:36 -05:00
last_shelf = last_shelfmember . parent if last_shelfmember else None
2022-12-16 01:08:10 -05:00
last_metadata = last_shelfmember . metadata if last_shelfmember else None
last_visibility = last_shelfmember . visibility if last_shelfmember else None
2022-12-29 23:49:28 -05:00
shelf = self . shelf_list [ shelf_type ] if shelf_type else None
2022-12-16 01:08:10 -05:00
changed = False
if last_shelf != shelf : # change shelf
changed = True
if last_shelf :
last_shelf . remove_item ( item )
2022-12-13 18:12:43 +00:00
if shelf :
2022-12-29 14:30:31 -05:00
new_shelfmember = shelf . append_item (
item , visibility = visibility , metadata = metadata or { }
)
2022-12-16 01:08:10 -05:00
elif last_shelf is None :
2022-12-29 14:30:31 -05:00
raise ValueError ( " empty shelf " )
2022-12-16 01:08:10 -05:00
else :
2022-12-17 16:18:16 -05:00
new_shelfmember = last_shelfmember
2022-12-16 01:08:10 -05:00
if metadata is not None and metadata != last_metadata : # change metadata
changed = True
last_shelfmember . metadata = metadata
last_shelfmember . visibility = visibility
last_shelfmember . save ( )
elif visibility != last_visibility : # change visibility
last_shelfmember . visibility = visibility
last_shelfmember . save ( )
2023-06-17 16:45:47 +02:00
if changed and not silence :
2022-12-16 01:08:10 -05:00
if metadata is None :
metadata = last_metadata or { }
2023-01-10 22:36:13 -05:00
log_time = (
2023-01-12 17:13:23 -05:00
new_shelfmember . created_time
if new_shelfmember and new_shelfmember != last_shelfmember
else timezone . now ( )
2023-01-10 22:36:13 -05:00
)
2022-12-29 14:30:31 -05:00
ShelfLogEntry . objects . create (
2023-01-11 00:57:58 -05:00
owner = self . owner ,
shelf_type = shelf_type ,
item = item ,
metadata = metadata ,
2023-01-10 22:36:13 -05:00
timestamp = log_time ,
2022-12-29 14:30:31 -05:00
)
2022-12-17 16:18:16 -05:00
return new_shelfmember
2022-12-11 23:20:28 +00:00
def get_log ( self ) :
2022-12-29 14:30:31 -05:00
return ShelfLogEntry . objects . filter ( owner = self . owner ) . order_by ( " timestamp " )
2022-12-11 23:20:28 +00:00
def get_log_for_item ( self , item ) :
2022-12-29 14:30:31 -05:00
return ShelfLogEntry . objects . filter ( owner = self . owner , item = item ) . order_by (
" timestamp "
)
2022-12-11 23:20:28 +00:00
2022-12-29 23:49:28 -05:00
def get_shelf ( self , shelf_type ) :
return self . shelf_list [ shelf_type ]
2022-12-11 23:20:28 +00:00
2023-06-02 21:54:48 -04:00
def get_latest_members ( self , shelf_type , item_category = None ) :
qs = self . shelf_list [ shelf_type ] . members . all ( ) . order_by ( " -created_time " )
2023-05-20 11:01:18 -04:00
if item_category :
2023-06-02 21:54:48 -04:00
return qs . filter ( query_item_category ( item_category ) )
2023-05-20 11:01:18 -04:00
else :
2023-06-02 21:54:48 -04:00
return qs
2022-12-29 23:49:28 -05:00
# def get_items_on_shelf(self, item_category, shelf_type):
# shelf = (
# self.owner.shelf_set.all()
# .filter(item_category=item_category, shelf_type=shelf_type)
# .first()
# )
# return shelf.members.all().order_by
2023-01-10 22:36:13 -05:00
@classmethod
def get_action_label ( cls , shelf_type , item_category ) :
2022-12-29 23:49:28 -05:00
sts = [
n [ 2 ] for n in ShelfTypeNames if n [ 0 ] == item_category and n [ 1 ] == shelf_type
]
2023-01-09 02:59:59 -05:00
return sts [ 0 ] if sts else shelf_type
2023-06-02 21:54:48 -04:00
@classmethod
def get_label ( cls , shelf_type , item_category ) :
2023-01-09 02:59:59 -05:00
ic = ItemCategory ( item_category ) . label
2023-06-02 21:54:48 -04:00
st = cls . get_action_label ( shelf_type , item_category )
2023-06-05 02:28:40 -04:00
return (
_ ( " {shelf_label} 的 {item_category} " ) . format ( shelf_label = st , item_category = ic )
if st
else None
2022-12-29 14:30:31 -05:00
)
2022-12-28 01:09:55 -05:00
2022-12-29 14:30:31 -05:00
@staticmethod
2022-12-13 06:44:29 +00:00
def get_manager_for_user ( user ) :
2022-12-13 18:12:43 +00:00
return ShelfManager ( user )
2022-12-13 06:44:29 +00:00
2023-04-20 13:36:12 -04:00
def get_calendar_data ( self , max_visiblity ) :
shelf_id = self . get_shelf ( ShelfType . COMPLETE ) . pk
2023-04-21 00:31:31 -04:00
timezone_offset = timezone . localtime ( timezone . now ( ) ) . strftime ( " % z " )
timezone_offset = timezone_offset [ : len ( timezone_offset ) - 2 ]
2023-04-20 13:36:12 -04:00
calendar_data = { }
2023-04-21 00:31:31 -04:00
sql = " SELECT to_char(DATE(journal_shelfmember.created_time::timestamp AT TIME ZONE %s ), ' YYYY-MM-DD ' ) AS dat, django_content_type.model typ, COUNT(1) count FROM journal_shelfmember, catalog_item, django_content_type WHERE journal_shelfmember.item_id = catalog_item.id AND django_content_type.id = catalog_item.polymorphic_ctype_id AND parent_id = %s AND journal_shelfmember.created_time >= NOW() - INTERVAL ' 366 days ' AND journal_shelfmember.visibility <= %s GROUP BY item_id, dat, typ; "
2023-04-20 13:36:12 -04:00
with connection . cursor ( ) as cursor :
2023-04-21 00:31:31 -04:00
cursor . execute ( sql , [ timezone_offset , shelf_id , int ( max_visiblity ) ] )
2023-04-20 13:36:12 -04:00
data = cursor . fetchall ( )
for line in data :
2023-04-21 00:31:31 -04:00
date = line [ 0 ]
2023-04-20 13:36:12 -04:00
typ = line [ 1 ]
if date not in calendar_data :
calendar_data [ date ] = { " items " : [ ] }
if typ [ : 2 ] == " tv " :
typ = " movie "
elif typ == " album " :
typ = " music "
elif typ == " edition " :
typ = " book "
elif typ not in [ " book " , " movie " , " music " , " game " ] :
typ = " other "
if typ not in calendar_data [ date ] [ " items " ] :
calendar_data [ date ] [ " items " ] . append ( typ )
return calendar_data
2022-12-13 06:44:29 +00:00
2022-12-13 18:12:43 +00:00
User . shelf_manager = cached_property ( ShelfManager . get_manager_for_user )
2022-12-29 14:30:31 -05:00
User . shelf_manager . __set_name__ ( User , " shelf_manager " )
2022-12-13 06:44:29 +00:00
2022-12-11 23:20:28 +00:00
"""
Collection
"""
class CollectionMember ( ListMember ) :
2022-12-29 14:30:31 -05:00
parent = models . ForeignKey (
" Collection " , related_name = " members " , on_delete = models . CASCADE
)
2022-12-11 23:20:28 +00:00
2022-12-29 14:30:31 -05:00
note = jsondata . CharField ( _ ( " 备注 " ) , null = True , blank = True )
2022-12-28 01:09:55 -05:00
2022-12-11 23:20:28 +00:00
2022-12-29 16:20:33 -05:00
_RE_HTML_TAG = re . compile ( r " <[^>]*> " )
2023-05-20 11:01:18 -04:00
_RE_SPOILER_TAG = re . compile ( r ' <(div|span) \ sclass= " spoiler " >.*</(div|span)> ' )
2022-12-29 16:20:33 -05:00
2022-12-12 16:46:37 +00:00
class Collection ( List ) :
2022-12-29 14:30:31 -05:00
url_path = " collection "
2022-12-11 23:20:28 +00:00
MEMBER_CLASS = CollectionMember
2023-01-14 09:48:30 -05:00
catalog_item = models . OneToOneField (
CatalogCollection , on_delete = models . PROTECT , related_name = " journal_item "
)
2023-01-05 03:06:13 -05:00
title = models . CharField ( _ ( " 标题 " ) , max_length = 1000 , default = " " )
2022-12-12 16:46:37 +00:00
brief = models . TextField ( _ ( " 简介 " ) , blank = True , default = " " )
2022-12-29 14:30:31 -05:00
cover = models . ImageField (
2023-01-12 11:15:28 -05:00
upload_to = piece_cover_path , default = DEFAULT_ITEM_COVER , blank = True
2022-12-29 14:30:31 -05:00
)
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
2023-01-13 23:48:28 -05:00
featured_by_users = models . ManyToManyField (
to = User , related_name = " featured_collections " , through = " FeaturedCollection "
)
2022-12-11 23:20:28 +00:00
2022-12-28 01:09:55 -05:00
@property
def html ( self ) :
2023-01-30 17:12:25 -05:00
html = render_md ( self . brief )
2022-12-28 01:09:55 -05:00
return html
@property
2022-12-11 23:20:28 +00:00
def plain_description ( self ) :
2023-01-30 17:12:25 -05:00
html = render_md ( self . brief )
2022-12-29 16:20:33 -05:00
return _RE_HTML_TAG . sub ( " " , html )
2022-12-11 23:20:28 +00:00
2023-05-20 11:01:18 -04:00
def featured_by_user_since ( self , user ) :
f = FeaturedCollection . objects . filter ( target = self , owner = user ) . first ( )
return f . created_time if f else None
2023-01-13 23:48:28 -05:00
def get_stats_for_user ( self , user ) :
items = list ( self . members . all ( ) . values_list ( " item_id " , flat = True ) )
stats = { " total " : len ( items ) }
for st , shelf in user . shelf_manager . shelf_list . items ( ) :
stats [ st ] = shelf . members . all ( ) . filter ( item_id__in = items ) . count ( )
stats [ " percentage " ] = (
round ( stats [ " complete " ] * 100 / stats [ " total " ] ) if stats [ " total " ] else 0
)
return stats
def get_progress_for_user ( self , user ) :
items = list ( self . members . all ( ) . values_list ( " item_id " , flat = True ) )
if len ( items ) == 0 :
return 0
shelf = user . shelf_manager . shelf_list [ " complete " ]
2023-01-14 09:02:53 -05:00
return round (
shelf . members . all ( ) . filter ( item_id__in = items ) . count ( ) * 100 / len ( items )
)
2023-01-13 23:48:28 -05:00
2022-12-12 16:46:37 +00:00
def save ( self , * args , * * kwargs ) :
2022-12-29 14:30:31 -05:00
if getattr ( self , " catalog_item " , None ) is None :
2022-12-12 16:46:37 +00:00
self . catalog_item = CatalogCollection ( )
2022-12-29 14:30:31 -05:00
if (
self . catalog_item . title != self . title
or self . catalog_item . brief != self . brief
) :
2022-12-12 16:46:37 +00:00
self . catalog_item . title = self . title
self . catalog_item . brief = self . brief
2022-12-21 14:34:36 -05:00
self . catalog_item . cover = self . cover
2022-12-12 16:46:37 +00:00
self . catalog_item . save ( )
super ( ) . save ( * args , * * kwargs )
2022-12-11 23:20:28 +00:00
2023-01-14 09:02:53 -05:00
class FeaturedCollection ( Piece ) :
2023-01-13 23:48:28 -05:00
owner = models . ForeignKey ( User , on_delete = models . CASCADE )
2023-01-14 09:02:53 -05:00
target = models . ForeignKey ( Collection , on_delete = models . CASCADE )
2023-01-13 23:48:28 -05:00
created_time = models . DateTimeField ( auto_now_add = True )
edited_time = models . DateTimeField ( auto_now = True )
class Meta :
2023-01-14 09:02:53 -05:00
unique_together = [ [ " owner " , " target " ] ]
@property
def visibility ( self ) :
return self . target . visibility
@cached_property
def progress ( self ) :
return self . target . get_progress_for_user ( self . owner )
2023-01-13 23:48:28 -05:00
2022-12-11 23:20:28 +00:00
"""
Tag
"""
class TagMember ( ListMember ) :
2022-12-29 14:30:31 -05:00
parent = models . ForeignKey ( " Tag " , related_name = " members " , on_delete = models . CASCADE )
2022-12-11 23:20:28 +00:00
2023-01-12 16:08:10 -05:00
class Meta :
unique_together = [ [ " parent " , " item " ] ]
2023-01-07 12:00:09 -05:00
2022-12-11 23:20:28 +00:00
2022-12-29 14:30:31 -05:00
TagValidators = [ RegexValidator ( regex = r " \ s+ " , inverse_match = True ) ]
2022-12-11 23:20:28 +00:00
class Tag ( List ) :
2022-12-12 16:46:37 +00:00
MEMBER_CLASS = TagMember
2022-12-29 14:30:31 -05:00
items = models . ManyToManyField ( Item , through = " TagMember " )
title = models . CharField (
max_length = 100 , null = False , blank = False , validators = TagValidators
)
2022-12-11 23:20:28 +00:00
# TODO case convert and space removal on save
# TODO check on save
class Meta :
2022-12-29 16:20:33 -05:00
unique_together = [ [ " owner " , " title " ] ]
2022-12-12 16:46:37 +00:00
2022-12-29 14:30:31 -05:00
@staticmethod
2023-02-09 15:25:44 -05:00
def cleanup_title ( title , replace = True ) :
2023-06-06 21:24:06 -04:00
t = re . sub ( r " \ s+ " , " " , title . strip ( ) )
2023-02-09 15:25:44 -05:00
return " _ " if not title and replace else t
2022-12-12 16:46:37 +00:00
2023-06-06 21:24:06 -04:00
@staticmethod
def deep_cleanup_title ( title ) :
""" Remove all non-word characters, only for public index purpose """
return re . sub ( r " \ W+ " , " " , title ) . strip ( )
2022-12-15 17:29:35 -05:00
class TagManager :
2022-12-29 14:30:31 -05:00
@staticmethod
2023-06-06 21:57:13 -04:00
def indexable_tags_for_item ( item ) :
2022-12-29 14:30:31 -05:00
tags = (
item . tag_set . all ( )
. filter ( visibility = 0 )
. values ( " title " )
. annotate ( frequency = Count ( " owner " ) )
. order_by ( " -frequency " ) [ : 20 ]
)
2023-06-06 21:57:13 -04:00
tag_titles = sorted (
[
t
for t in set ( map ( lambda t : Tag . deep_cleanup_title ( t [ " title " ] ) , tags ) )
if t
]
)
2023-06-06 21:24:06 -04:00
return tag_titles
2022-12-12 16:46:37 +00:00
2022-12-29 14:30:31 -05:00
@staticmethod
2023-05-20 11:01:18 -04:00
def all_tags_for_user ( user , public_only = False ) :
2022-12-29 14:30:31 -05:00
tags = (
user . tag_set . all ( )
. values ( " title " )
. annotate ( frequency = Count ( " members__id " ) )
. order_by ( " -frequency " )
)
2023-05-20 11:01:18 -04:00
if public_only :
tags = tags . filter ( visibility = 0 )
2022-12-29 14:30:31 -05:00
return list ( map ( lambda t : t [ " title " ] , tags ) )
2022-12-12 16:46:37 +00:00
2022-12-29 14:30:31 -05:00
@staticmethod
2022-12-17 16:18:16 -05:00
def tag_item_by_user ( item , user , tag_titles , default_visibility = 0 ) :
titles = set ( [ Tag . cleanup_title ( tag_title ) for tag_title in tag_titles ] )
2022-12-29 14:30:31 -05:00
current_titles = set (
[ m . parent . title for m in TagMember . objects . filter ( owner = user , item = item ) ]
)
2022-12-17 16:18:16 -05:00
for title in titles - current_titles :
tag = Tag . objects . filter ( owner = user , title = title ) . first ( )
if not tag :
2022-12-29 14:30:31 -05:00
tag = Tag . objects . create (
owner = user , title = title , visibility = default_visibility
)
2023-06-22 08:00:50 -04:00
tag . append_item ( item , visibility = default_visibility )
2022-12-17 16:18:16 -05:00
for title in current_titles - titles :
tag = Tag . objects . filter ( owner = user , title = title ) . first ( )
2023-07-19 11:12:58 -04:00
if tag :
tag . remove_item ( item )
2022-12-17 16:18:16 -05:00
2022-12-29 14:30:31 -05:00
@staticmethod
2022-12-23 00:08:42 -05:00
def get_item_tags_by_user ( item , user ) :
2022-12-29 14:30:31 -05:00
current_titles = [
m . parent . title for m in TagMember . objects . filter ( owner = user , item = item )
]
2022-12-23 00:08:42 -05:00
return current_titles
2022-12-29 14:30:31 -05:00
@staticmethod
2022-12-15 17:29:35 -05:00
def get_manager_for_user ( user ) :
return TagManager ( user )
def __init__ ( self , user ) :
self . owner = user
2022-12-29 14:30:31 -05:00
@property
2022-12-15 17:29:35 -05:00
def all_tags ( self ) :
return TagManager . all_tags_for_user ( self . owner )
2023-05-20 11:01:18 -04:00
@property
def public_tags ( self ) :
return TagManager . all_tags_for_user ( self . owner , public_only = True )
2022-12-15 17:29:35 -05:00
def get_item_tags ( self , item ) :
2022-12-29 14:30:31 -05:00
return sorted (
[
m [ " parent__title " ]
for m in TagMember . objects . filter (
parent__owner = self . owner , item = item
) . values ( " parent__title " )
]
)
2022-12-15 17:29:35 -05:00
2023-06-06 21:57:13 -04:00
Item . tags = property ( TagManager . indexable_tags_for_item )
2022-12-15 17:29:35 -05:00
User . tags = property ( TagManager . all_tags_for_user )
User . tag_manager = cached_property ( TagManager . get_manager_for_user )
2022-12-29 14:30:31 -05:00
User . tag_manager . __set_name__ ( User , " tag_manager " )
2022-12-15 17:29:35 -05:00
class Mark :
2023-06-02 21:54:48 -04:00
"""
Holding Mark for an item on an shelf ,
which is a combo object of ShelfMember , Comment , Rating and Tags .
it mimics previous mark behaviour .
"""
2022-12-15 17:29:35 -05:00
def __init__ ( self , user , item ) :
self . owner = user
self . item = item
2022-12-29 14:30:31 -05:00
@cached_property
2022-12-15 17:29:35 -05:00
def shelfmember ( self ) :
return self . owner . shelf_manager . locate_item ( self . item )
2022-12-29 14:30:31 -05:00
@property
2022-12-15 17:29:35 -05:00
def id ( self ) :
2022-12-18 20:28:39 -05:00
return self . shelfmember . id if self . shelfmember else None
2022-12-15 17:29:35 -05:00
2023-06-02 21:54:48 -04:00
@cached_property
2022-12-23 00:08:42 -05:00
def shelf ( self ) :
return self . shelfmember . parent if self . shelfmember else None
2023-06-04 10:32:31 -04:00
@property
2022-12-15 17:29:35 -05:00
def shelf_type ( self ) :
2022-12-21 14:34:36 -05:00
return self . shelfmember . parent . shelf_type if self . shelfmember else None
2022-12-15 17:29:35 -05:00
2023-01-09 02:59:59 -05:00
@property
def action_label ( self ) :
2023-05-21 14:12:33 -04:00
if self . shelfmember :
2023-06-02 21:54:48 -04:00
return ShelfManager . get_action_label ( self . shelf_type , self . item . category )
2023-05-21 14:12:33 -04:00
if self . comment :
2023-06-02 21:54:48 -04:00
return ShelfManager . get_action_label (
2023-05-21 14:12:33 -04:00
ShelfType . PROGRESS , self . comment . item . category
)
return " "
2023-01-09 02:59:59 -05:00
2022-12-29 14:30:31 -05:00
@property
2022-12-15 17:29:35 -05:00
def shelf_label ( self ) :
2022-12-29 23:49:28 -05:00
return (
2023-06-02 21:54:48 -04:00
ShelfManager . get_label ( self . shelf_type , self . item . category )
2022-12-29 23:49:28 -05:00
if self . shelfmember
else None
)
2022-12-15 17:29:35 -05:00
2022-12-29 14:30:31 -05:00
@property
2022-12-16 01:08:10 -05:00
def created_time ( self ) :
return self . shelfmember . created_time if self . shelfmember else None
2022-12-29 14:30:31 -05:00
@property
2022-12-18 20:28:39 -05:00
def metadata ( self ) :
return self . shelfmember . metadata if self . shelfmember else None
2022-12-29 14:30:31 -05:00
@property
2022-12-15 17:29:35 -05:00
def visibility ( self ) :
2023-01-09 09:48:01 -05:00
return (
self . shelfmember . visibility
if self . shelfmember
else self . owner . get_preference ( ) . default_visibility
)
2022-12-15 17:29:35 -05:00
2022-12-29 14:30:31 -05:00
@cached_property
2022-12-15 17:29:35 -05:00
def tags ( self ) :
return self . owner . tag_manager . get_item_tags ( self . item )
2023-05-20 11:01:18 -04:00
@cached_property
def rating_grade ( self ) :
return Rating . get_item_rating_by_user ( self . item , self . owner )
2022-12-29 14:30:31 -05:00
@cached_property
2022-12-15 17:29:35 -05:00
def comment ( self ) :
2023-07-12 17:55:48 -04:00
return Comment . objects . filter ( owner = self . owner , item = self . item ) . first ( )
2022-12-15 17:29:35 -05:00
2022-12-29 14:30:31 -05:00
@property
2023-06-02 21:54:48 -04:00
def comment_text ( self ) :
return ( self . comment . text or None ) if self . comment else None
2022-12-15 17:29:35 -05:00
2023-01-31 10:24:57 -05:00
@property
def comment_html ( self ) :
return self . comment . html if self . comment else None
2022-12-29 14:30:31 -05:00
@cached_property
2022-12-15 17:29:35 -05:00
def review ( self ) :
return Review . objects . filter ( owner = self . owner , item = self . item ) . first ( )
2022-12-29 14:30:31 -05:00
def update (
self ,
shelf_type ,
comment_text ,
rating_grade ,
visibility ,
metadata = None ,
created_time = None ,
share_to_mastodon = False ,
2023-06-17 16:45:47 +02:00
silence = False ,
2022-12-29 14:30:31 -05:00
) :
2023-06-17 16:45:47 +02:00
# silence=False means update is logged.
2022-12-29 14:30:31 -05:00
share = (
share_to_mastodon
and shelf_type is not None
and (
shelf_type != self . shelf_type
2023-06-02 21:54:48 -04:00
or comment_text != self . comment_text
or rating_grade != self . rating_grade
2022-12-29 14:30:31 -05:00
)
)
2023-06-02 21:54:48 -04:00
if created_time and created_time > = timezone . now ( ) :
created_time = None
2023-01-10 22:36:13 -05:00
share_as_new_post = shelf_type != self . shelf_type
2023-05-20 11:01:18 -04:00
original_visibility = self . visibility
if shelf_type != self . shelf_type or visibility != original_visibility :
2022-12-29 14:30:31 -05:00
self . shelfmember = self . owner . shelf_manager . move_item (
2023-06-17 16:45:47 +02:00
self . item ,
shelf_type ,
visibility = visibility ,
metadata = metadata ,
silence = silence ,
2022-12-29 14:30:31 -05:00
)
2023-06-17 16:45:47 +02:00
if not silence and self . shelfmember and created_time :
2023-06-02 21:54:48 -04:00
# if it's an update(not delete) and created_time is specified,
# update the timestamp of the shelfmember and log
2023-01-10 22:36:13 -05:00
log = ShelfLogEntry . objects . filter (
owner = self . owner ,
item = self . item ,
2023-01-12 17:13:23 -05:00
timestamp = self . shelfmember . created_time ,
) . first ( )
2023-01-10 22:36:13 -05:00
self . shelfmember . created_time = created_time
self . shelfmember . save ( update_fields = [ " created_time " ] )
if log :
log . timestamp = created_time
log . save ( update_fields = [ " timestamp " ] )
else :
ShelfLogEntry . objects . create (
owner = self . owner ,
2023-01-12 17:13:23 -05:00
shelf_type = shelf_type ,
2023-01-10 22:36:13 -05:00
item = self . item ,
metadata = self . metadata ,
timestamp = created_time ,
)
2023-06-02 21:54:48 -04:00
if comment_text != self . comment_text or visibility != original_visibility :
2022-12-29 14:30:31 -05:00
self . comment = Comment . comment_item_by_user (
2023-06-02 21:54:48 -04:00
self . item ,
self . owner ,
comment_text ,
visibility ,
self . shelfmember . created_time if self . shelfmember else None ,
2022-12-29 14:30:31 -05:00
)
2023-06-02 21:54:48 -04:00
if rating_grade != self . rating_grade or visibility != original_visibility :
2022-12-18 20:28:39 -05:00
Rating . rate_item_by_user ( self . item , self . owner , rating_grade , visibility )
2023-06-02 21:54:48 -04:00
self . rating_grade = rating_grade
2022-12-23 00:08:42 -05:00
if share :
# this is a bit hacky but let's keep it until move to implement ActivityPub,
# by then, we'll just change this to boost
from mastodon . api import share_mark
2022-12-29 14:30:31 -05:00
self . shared_link = (
self . shelfmember . metadata . get ( " shared_link " )
2023-01-10 22:36:13 -05:00
if self . shelfmember . metadata and not share_as_new_post
2022-12-29 14:30:31 -05:00
else None
)
2022-12-23 00:08:42 -05:00
self . save = lambda * * args : None
2023-07-18 17:31:51 -04:00
result , code = share_mark ( self )
if not result :
if code == 401 :
raise PermissionDenied ( )
else :
raise ValueError ( code )
2022-12-29 14:30:31 -05:00
if self . shelfmember . metadata . get ( " shared_link " ) != self . shared_link :
self . shelfmember . metadata [ " shared_link " ] = self . shared_link
2022-12-23 00:08:42 -05:00
self . shelfmember . save ( )
2023-01-10 22:36:13 -05:00
elif share_as_new_post and self . shelfmember :
self . shelfmember . metadata [ " shared_link " ] = None
self . shelfmember . save ( )
2022-12-25 13:45:24 -05:00
2023-07-13 14:44:01 +02:00
def delete ( self , silence = False ) :
2023-07-17 19:00:29 -04:00
# self.logs.delete() # When deleting a mark, all logs of the mark are deleted first.
2023-07-13 14:44:01 +02:00
self . update ( None , None , None , 0 , silence = silence )
2023-01-01 01:07:32 -05:00
2023-06-17 16:45:47 +02:00
def delete_log ( self , log_id ) :
ShelfLogEntry . objects . filter (
owner = self . owner , item = self . item , id = log_id
) . delete ( )
2023-07-17 19:00:29 -04:00
def delete_all_logs ( self ) :
self . logs . delete ( )
2023-06-17 16:45:47 +02:00
2023-01-10 22:36:13 -05:00
@property
def logs ( self ) :
return ShelfLogEntry . objects . filter ( owner = self . owner , item = self . item ) . order_by (
" timestamp "
)
2023-01-01 01:07:32 -05:00
2023-05-20 11:01:18 -04:00
def reset_journal_visibility_for_user ( user : User , visibility : int ) :
2023-01-01 01:07:32 -05:00
ShelfMember . objects . filter ( owner = user ) . update ( visibility = visibility )
Comment . objects . filter ( owner = user ) . update ( visibility = visibility )
Rating . objects . filter ( owner = user ) . update ( visibility = visibility )
Review . objects . filter ( owner = user ) . update ( visibility = visibility )
2023-01-01 23:50:57 -05:00
def remove_data_by_user ( user : User ) :
ShelfMember . objects . filter ( owner = user ) . delete ( )
Comment . objects . filter ( owner = user ) . delete ( )
Rating . objects . filter ( owner = user ) . delete ( )
Review . objects . filter ( owner = user ) . delete ( )
2023-05-23 07:53:38 -04:00
TagMember . objects . filter ( owner = user ) . delete ( )
Tag . objects . filter ( owner = user ) . delete ( )
CollectionMember . objects . filter ( owner = user ) . delete ( )
Collection . objects . filter ( owner = user ) . delete ( )
FeaturedCollection . objects . filter ( owner = user ) . delete ( )
2023-01-07 00:35:30 -05:00
2023-06-06 22:20:50 -04:00
def update_journal_for_merged_item ( legacy_item_uuid , delete_duplicated = False ) :
2023-01-07 00:35:30 -05:00
legacy_item = Item . get_by_url ( legacy_item_uuid )
if not legacy_item :
_logger . error ( " update_journal_for_merged_item: unable to find item " )
return
new_item = legacy_item . merged_to_item
2023-01-23 16:31:30 -05:00
for cls in list ( Content . __subclasses__ ( ) ) + list ( ListMember . __subclasses__ ( ) ) :
2023-01-07 00:35:30 -05:00
for p in cls . objects . filter ( item = legacy_item ) :
try :
p . item = new_item
p . save ( update_fields = [ " item_id " ] )
except :
2023-06-06 22:20:50 -04:00
if delete_duplicated :
_logger . warn (
f " deleted piece { p } when merging { cls . __name__ } : { legacy_item } -> { new_item } "
)
p . delete ( )
else :
_logger . warn (
f " skip piece { p } when merging { cls . __name__ } : { legacy_item } -> { new_item } "
)
2023-05-13 14:10:25 -04:00
def journal_exists_for_item ( item ) :
for cls in list ( Content . __subclasses__ ( ) ) + list ( ListMember . __subclasses__ ( ) ) :
if cls . objects . filter ( item = item ) . exists ( ) :
2023-06-02 21:54:48 -04:00
return True
return False
2023-05-13 14:10:25 -04:00
2023-06-12 15:07:55 -04:00
Item . journal_exists = journal_exists_for_item