lib.itmens/journal/models.py

981 lines
31 KiB
Python
Raw Normal View History

2022-12-11 23:20:28 +00:00
from django.db import models
from polymorphic.models import PolymorphicModel
from users.models import User
2022-12-13 18:12:43 +00:00
from catalog.common.models import Item, ItemCategory
from .mixins import UserOwnedObjectMixin
2022-12-12 16:46:37 +00:00
from catalog.collection.models import Collection as CatalogCollection
2022-12-11 23:20:28 +00:00
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
from django.db.models import Count, Avg
2022-12-29 16:20:33 -05:00
from django.contrib.contenttypes.models import ContentType
2022-12-13 18:12:43 +00:00
import django.dispatch
2022-12-17 16:18:16 -05:00
import uuid
2022-12-29 16:20:33 -05:00
import re
2023-01-12 11:15:28 -05:00
from catalog.common.utils import DEFAULT_ITEM_COVER, piece_cover_path
2022-12-21 14:34:36 -05:00
from django.utils.baseconv import base62
2022-12-23 00:08:42 -05:00
from django.db.models import Q
2022-12-27 14:52:03 -05:00
from catalog.models import *
from django.contrib.contenttypes.models import ContentType
2022-12-28 01:09:55 -05:00
from markdown import markdown
2022-12-29 14:30:31 -05:00
from catalog.common import jsondata
2022-12-23 00:08:42 -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=[])
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)
2022-12-23 00:08:42 -05:00
def query_visible(user):
2022-12-29 14:30:31 -05:00
return (
Q(visibility=0)
| Q(owner_id__in=user.following if user.is_authenticated else [], visibility=1)
2022-12-29 14:30:31 -05:00
| Q(owner_id=user.id)
)
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):
2022-12-28 10:24:07 -05:00
classes = all_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
2022-12-28 10:24:07 -05:00
ct = all_content_types()
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):
2022-12-29 14:30:31 -05:00
url_path = "piece" # 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):
return (settings.APP_WEBSITE + self.url) if self.url_path else None
@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
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)
item = models.ForeignKey(Item, on_delete=models.PROTECT)
2022-12-11 23:20:28 +00:00
2022-12-27 14:52:03 -05:00
@cached_property
def mark(self):
m = Mark(self.owner, self.item)
m.review = self
return m
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
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-29 16:20:33 -05:00
@staticmethod
def user_liked_piece(user, piece):
return Like.objects.filter(owner=user, target=piece).first()
2022-12-21 14:34:36 -05:00
@staticmethod
def user_like_piece(user, piece):
if not piece or piece.__class__ not in [Collection]:
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-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)
@staticmethod
def comment_item_by_user(item, user, text, visibility=0):
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(
owner=user, item=item, text=text, visibility=visibility
)
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
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-07 12:00:09 -05:00
return markdown(self.body)
2022-12-24 01:28:24 -05:00
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)
2022-12-29 14:30:31 -05:00
@staticmethod
2022-12-19 17:50:00 -05:00
def review_item_by_user(item, user, title, body, metadata={}, visibility=0):
2022-12-15 17:29:35 -05:00
# allow multiple reviews per item per user.
2022-12-29 14:30:31 -05:00
review = Review.objects.create(
owner=user,
item=item,
title=title,
body=body,
metadata=metadata,
visibility=visibility,
)
2022-12-15 17:29:35 -05:00
"""
review = Review.objects.filter(owner=user, item=item).first()
if title is None:
if review is not None:
review.delete()
review = None
elif review is None:
review = Review.objects.create(owner=user, item=item, title=title, body=body, visibility=visibility)
else:
review.title = title
review.body = body
review.visibility = visibility
review.save()
"""
return review
2022-12-11 23:20:28 +00:00
2022-12-15 17:29:35 -05:00
class Rating(Content):
2023-01-07 12:00:09 -05:00
# class Meta:
# unique_together = [["owner", "item"]]
# FIXME enable after migration
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
def get_rating_for_item(item):
2022-12-29 14:30:31 -05:00
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(
average=Avg("grade"), count=Count("item")
)
return stat["average"] if stat["count"] >= 5 else None
2022-12-29 14:30:31 -05:00
@staticmethod
def get_rating_count_for_item(item):
2022-12-29 14:30:31 -05:00
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(
count=Count("item")
)
return stat["count"]
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()
return rating.grade if rating else None
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)
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 23:49:28 -05:00
def get_members_in_category(self, item_category):
return self.members.all().filter(query_item_category(item_category))
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
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
"""
2022-12-29 14:30:31 -05:00
if item is None or self.get_member_for_item(item):
2022-12-11 23:20:28 +00:00
return None
else:
ml = self.ordered_members
2022-12-29 14:30:31 -05:00
p = {"parent": self}
2022-12-11 23:20:28 +00:00
p.update(params)
2022-12-29 14:30:31 -05:00
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
)
2022-12-13 18:12:43 +00:00
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()
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, _("玩过")],
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-07 12:00:09 -05:00
# class Meta:
# unique_together = [["parent", "item"]]
# FIXME enable after migration
2023-01-01 01:07:32 -05:00
@cached_property
def mark(self):
m = Mark(self.owner, self.item)
m.shelfmember = self
return m
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)
2022-12-29 14:30:31 -05:00
timestamp = models.DateTimeField(
default=timezone.now
) # 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):
2022-12-29 14:30:31 -05:00
return f"{self.owner}:{self.shelf}:{self.item}:{self.metadata}"
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
2022-12-29 23:49:28 -05:00
def locate_item(self, item) -> ShelfMember:
2022-12-29 14:30:31 -05:00
return ShelfMember.objects.filter(
2022-12-29 23:49:28 -05:00
item=item, parent__in=list(self.shelf_list.values())
2022-12-29 14:30:31 -05:00
).first()
2022-12-11 23:20:28 +00:00
2022-12-13 18:12:43 +00:00
def move_item(self, item, shelf_type, visibility=0, metadata=None):
# shelf_type=None means remove from current shelf
# metadata=None means no change
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()
if changed:
if metadata is None:
metadata = last_metadata or {}
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,
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
2022-12-29 23:49:28 -05:00
def get_members(self, shelf_type, item_category):
return self.shelf_list[shelf_type].get_members_in_category(item_category)
# 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-09 02:59:59 -05:00
def get_action_label(self, 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
def get_label(self, shelf_type, item_category):
ic = ItemCategory(item_category).label
st = self.get_action_label(shelf_type, item_category)
2022-12-29 23:49:28 -05:00
return _("{shelf_label}{item_category}").format(
shelf_label=st, item_category=ic
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
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"<[^>]*>")
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
2022-12-12 16:46:37 +00:00
catalog_item = models.OneToOneField(CatalogCollection, on_delete=models.PROTECT)
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
2022-12-11 23:20:28 +00:00
2022-12-28 01:09:55 -05:00
@property
def html(self):
html = markdown(self.brief)
return html
@property
2022-12-11 23:20:28 +00:00
def plain_description(self):
2022-12-28 01:09:55 -05:00
html = markdown(self.brief)
2022-12-29 16:20:33 -05:00
return _RE_HTML_TAG.sub(" ", html)
2022-12-11 23:20:28 +00: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
"""
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-07 12:00:09 -05:00
# class Meta:
# unique_together = [["parent", "item"]]
# FIXME enable after migration
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
2022-12-12 16:46:37 +00:00
def cleanup_title(title):
return title.strip().lower()
2022-12-15 17:29:35 -05:00
class TagManager:
2022-12-29 14:30:31 -05:00
@staticmethod
2022-12-12 16:46:37 +00:00
def public_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]
)
return sorted(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-12 16:46:37 +00:00
def all_tags_for_user(user):
2022-12-29 14:30:31 -05:00
tags = (
user.tag_set.all()
.values("title")
.annotate(frequency=Count("members__id"))
.order_by("-frequency")
)
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
)
2022-12-17 16:18:16 -05:00
tag.append_item(item)
for title in current_titles - titles:
tag = Tag.objects.filter(owner=user, title=title).first()
tag.remove_item(item)
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-12 16:46:37 +00:00
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:
2022-12-29 14:30:31 -05:00
tag = Tag.objects.create(
owner=user, title=title, visibility=default_visibility
)
2022-12-12 16:46:37 +00:00
tag.append_item(item)
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)
def add_item_tags(self, item, tags, visibility=0):
for tag in tags:
TagManager.add_tag_by_user(item, tag, self.owner, visibility)
2022-12-12 16:46:37 +00:00
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
Item.tags = property(TagManager.public_tags_for_item)
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:
2022-12-29 14:30:31 -05:00
"""this 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
2022-12-29 14:30:31 -05:00
@property
2022-12-23 00:08:42 -05:00
def shelf(self):
return self.shelfmember.parent if self.shelfmember else None
2022-12-29 14:30:31 -05: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):
return (
self.owner.shelf_manager.get_action_label(
self.shelf_type, self.item.category
)
if self.shelfmember
else None
)
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-01-09 02:59:59 -05:00
self.owner.shelf_manager.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)
2022-12-29 14:30:31 -05:00
@cached_property
2022-12-15 17:29:35 -05:00
def rating(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):
return Comment.objects.filter(owner=self.owner, item=self.item).first()
2022-12-29 14:30:31 -05:00
@property
2022-12-15 17:29:35 -05:00
def text(self):
return self.comment.text 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,
):
share = (
share_to_mastodon
and shelf_type is not None
and (
shelf_type != self.shelf_type
or comment_text != self.text
or rating_grade != self.rating
)
)
2022-12-15 17:29:35 -05:00
if shelf_type != self.shelf_type or visibility != self.visibility:
2022-12-29 14:30:31 -05:00
self.shelfmember = self.owner.shelf_manager.move_item(
self.item, shelf_type, visibility=visibility, metadata=metadata
)
2022-12-18 20:28:39 -05:00
if self.shelfmember and created_time:
self.shelfmember.created_time = created_time
2022-12-17 16:18:16 -05:00
self.shelfmember.save()
2022-12-15 17:29:35 -05:00
if comment_text != self.text or visibility != self.visibility:
2022-12-29 14:30:31 -05:00
self.comment = Comment.comment_item_by_user(
self.item, self.owner, comment_text, visibility
)
2022-12-15 17:29:35 -05:00
if rating_grade != self.rating or visibility != self.visibility:
2022-12-18 20:28:39 -05:00
Rating.rate_item_by_user(self.item, self.owner, rating_grade, visibility)
2022-12-15 17:29:35 -05:00
self.rating = 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")
if self.shelfmember.metadata
else None
)
2023-01-09 09:43:35 -05:00
self.translated_status = self.action_label
2022-12-23 00:08:42 -05:00
self.save = lambda **args: None
if not share_mark(self):
raise ValueError("sharing failed")
if not self.shelfmember.metadata:
self.shelfmember.metadata = {}
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()
2022-12-25 13:45:24 -05:00
def delete(self):
self.update(None, None, None, 0)
2023-01-01 01:07:32 -05:00
def reset_visibility_for_user(user: User, visibility: int):
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)
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()
def update_journal_for_merged_item(legacy_item_uuid):
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
for cls in Content.__subclasses__ + ListMember.__subclasses__:
_logger.info(f"update {cls.__name__}: {legacy_item} -> {new_item}")
for p in cls.objects.filter(item=legacy_item):
try:
p.item = new_item
p.save(update_fields=["item_id"])
except:
_logger.info(f"delete duplicated piece {p}")
p.delete()