2023-08-10 11:27:31 -04:00
|
|
|
from functools import cached_property
|
2025-01-11 17:20:02 -05:00
|
|
|
from typing import TYPE_CHECKING
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
import django.dispatch
|
|
|
|
from django.db import models
|
|
|
|
from django.utils import timezone
|
|
|
|
|
|
|
|
from catalog.models import Item, ItemCategory
|
2023-07-20 21:59:49 -04:00
|
|
|
from users.models import APIdentity
|
2023-08-10 11:27:31 -04:00
|
|
|
|
2024-06-15 18:26:20 -04:00
|
|
|
from .common import Piece, VisibilityType
|
2023-08-10 11:27:31 -04:00
|
|
|
|
|
|
|
list_add = django.dispatch.Signal()
|
|
|
|
list_remove = django.dispatch.Signal()
|
|
|
|
|
|
|
|
|
|
|
|
class List(Piece):
|
|
|
|
"""
|
2023-07-20 21:59:49 -04:00
|
|
|
List (abstract model)
|
2023-08-10 11:27:31 -04:00
|
|
|
"""
|
|
|
|
|
2024-05-27 15:44:12 -04:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
MEMBER_CLASS: "type[ListMember]"
|
|
|
|
members: "models.QuerySet[ListMember]"
|
|
|
|
items: "models.ManyToManyField[Item, List]"
|
2023-07-20 21:59:49 -04:00
|
|
|
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
2023-08-10 11:27:31 -04:00
|
|
|
visibility = models.PositiveSmallIntegerField(
|
2024-06-15 18:26:20 -04:00
|
|
|
choices=VisibilityType.choices, default=0, null=False
|
|
|
|
) # type:ignore
|
2023-07-20 21:59:49 -04:00
|
|
|
created_time = models.DateTimeField(default=timezone.now)
|
2023-11-20 19:31:31 -05:00
|
|
|
edited_time = models.DateTimeField(auto_now=True)
|
2023-08-10 11:27:31 -04:00
|
|
|
metadata = models.JSONField(default=dict)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
|
|
|
# MEMBER_CLASS = None # subclass must override this
|
|
|
|
# subclass must add this:
|
|
|
|
# items = models.ManyToManyField(Item, through='ListMember')
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ordered_members(self):
|
|
|
|
return self.members.all().order_by("position")
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ordered_items(self):
|
|
|
|
return self.items.all().order_by(
|
|
|
|
self.MEMBER_CLASS.__name__.lower() + "__position"
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def recent_items(self):
|
|
|
|
return self.items.all().order_by(
|
|
|
|
"-" + self.MEMBER_CLASS.__name__.lower() + "__created_time"
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def recent_members(self):
|
|
|
|
return self.members.all().order_by("-created_time")
|
|
|
|
|
|
|
|
def get_member_for_item(self, item):
|
|
|
|
return self.members.filter(item=item).first()
|
|
|
|
|
|
|
|
def get_summary(self):
|
|
|
|
summary = {k: 0 for k in ItemCategory.values}
|
|
|
|
for c in self.recent_items:
|
|
|
|
summary[c.category] += 1
|
|
|
|
return summary
|
|
|
|
|
|
|
|
def append_item(self, item, **params):
|
|
|
|
"""
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
if item is None:
|
2024-05-31 11:15:10 -04:00
|
|
|
raise ValueError("item is None")
|
2023-08-10 11:27:31 -04:00
|
|
|
member = self.get_member_for_item(item)
|
|
|
|
if member:
|
|
|
|
return member
|
|
|
|
ml = self.ordered_members
|
|
|
|
p = {"parent": self}
|
|
|
|
p.update(params)
|
2024-05-27 15:44:12 -04:00
|
|
|
lm = ml.last()
|
2023-08-10 11:27:31 -04:00
|
|
|
member = self.MEMBER_CLASS.objects.create(
|
|
|
|
owner=self.owner,
|
2024-05-27 15:44:12 -04:00
|
|
|
position=lm.position + 1 if lm else 1,
|
2023-08-10 11:27:31 -04:00
|
|
|
item=item,
|
|
|
|
**p,
|
|
|
|
)
|
|
|
|
list_add.send(sender=self.__class__, instance=self, item=item, member=member)
|
|
|
|
return member
|
|
|
|
|
|
|
|
def remove_item(self, item):
|
|
|
|
member = self.get_member_for_item(item)
|
|
|
|
if member:
|
|
|
|
list_remove.send(
|
|
|
|
sender=self.__class__, instance=self, item=item, member=member
|
|
|
|
)
|
|
|
|
member.delete()
|
|
|
|
|
|
|
|
def update_member_order(self, ordered_member_ids):
|
|
|
|
for m in self.members.all():
|
|
|
|
try:
|
2024-05-27 15:44:12 -04:00
|
|
|
i = ordered_member_ids.index(m.pk)
|
2023-08-10 11:27:31 -04:00
|
|
|
if m.position != i + 1:
|
|
|
|
m.position = i + 1
|
|
|
|
m.save()
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def move_up_item(self, item):
|
|
|
|
members = self.ordered_members
|
|
|
|
member = self.get_member_for_item(item)
|
|
|
|
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 = self.get_member_for_item(item)
|
|
|
|
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()
|
|
|
|
|
|
|
|
def update_item_metadata(self, item, metadata):
|
|
|
|
member = self.get_member_for_item(item)
|
|
|
|
if member:
|
|
|
|
member.metadata = metadata
|
|
|
|
member.save()
|
|
|
|
|
|
|
|
|
|
|
|
class ListMember(Piece):
|
|
|
|
"""
|
|
|
|
ListMember - List class's member class
|
|
|
|
It's an abstract class, subclass must add this:
|
|
|
|
|
|
|
|
parent = models.ForeignKey('List', related_name='members', on_delete=models.CASCADE)
|
|
|
|
"""
|
|
|
|
|
2024-05-27 15:44:12 -04:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
parent: models.ForeignKey["ListMember", "List"]
|
2023-07-20 21:59:49 -04:00
|
|
|
owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT)
|
2023-08-10 11:27:31 -04:00
|
|
|
visibility = models.PositiveSmallIntegerField(
|
2024-06-15 18:26:20 -04:00
|
|
|
choices=VisibilityType.choices, default=0, null=False
|
|
|
|
) # type:ignore
|
2023-08-10 11:27:31 -04:00
|
|
|
created_time = models.DateTimeField(default=timezone.now)
|
2023-11-20 19:31:31 -05:00
|
|
|
edited_time = models.DateTimeField(auto_now=True)
|
2023-08-10 11:27:31 -04:00
|
|
|
metadata = models.JSONField(default=dict)
|
|
|
|
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
|
|
|
position = models.PositiveIntegerField()
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
def mark(self):
|
|
|
|
from .mark import Mark
|
|
|
|
|
|
|
|
m = Mark(self.owner, self.item)
|
|
|
|
return m
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
|
|
|
def __str__(self):
|
2024-05-14 10:54:49 -04:00
|
|
|
return f"{self.__class__.__name__}:{self.pk}[{self.parent}]:{self.item}"
|