bookwyrm/bookwyrm/models/shelf.py
Bart Schuurmans 0d621b68e0 Reorder operations in save() overrides
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.
2024-04-25 10:12:30 +02:00

138 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")