Accessing many-to-many relations before saving is no longer allowed. Reorder all operations consistently: 1. Validations 2. Modify own fields 3. Perform save by calling super().save() 4. Modify related objects and clear caches Especially clearing caches should be done after actually saving, otherwise the old data can be re-added immediately by another request before the new data is written.
137 lines
4.4 KiB
Python
137 lines
4.4 KiB
Python
""" puttin' books on shelves """
|
|
import re
|
|
from django.core.cache import cache
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
|
|
from bookwyrm import activitypub
|
|
from bookwyrm.settings import BASE_URL
|
|
from bookwyrm.tasks import BROADCAST
|
|
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
|
from .base_model import BookWyrmModel
|
|
from . import fields
|
|
|
|
|
|
class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|
"""a list of books owned by a user"""
|
|
|
|
TO_READ = "to-read"
|
|
READING = "reading"
|
|
READ_FINISHED = "read"
|
|
STOPPED_READING = "stopped-reading"
|
|
|
|
READ_STATUS_IDENTIFIERS = (TO_READ, READING, READ_FINISHED, STOPPED_READING)
|
|
|
|
name = fields.CharField(max_length=100)
|
|
identifier = models.CharField(max_length=100)
|
|
description = models.TextField(blank=True, null=True, max_length=500)
|
|
user = fields.ForeignKey(
|
|
"User", on_delete=models.PROTECT, activitypub_field="owner"
|
|
)
|
|
editable = models.BooleanField(default=True)
|
|
privacy = fields.PrivacyField()
|
|
books = models.ManyToManyField(
|
|
"Edition",
|
|
symmetrical=False,
|
|
through="ShelfBook",
|
|
through_fields=("shelf", "book"),
|
|
)
|
|
|
|
activity_serializer = activitypub.Shelf
|
|
|
|
def save(self, *args, priority=BROADCAST, **kwargs):
|
|
"""set the identifier"""
|
|
super().save(*args, priority=priority, **kwargs)
|
|
if not self.identifier:
|
|
# this needs the auto increment ID from the save() above
|
|
self.identifier = self.get_identifier()
|
|
super().save(*args, **kwargs, broadcast=False)
|
|
|
|
def get_identifier(self):
|
|
"""custom-shelf-123 for the url"""
|
|
slug = re.sub(r"[^\w]", "", self.name).lower()
|
|
return f"{slug}-{self.id}"
|
|
|
|
@property
|
|
def collection_queryset(self):
|
|
"""list of books for this shelf, overrides OrderedCollectionMixin"""
|
|
return self.books.order_by("shelfbook")
|
|
|
|
@property
|
|
def deletable(self):
|
|
"""can the shelf be safely deleted?"""
|
|
return self.editable and not self.shelfbook_set.exists()
|
|
|
|
def get_remote_id(self):
|
|
"""shelf identifier instead of id"""
|
|
base_path = self.user.remote_id
|
|
identifier = self.identifier or self.get_identifier()
|
|
return f"{base_path}/books/{identifier}"
|
|
|
|
@property
|
|
def local_path(self):
|
|
"""No slugs"""
|
|
return self.get_remote_id().replace(BASE_URL, "")
|
|
|
|
def raise_not_deletable(self, viewer):
|
|
"""don't let anyone delete a default shelf"""
|
|
super().raise_not_deletable(viewer)
|
|
if not self.deletable:
|
|
raise PermissionDenied()
|
|
|
|
class Meta:
|
|
"""user/shelf uniqueness"""
|
|
|
|
unique_together = ("user", "identifier")
|
|
|
|
|
|
class ShelfBook(CollectionItemMixin, BookWyrmModel):
|
|
"""many to many join table for books and shelves"""
|
|
|
|
book = fields.ForeignKey(
|
|
"Edition", on_delete=models.PROTECT, activitypub_field="book"
|
|
)
|
|
shelf = models.ForeignKey("Shelf", on_delete=models.PROTECT)
|
|
shelved_date = models.DateTimeField(default=timezone.now)
|
|
user = fields.ForeignKey(
|
|
"User", on_delete=models.PROTECT, activitypub_field="actor"
|
|
)
|
|
|
|
activity_serializer = activitypub.ShelfItem
|
|
collection_field = "shelf"
|
|
|
|
def save(self, *args, priority=BROADCAST, **kwargs):
|
|
if not self.user:
|
|
self.user = self.shelf.user
|
|
|
|
is_update = self.id is not None
|
|
super().save(*args, priority=priority, **kwargs)
|
|
|
|
if is_update and self.user.local:
|
|
# remove all caches related to all editions of this book
|
|
cache.delete_many(
|
|
[
|
|
f"book-on-shelf-{book.id}-{self.shelf_id}"
|
|
for book in self.book.parent_work.editions.all()
|
|
]
|
|
)
|
|
|
|
def delete(self, *args, **kwargs):
|
|
if self.id and self.user.local:
|
|
cache.delete_many(
|
|
[
|
|
f"book-on-shelf-{book}-{self.shelf_id}"
|
|
for book in self.book.parent_work.editions.values_list(
|
|
"id", flat=True
|
|
)
|
|
]
|
|
)
|
|
super().delete(*args, **kwargs)
|
|
|
|
class Meta:
|
|
"""an opinionated constraint!
|
|
you can't put a book on shelf twice"""
|
|
|
|
unique_together = ("book", "shelf")
|
|
ordering = ("-shelved_date", "-created_date", "-updated_date")
|