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 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
|
2022-12-14 21:12:37 -05:00
|
|
|
from django.db.models import Count, Avg
|
2022-12-13 18:12:43 +00:00
|
|
|
import django.dispatch
|
2022-12-14 21:12:37 -05:00
|
|
|
import math
|
2022-12-17 16:18:16 -05:00
|
|
|
import uuid
|
2022-12-21 14:34:36 -05:00
|
|
|
from catalog.common.utils import DEFAULT_ITEM_COVER, item_cover_path
|
|
|
|
from django.utils.baseconv import base62
|
2022-12-11 23:20:28 +00:00
|
|
|
|
|
|
|
|
2022-12-13 06:44:29 +00:00
|
|
|
class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
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
|
|
|
owner = models.ForeignKey(User, on_delete=models.PROTECT)
|
|
|
|
visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only
|
2022-12-19 19:08:41 -05:00
|
|
|
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-13 06:44:29 +00:00
|
|
|
metadata = models.JSONField(default=dict)
|
|
|
|
attached_to = models.ForeignKey(User, null=True, default=None, on_delete=models.SET_NULL, related_name="attached_with")
|
|
|
|
|
2022-12-21 14:34:36 -05:00
|
|
|
@property
|
|
|
|
def uuid(self):
|
|
|
|
return base62.encode(self.uid.int)
|
|
|
|
|
2022-12-13 06:44:29 +00:00
|
|
|
|
2022-12-15 17:29:35 -05:00
|
|
|
class Content(Piece):
|
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-13 06:44:29 +00:00
|
|
|
return f"{self.id}({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):
|
|
|
|
target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name='likes')
|
|
|
|
|
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-20 11:32:44 -05:00
|
|
|
|
2022-12-11 23:20:28 +00:00
|
|
|
class Note(Content):
|
|
|
|
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:
|
|
|
|
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-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-15 17:29:35 -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-19 17:50:00 -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):
|
|
|
|
grade = models.PositiveSmallIntegerField(default=0, validators=[MaxValueValidator(10), MinValueValidator(1)], null=True)
|
2022-12-11 23:20:28 +00:00
|
|
|
|
2022-12-14 21:12:37 -05:00
|
|
|
@staticmethod
|
|
|
|
def get_rating_for_item(item):
|
2022-12-15 17:29:35 -05:00
|
|
|
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(average=Avg('grade'), count=Count('item'))
|
|
|
|
return math.ceil(stat['average']) if stat['count'] >= 5 else None
|
2022-12-14 21:12:37 -05:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_rating_count_for_item(item):
|
2022-12-15 17:29:35 -05:00
|
|
|
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(count=Count('item'))
|
2022-12-14 21:12:37 -05:00
|
|
|
return stat['count']
|
|
|
|
|
2022-12-15 17:29:35 -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-15 17:29:35 -05:00
|
|
|
raise ValueError(f'Invalid rating grade: {rating_grade}')
|
|
|
|
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-15 17:29:35 -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
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
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-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)
|
2022-12-14 21:12:37 -05:00
|
|
|
|
|
|
|
|
2022-12-20 11:32:44 -05:00
|
|
|
class Reply(Piece):
|
|
|
|
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()
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
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-11 23:20:28 +00:00
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
2022-12-12 16:46:37 +00:00
|
|
|
_owner = models.ForeignKey(User, on_delete=models.PROTECT) # duplicated owner field to make unique key possible for subclasses
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
self._owner = self.owner
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
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')
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ordered_members(self):
|
|
|
|
return self.members.all().order_by('position', 'item_id')
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ordered_items(self):
|
2022-12-12 16:46:37 +00:00
|
|
|
return self.items.all().order_by(self.MEMBER_CLASS.__name__.lower() + '__position')
|
2022-12-11 23:20:28 +00:00
|
|
|
|
|
|
|
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
|
2022-12-21 14:34:36 -05:00
|
|
|
p = {'parent': self}
|
2022-12-11 23:20:28 +00:00
|
|
|
p.update(params)
|
2022-12-13 18:12:43 +00: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)
|
|
|
|
return member
|
2022-12-11 23:20:28 +00:00
|
|
|
|
|
|
|
def remove_item(self, item):
|
|
|
|
member = self.members.all().filter(item=item).first()
|
|
|
|
if member:
|
2022-12-17 16:18:16 -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
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
_list = models.ForeignKey('ListClass', related_name='members', on_delete=models.CASCADE)
|
|
|
|
|
|
|
|
it starts with _ bc Django internally created OneToOne Field on Piece
|
|
|
|
https://docs.djangoproject.com/en/3.2/topics/db/models/#specifying-the-parent-link-field
|
|
|
|
"""
|
2022-12-11 23:20:28 +00:00
|
|
|
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
|
|
|
position = models.PositiveIntegerField()
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
2022-12-13 18:12:43 +00:00
|
|
|
def __str__(self):
|
|
|
|
return f'{self.id}:{self.position} ({self.item})'
|
|
|
|
|
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-17 16:18:16 -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-17 16:18:16 -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, _('玩过')],
|
|
|
|
[ItemCategory.Collection, ShelfType.WISHLIST, _('关注')],
|
2022-12-11 23:20:28 +00:00
|
|
|
# TODO add more combinations
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2022-12-13 18:12:43 +00:00
|
|
|
class ShelfMember(ListMember):
|
2022-12-21 14:34:36 -05:00
|
|
|
parent = models.ForeignKey('Shelf', related_name='members', on_delete=models.CASCADE)
|
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-13 18:12:43 +00:00
|
|
|
unique_together = [['_owner', 'item_category', 'shelf_type']]
|
2022-12-11 23:20:28 +00:00
|
|
|
|
2022-12-13 18:12:43 +00:00
|
|
|
MEMBER_CLASS = ShelfMember
|
|
|
|
items = models.ManyToManyField(Item, through='ShelfMember', related_name="+")
|
2022-12-11 23:20:28 +00:00
|
|
|
item_category = models.CharField(choices=ItemCategory.choices, max_length=100, null=False, blank=False)
|
2022-12-13 18:12:43 +00:00
|
|
|
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):
|
|
|
|
return f'{self.id} {self.title}'
|
|
|
|
|
|
|
|
@cached_property
|
2022-12-15 17:29:35 -05:00
|
|
|
def shelf_label(self):
|
2022-12-13 18:12:43 +00:00
|
|
|
return next(iter([n[2] for n in iter(ShelfTypeNames) if n[0] == self.item_category and n[1] == self.shelf_type]), self.shelf_type)
|
2022-12-11 23:20:28 +00:00
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def title(self):
|
2022-12-15 17:29:35 -05:00
|
|
|
q = _("{item_category} {shelf_label} list").format(shelf_label=self.shelf_label, item_category=self.item_category)
|
2022-12-13 18:12:43 +00:00
|
|
|
return _("{user}'s {shelf_name}").format(user=self.owner.mastodon_username, shelf_name=q)
|
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)
|
2022-12-18 20:28:39 -05:00
|
|
|
shelf = models.ForeignKey(Shelf, on_delete=models.CASCADE, related_name='entries', null=True) # None means removed from any shelf
|
2022-12-11 23:20:28 +00:00
|
|
|
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
2022-12-13 18:12:43 +00: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-13 18:12:43 +00: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
|
|
|
|
|
|
|
|
def initialize(self):
|
|
|
|
for ic in ItemCategory:
|
2022-12-13 18:12:43 +00:00
|
|
|
for qt in ShelfType:
|
|
|
|
Shelf.objects.create(owner=self.owner, item_category=ic, shelf_type=qt)
|
2022-12-11 23:20:28 +00:00
|
|
|
|
2022-12-13 18:12:43 +00:00
|
|
|
def _shelf_member_for_item(self, item):
|
2022-12-21 14:34:36 -05:00
|
|
|
return ShelfMember.objects.filter(item=item, parent__in=self.owner.shelf_set.all()).first()
|
2022-12-11 23:20:28 +00:00
|
|
|
|
2022-12-13 18:12:43 +00:00
|
|
|
def _shelf_for_item_and_type(item, shelf_type):
|
|
|
|
if not item or not shelf_type:
|
2022-12-11 23:20:28 +00:00
|
|
|
return None
|
2022-12-13 18:12:43 +00:00
|
|
|
return self.owner.shelf_set.all().filter(item_category=item.category, shelf_type=shelf_type)
|
2022-12-11 23:20:28 +00:00
|
|
|
|
2022-12-15 17:29:35 -05:00
|
|
|
def locate_item(self, item):
|
|
|
|
member = ShelfMember.objects.filter(owner=self.owner, item=item).first()
|
2022-12-21 14:34:36 -05:00
|
|
|
return member # .parent if member else None
|
2022-12-15 17:29:35 -05: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:
|
|
|
|
raise ValueError('empty item')
|
2022-12-17 16:18:16 -05:00
|
|
|
new_shelfmember = None
|
2022-12-16 01:08:10 -05:00
|
|
|
last_shelfmember = self._shelf_member_for_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-13 18:12:43 +00:00
|
|
|
shelf = self.get_shelf(item.category, 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-17 16:18:16 -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:
|
|
|
|
raise ValueError('empty shelf')
|
|
|
|
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 {}
|
|
|
|
ShelfLogEntry.objects.create(owner=self.owner, shelf=shelf, item=item, metadata=metadata)
|
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-13 18:12:43 +00: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-13 18:12:43 +00:00
|
|
|
return ShelfLogEntry.objects.filter(owner=self.owner, item=item).order_by('timestamp')
|
2022-12-11 23:20:28 +00:00
|
|
|
|
2022-12-13 18:12:43 +00:00
|
|
|
def get_shelf(self, item_category, shelf_type):
|
|
|
|
return self.owner.shelf_set.all().filter(item_category=item_category, shelf_type=shelf_type).first()
|
2022-12-11 23:20:28 +00:00
|
|
|
|
2022-12-13 06:44:29 +00:00
|
|
|
@staticmethod
|
|
|
|
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)
|
|
|
|
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-21 14:34:36 -05:00
|
|
|
parent = models.ForeignKey('Collection', related_name='members', on_delete=models.CASCADE)
|
2022-12-11 23:20:28 +00:00
|
|
|
|
|
|
|
|
2022-12-12 16:46:37 +00:00
|
|
|
class Collection(List):
|
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)
|
|
|
|
title = models.CharField(_("title in primary language"), max_length=1000, default="")
|
|
|
|
brief = models.TextField(_("简介"), blank=True, default="")
|
2022-12-21 14:34:36 -05:00
|
|
|
cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True)
|
2022-12-11 23:20:28 +00: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
|
|
|
|
|
|
|
|
@property
|
|
|
|
def plain_description(self):
|
|
|
|
html = markdown(self.description)
|
|
|
|
return RE_HTML_TAG.sub(' ', html)
|
|
|
|
|
2022-12-12 16:46:37 +00:00
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
if getattr(self, 'catalog_item', None) is None:
|
|
|
|
self.catalog_item = CatalogCollection()
|
|
|
|
if self.catalog_item.title != self.title or self.catalog_item.brief != self.brief:
|
|
|
|
self.catalog_item.title = self.title
|
|
|
|
self.catalog_item.brief = self.brief
|
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-21 14:34:36 -05:00
|
|
|
parent = models.ForeignKey('Tag', related_name='members', on_delete=models.CASCADE)
|
2022-12-11 23:20:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
TagValidators = [RegexValidator(regex=r'\s+', inverse_match=True)]
|
|
|
|
|
|
|
|
|
|
|
|
class Tag(List):
|
2022-12-12 16:46:37 +00:00
|
|
|
MEMBER_CLASS = TagMember
|
|
|
|
items = models.ManyToManyField(Item, through='TagMember')
|
2022-12-11 23:20:28 +00:00
|
|
|
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:
|
2022-12-12 16:46:37 +00:00
|
|
|
unique_together = [['_owner', 'title']]
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def cleanup_title(title):
|
|
|
|
return title.strip().lower()
|
|
|
|
|
2022-12-15 17:29:35 -05:00
|
|
|
|
|
|
|
class TagManager:
|
2022-12-12 16:46:37 +00:00
|
|
|
@staticmethod
|
|
|
|
def public_tags_for_item(item):
|
|
|
|
tags = item.tag_set.all().filter(visibility=0).values('title').annotate(frequency=Count('owner')).order_by('-frequency')
|
2022-12-17 17:18:08 -05:00
|
|
|
return sorted(list(map(lambda t: t['title'], tags)))
|
2022-12-12 16:46:37 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def all_tags_for_user(user):
|
|
|
|
tags = user.tag_set.all().values('title').annotate(frequency=Count('members')).order_by('-frequency')
|
2022-12-17 17:18:08 -05:00
|
|
|
return sorted(list(map(lambda t: t['title'], tags)))
|
2022-12-12 16:46:37 +00:00
|
|
|
|
2022-12-17 16:18:16 -05:00
|
|
|
@staticmethod
|
|
|
|
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-21 14:34:36 -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:
|
|
|
|
tag = Tag.objects.create(owner=user, title=title, visibility=default_visibility)
|
|
|
|
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-12 16:46:37 +00:00
|
|
|
@staticmethod
|
|
|
|
def add_tag_by_user(item, tag_title, user, default_visibility=0):
|
|
|
|
title = Tag.cleanup_title(tag_title)
|
|
|
|
tag = Tag.objects.filter(owner=user, title=title).first()
|
|
|
|
if not tag:
|
|
|
|
tag = Tag.objects.create(owner=user, title=title, visibility=default_visibility)
|
|
|
|
tag.append_item(item)
|
|
|
|
|
2022-12-15 17:29:35 -05:00
|
|
|
@staticmethod
|
|
|
|
def get_manager_for_user(user):
|
|
|
|
return TagManager(user)
|
|
|
|
|
|
|
|
def __init__(self, user):
|
|
|
|
self.owner = user
|
|
|
|
|
2022-12-21 14:34:36 -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-21 14:34:36 -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)
|
|
|
|
User.tag_manager.__set_name__(User, 'tag_manager')
|
|
|
|
|
|
|
|
|
|
|
|
class Mark:
|
|
|
|
""" this mimics previous mark behaviour """
|
|
|
|
|
|
|
|
def __init__(self, user, item):
|
|
|
|
self.owner = user
|
|
|
|
self.item = item
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def shelfmember(self):
|
|
|
|
return self.owner.shelf_manager.locate_item(self.item)
|
|
|
|
|
|
|
|
@property
|
|
|
|
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
|
|
|
|
|
|
|
@property
|
|
|
|
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
|
|
|
|
|
|
|
@property
|
|
|
|
def shelf_label(self):
|
2022-12-21 14:34:36 -05:00
|
|
|
return self.shelfmember.parent.shelf_label if self.shelfmember else None
|
2022-12-15 17:29:35 -05:00
|
|
|
|
2022-12-16 01:08:10 -05:00
|
|
|
@property
|
|
|
|
def created_time(self):
|
|
|
|
return self.shelfmember.created_time if self.shelfmember else None
|
|
|
|
|
2022-12-18 20:28:39 -05:00
|
|
|
@property
|
|
|
|
def metadata(self):
|
|
|
|
return self.shelfmember.metadata if self.shelfmember else None
|
|
|
|
|
2022-12-15 17:29:35 -05:00
|
|
|
@property
|
|
|
|
def visibility(self):
|
|
|
|
return self.shelfmember.visibility if self.shelfmember else None
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def tags(self):
|
|
|
|
return self.owner.tag_manager.get_item_tags(self.item)
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def rating(self):
|
|
|
|
return Rating.get_item_rating_by_user(self.item, self.owner)
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def comment(self):
|
|
|
|
return Comment.objects.filter(owner=self.owner, item=self.item).first()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def text(self):
|
|
|
|
return self.comment.text if self.comment else None
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def review(self):
|
|
|
|
return Review.objects.filter(owner=self.owner, item=self.item).first()
|
|
|
|
|
2022-12-17 16:18:16 -05:00
|
|
|
def update(self, shelf_type, comment_text, rating_grade, visibility, metadata=None, created_time=None):
|
2022-12-15 17:29:35 -05:00
|
|
|
if shelf_type != self.shelf_type or visibility != self.visibility:
|
2022-12-18 20:28:39 -05:00
|
|
|
self.shelfmember = self.owner.shelf_manager.move_item(self.item, shelf_type, visibility=visibility, metadata=metadata)
|
|
|
|
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:
|
|
|
|
self.comment = Comment.comment_item_by_user(self.item, self.owner, comment_text, visibility)
|
|
|
|
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
|