diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index 7615adcf..c5b896e3 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -11,6 +11,7 @@ class Book(ActivityObject): """ serializes an edition or work, abstract """ title: str + lastEditedBy: str = None sortTitle: str = "" subtitle: str = "" description: str = "" @@ -64,6 +65,7 @@ class Author(ActivityObject): """ author of a book """ name: str + lastEditedBy: str = None born: str = None died: str = None aliases: List[str] = field(default_factory=lambda: []) diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index c2cbfea3..090beea5 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -176,7 +176,8 @@ class Remove(Add): def action(self): """ find and remove the activity object """ obj = self.object.to_model(save=False, allow_create=False) - obj.delete() + if obj: + obj.delete() @dataclass(init=False) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 1f1f1a3b..df68334e 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -156,14 +156,6 @@ class UserGroupForm(CustomForm): fields = ["groups"] -class TagForm(CustomForm): - class Meta: - model = models.Tag - fields = ["name"] - help_texts = {f: None for f in fields} - labels = {"name": "Add a tag"} - - class CoverForm(CustomForm): class Meta: model = models.Book diff --git a/bookwyrm/migrations/0069_auto_20210422_1604.py b/bookwyrm/migrations/0069_auto_20210422_1604.py new file mode 100644 index 00000000..6591e7b9 --- /dev/null +++ b/bookwyrm/migrations/0069_auto_20210422_1604.py @@ -0,0 +1,34 @@ +# Generated by Django 3.1.8 on 2021-04-22 16:04 + +import bookwyrm.models.fields +from django.conf import settings +from django.db import migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0068_ordering_for_list_items"), + ] + + operations = [ + migrations.AlterField( + model_name="author", + name="last_edited_by", + field=bookwyrm.models.fields.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="book", + name="last_edited_by", + field=bookwyrm.models.fields.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/bookwyrm/migrations/0070_auto_20210423_0121.py b/bookwyrm/migrations/0070_auto_20210423_0121.py new file mode 100644 index 00000000..0b04c3ca --- /dev/null +++ b/bookwyrm/migrations/0070_auto_20210423_0121.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.8 on 2021-04-23 01:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0069_auto_20210422_1604"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="usertag", + unique_together=None, + ), + migrations.RemoveField( + model_name="usertag", + name="book", + ), + migrations.RemoveField( + model_name="usertag", + name="tag", + ), + migrations.RemoveField( + model_name="usertag", + name="user", + ), + migrations.DeleteModel( + name="Tag", + ), + migrations.DeleteModel( + name="UserTag", + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 35e32c2c..2a25a525 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -17,8 +17,6 @@ from .favorite import Favorite from .notification import Notification from .readthrough import ReadThrough, ProgressUpdate, ProgressMode -from .tag import Tag, UserTag - from .user import User, KeyPair, AnnualGoal from .relationship import UserFollows, UserFollowRequest, UserBlocks from .report import Report, ReportComment diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index f687e96c..02cfafc0 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -148,13 +148,17 @@ class ActivitypubMixin: mentions = self.recipients if hasattr(self, "recipients") else [] # we always send activities to explicitly mentioned users' inboxes - recipients = [u.inbox for u in mentions or []] + recipients = [u.inbox for u in mentions or [] if not u.local] # unless it's a dm, all the followers should receive the activity if privacy != "direct": # we will send this out to a subset of all remote users - queryset = user_model.viewer_aware_objects(user).filter( - local=False, + queryset = ( + user_model.viewer_aware_objects(user) + .filter( + local=False, + ) + .distinct() ) # filter users first by whether they're using the desired software # this lets us send book updates only to other bw servers @@ -175,7 +179,7 @@ class ActivitypubMixin: "inbox", flat=True ) recipients += list(shared_inboxes) + list(inboxes) - return recipients + return list(set(recipients)) def to_activity_dataclass(self): """ convert from a model to an activity """ @@ -200,7 +204,9 @@ class ObjectMixin(ActivitypubMixin): created = created or not bool(self.id) # first off, we want to save normally no matter what super().save(*args, **kwargs) - if not broadcast: + if not broadcast or ( + hasattr(self, "status_type") and self.status_type == "Announce" + ): return # this will work for objects owned by a user (lists, shelves) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index a6824c0a..5280c7aa 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -26,7 +26,11 @@ class BookDataModel(ObjectMixin, BookWyrmModel): max_length=255, blank=True, null=True, deduplication_field=True ) - last_edited_by = models.ForeignKey("User", on_delete=models.PROTECT, null=True) + last_edited_by = fields.ForeignKey( + "User", + on_delete=models.PROTECT, + null=True, + ) class Meta: """ can't initialize this model, that wouldn't make sense """ diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 2aefae51..a1b2035b 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -275,9 +275,12 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): return [i.remote_id for i in value.all()] def field_from_activity(self, value): - items = [] if value is None or value is MISSING: - return [] + return None + if not isinstance(value, list): + # If this is a link, we currently aren't doing anything with it + return None + items = [] for remote_id in value: try: validate_remote_id(remote_id) diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index 927c8740..3f849597 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -101,12 +101,15 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): def save(self, *args, broadcast=True, **kwargs): """ make sure the follow or block relationship doesn't already exist """ - # don't create a request if a follow already exists + # if there's a request for a follow that already exists, accept it + # without changing the local database state if UserFollows.objects.filter( user_subject=self.user_subject, user_object=self.user_object, ).exists(): - raise IntegrityError() + self.accept(broadcast_only=True) + return + # blocking in either direction is a no-go if UserBlocks.objects.filter( Q( @@ -141,9 +144,9 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): """ get id for sending an accept or reject of a local user """ base_path = self.user_object.remote_id - return "%s#%s/%d" % (base_path, status, self.id) + return "%s#%s/%d" % (base_path, status, self.id or 0) - def accept(self): + def accept(self, broadcast_only=False): """ turn this request into the real deal""" user = self.user_object if not self.user_subject.local: @@ -153,6 +156,9 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): object=self.to_activity(), ).serialize() self.broadcast(activity, user) + if broadcast_only: + return + with transaction.atomic(): UserFollows.from_request(self) self.delete() diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 360288e9..0dee0b75 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -351,6 +351,16 @@ class Boost(ActivityMixin, Status): def save(self, *args, **kwargs): """ save and notify """ + # This constraint can't work as it would cross tables. + # class Meta: + # unique_together = ('user', 'boosted_status') + if ( + Boost.objects.filter(boosted_status=self.boosted_status, user=self.user) + .exclude(id=self.id) + .exists() + ): + return + super().save(*args, **kwargs) if not self.boosted_status.user.local or self.boosted_status.user == self.user: return diff --git a/bookwyrm/models/tag.py b/bookwyrm/models/tag.py deleted file mode 100644 index 2c45b8f9..00000000 --- a/bookwyrm/models/tag.py +++ /dev/null @@ -1,63 +0,0 @@ -""" models for storing different kinds of Activities """ -import urllib.parse - -from django.apps import apps -from django.db import models - -from bookwyrm import activitypub -from bookwyrm.settings import DOMAIN -from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin -from .base_model import BookWyrmModel -from . import fields - - -class Tag(OrderedCollectionMixin, BookWyrmModel): - """ freeform tags for books """ - - name = fields.CharField(max_length=100, unique=True) - identifier = models.CharField(max_length=100) - - @property - def books(self): - """ count of books associated with this tag """ - edition_model = apps.get_model("bookwyrm.Edition", require_ready=True) - return ( - edition_model.objects.filter(usertag__tag__identifier=self.identifier) - .order_by("-created_date") - .distinct() - ) - - collection_queryset = books - - def get_remote_id(self): - """ tag should use identifier not id in remote_id """ - base_path = "https://%s" % DOMAIN - return "%s/tag/%s" % (base_path, self.identifier) - - def save(self, *args, **kwargs): - """ create a url-safe lookup key for the tag """ - if not self.id: - # add identifiers to new tags - self.identifier = urllib.parse.quote_plus(self.name) - super().save(*args, **kwargs) - - -class UserTag(CollectionItemMixin, BookWyrmModel): - """ an instance of a tag on a book by a user """ - - user = fields.ForeignKey( - "User", on_delete=models.PROTECT, activitypub_field="actor" - ) - book = fields.ForeignKey( - "Edition", on_delete=models.PROTECT, activitypub_field="object" - ) - tag = fields.ForeignKey("Tag", on_delete=models.PROTECT, activitypub_field="target") - - activity_serializer = activitypub.Add - object_field = "book" - collection_field = "tag" - - class Meta: - """ unqiueness constraint """ - - unique_together = ("user", "book", "tag") diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index fe7617bc..cb9e1d12 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -1,6 +1,5 @@ html { scroll-behavior: smooth; - scroll-padding-top: 20%; } body { @@ -110,6 +109,13 @@ body { } } +/** Stars + ******************************************************************************/ + +.stars { + white-space: nowrap; +} + /** Stars in a review form * * Specificity makes hovering taking over checked inputs. diff --git a/bookwyrm/templates/components/dropdown.html b/bookwyrm/templates/components/dropdown.html index 734d6276..96dce823 100644 --- a/bookwyrm/templates/components/dropdown.html +++ b/bookwyrm/templates/components/dropdown.html @@ -23,7 +23,7 @@