""" make a list of books!! """ import uuid from django.apps import apps from django.core.exceptions import PermissionDenied from django.db import models from django.db.models import Q from django.utils import timezone from bookwyrm import activitypub from bookwyrm.settings import DOMAIN from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin from .base_model import BookWyrmModel from .group import GroupMember from . import fields CurationType = models.TextChoices( "Curation", ["closed", "open", "curated", "group"], ) class List(OrderedCollectionMixin, BookWyrmModel): """a list of books""" name = fields.CharField(max_length=100) user = fields.ForeignKey( "User", on_delete=models.PROTECT, activitypub_field="owner" ) description = fields.TextField(blank=True, null=True, activitypub_field="summary") privacy = fields.PrivacyField() curation = fields.CharField( max_length=255, default="closed", choices=CurationType.choices ) group = models.ForeignKey( "Group", on_delete=models.SET_NULL, default=None, blank=True, null=True, ) books = models.ManyToManyField( "Edition", symmetrical=False, through="ListItem", through_fields=("book_list", "book"), ) embed_key = models.UUIDField(unique=True, null=True, editable=False) activity_serializer = activitypub.BookList def get_remote_id(self): """don't want the user to be in there in this case""" return f"https://{DOMAIN}/list/{self.id}" @property def collection_queryset(self): """list of books for this shelf, overrides OrderedCollectionMixin""" return self.books.filter(listitem__approved=True).order_by("listitem") class Meta: """default sorting""" ordering = ("-updated_date",) def raise_not_editable(self, viewer): """the associated user OR the list owner can edit""" if self.user == viewer: return # group members can edit items in group lists is_group_member = GroupMember.objects.filter( group=self.group, user=viewer ).exists() if is_group_member: return super().raise_not_editable(viewer) def raise_not_submittable(self, viewer): """can the user submit a book to the list?""" # if you can't view the list you can't submit to it self.raise_visible_to_user(viewer) # all good if you're the owner or the list is open if self.user == viewer or self.curation in ["open", "curated"]: return if self.curation == "group": is_group_member = GroupMember.objects.filter( group=self.group, user=viewer ).exists() if is_group_member: return raise PermissionDenied() @classmethod def followers_filter(cls, queryset, viewer): """Override filter for "followers" privacy level to allow non-following group members to see the existence of group lists""" return queryset.exclude( ~Q( # user isn't following or group member Q(user__followers=viewer) | Q(user=viewer) | Q(group__memberships__user=viewer) ), privacy="followers", # and the status (of the list) is followers only ) @classmethod def direct_filter(cls, queryset, viewer): """Override filter for "direct" privacy level to allow group members to see the existence of group lists""" return queryset.exclude( ~Q( # user not self and not in the group if this is a group list Q(user=viewer) | Q(group__memberships__user=viewer) ), privacy="direct", ) @classmethod def remove_from_group(cls, owner, user): """remove a list from a group""" cls.objects.filter(group__user=owner, user=user).all().update( group=None, curation="closed" ) def save(self, *args, **kwargs): """on save, update embed_key and avoid clash with existing code""" if not self.embed_key: self.embed_key = uuid.uuid4() return super().save(*args, **kwargs) class ListItem(CollectionItemMixin, BookWyrmModel): """ok""" book = fields.ForeignKey( "Edition", on_delete=models.PROTECT, activitypub_field="book" ) book_list = models.ForeignKey("List", on_delete=models.CASCADE) user = fields.ForeignKey( "User", on_delete=models.PROTECT, activitypub_field="actor" ) notes = fields.TextField(blank=True, null=True, max_length=300) approved = models.BooleanField(default=True) order = fields.IntegerField() endorsement = models.ManyToManyField("User", related_name="endorsers") activity_serializer = activitypub.ListItem collection_field = "book_list" def save(self, *args, **kwargs): """create a notification too""" created = not bool(self.id) super().save(*args, **kwargs) # tick the updated date on the parent list self.book_list.updated_date = timezone.now() self.book_list.save(broadcast=False) list_owner = self.book_list.user model = apps.get_model("bookwyrm.Notification", require_ready=True) # create a notification if somoene ELSE added to a local user's list if created and list_owner.local and list_owner != self.user: model.objects.create( user=list_owner, related_user=self.user, related_list_item=self, notification_type="ADD", ) if self.book_list.group: for membership in self.book_list.group.memberships.all(): if membership.user != self.user: model.objects.create( user=membership.user, related_user=self.user, related_list_item=self, notification_type="ADD", ) def raise_not_deletable(self, viewer): """the associated user OR the list owner can delete""" if self.book_list.user == viewer: return # group members can delete items in group lists is_group_member = GroupMember.objects.filter( group=self.book_list.group, user=viewer ).exists() if is_group_member: return super().raise_not_deletable(viewer) class Meta: """A book may only be placed into a list once, and each order in the list may be used only once""" unique_together = (("book", "book_list"), ("order", "book_list")) ordering = ("-created_date",)