diff --git a/FEDERATION.md b/FEDERATION.md index dd0c917e2..d80e98bd3 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -13,14 +13,15 @@ User relationship interactions follow the standard ActivityPub spec. - `Block`: prevent users from seeing one another's statuses, and prevents the blocked user from viewing the actor's profile - `Update`: updates a user's profile and settings - `Delete`: deactivates a user -- `Undo`: reverses a `Follow` or `Block` +- `Undo`: reverses a `Block` or `Follow` ### Activities - `Create/Status`: saves a new status in the database. - `Delete/Status`: Removes a status - `Like/Status`: Creates a favorite on the status - `Announce/Status`: Boosts the status into the actor's timeline -- `Undo/*`,: Reverses a `Like` or `Announce` +- `Undo/*`,: Reverses an `Announce`, `Like`, or `Move` +- `Move/User`: Moves a user from one ActivityPub id to another. ### Collections User's books and lists are represented by [`OrderedCollection`](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection) diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index 2697620f0..41decd68a 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -23,6 +23,7 @@ from .verbs import Create, Delete, Undo, Update from .verbs import Follow, Accept, Reject, Block from .verbs import Add, Remove from .verbs import Announce, Like +from .verbs import Move # this creates a list of all the Activity types that we can serialize, # so when an Activity comes in from outside, we can check if it's known diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index 5db0dc3ac..a53222053 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -22,8 +22,6 @@ class BookData(ActivityObject): aasin: Optional[str] = None isfdb: Optional[str] = None lastEditedBy: Optional[str] = None - links: list[str] = field(default_factory=list) - fileLinks: list[str] = field(default_factory=list) # pylint: disable=invalid-name @@ -45,6 +43,8 @@ class Book(BookData): firstPublishedDate: str = "" publishedDate: str = "" + fileLinks: list[str] = field(default_factory=list) + cover: Optional[Document] = None type: str = "Book" diff --git a/bookwyrm/activitypub/person.py b/bookwyrm/activitypub/person.py index 61c15a579..85cf44409 100644 --- a/bookwyrm/activitypub/person.py +++ b/bookwyrm/activitypub/person.py @@ -40,4 +40,6 @@ class Person(ActivityObject): manuallyApprovesFollowers: str = False discoverable: str = False hideFollows: str = False + movedTo: str = None + alsoKnownAs: dict[str] = None type: str = "Person" diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index 4b7514b5a..00c9524fe 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -231,3 +231,30 @@ class Announce(Verb): def action(self, allow_external_connections=True): """boost""" self.to_model(allow_external_connections=allow_external_connections) + + +@dataclass(init=False) +class Move(Verb): + """a user moving an object""" + + object: str + type: str = "Move" + origin: str = None + target: str = None + + def action(self, allow_external_connections=True): + """move""" + + object_is_user = resolve_remote_id(remote_id=self.object, model="User") + + if object_is_user: + model = apps.get_model("bookwyrm.MoveUser") + + self.to_model( + model=model, + save=True, + allow_external_connections=allow_external_connections, + ) + else: + # we might do something with this to move other objects at some point + pass diff --git a/bookwyrm/forms/edit_user.py b/bookwyrm/forms/edit_user.py index ce7bb6d07..9024972c3 100644 --- a/bookwyrm/forms/edit_user.py +++ b/bookwyrm/forms/edit_user.py @@ -70,6 +70,22 @@ class DeleteUserForm(CustomForm): fields = ["password"] +class MoveUserForm(CustomForm): + target = forms.CharField(widget=forms.TextInput) + + class Meta: + model = models.User + fields = ["password"] + + +class AliasUserForm(CustomForm): + username = forms.CharField(widget=forms.TextInput) + + class Meta: + model = models.User + fields = ["password"] + + class ChangePasswordForm(CustomForm): current_password = forms.CharField(widget=forms.PasswordInput) confirm_password = forms.CharField(widget=forms.PasswordInput) diff --git a/bookwyrm/isbn/isbn.py b/bookwyrm/isbn/isbn.py index 4cc7f47dd..56062ff7b 100644 --- a/bookwyrm/isbn/isbn.py +++ b/bookwyrm/isbn/isbn.py @@ -40,7 +40,12 @@ class IsbnHyphenator: self.__element_tree = ElementTree.parse(self.__range_file_path) gs1_prefix = isbn_13[:3] - reg_group = self.__find_reg_group(isbn_13, gs1_prefix) + try: + reg_group = self.__find_reg_group(isbn_13, gs1_prefix) + except ValueError: + # if the reg groups are invalid, just return the original isbn + return isbn_13 + if reg_group is None: return isbn_13 # failed to hyphenate diff --git a/bookwyrm/migrations/0179_populate_sort_title.py b/bookwyrm/migrations/0179_populate_sort_title.py index e238bca1d..a149a68a7 100644 --- a/bookwyrm/migrations/0179_populate_sort_title.py +++ b/bookwyrm/migrations/0179_populate_sort_title.py @@ -45,5 +45,7 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(populate_sort_title), + migrations.RunPython( + populate_sort_title, reverse_code=migrations.RunPython.noop + ), ] diff --git a/bookwyrm/migrations/0182_auto_20231027_1122.py b/bookwyrm/migrations/0182_auto_20231027_1122.py new file mode 100644 index 000000000..ab57907a9 --- /dev/null +++ b/bookwyrm/migrations/0182_auto_20231027_1122.py @@ -0,0 +1,130 @@ +# Generated by Django 3.2.20 on 2023-10-27 11:22 + +import bookwyrm.models.activitypub_mixin +import bookwyrm.models.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0181_merge_20230806_2302"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="also_known_as", + field=bookwyrm.models.fields.ManyToManyField(to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name="user", + name="moved_to", + field=bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), + ), + migrations.AlterField( + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("MENTION", "Mention"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT", "Import"), + ("ADD", "Add"), + ("REPORT", "Report"), + ("LINK_DOMAIN", "Link Domain"), + ("INVITE", "Invite"), + ("ACCEPT", "Accept"), + ("JOIN", "Join"), + ("LEAVE", "Leave"), + ("REMOVE", "Remove"), + ("GROUP_PRIVACY", "Group Privacy"), + ("GROUP_NAME", "Group Name"), + ("GROUP_DESCRIPTION", "Group Description"), + ("MOVE", "Move"), + ], + max_length=255, + ), + ), + migrations.CreateModel( + name="Move", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now_add=True)), + ("updated_date", models.DateTimeField(auto_now=True)), + ( + "remote_id", + bookwyrm.models.fields.RemoteIdField( + max_length=255, + null=True, + validators=[bookwyrm.models.fields.validate_remote_id], + ), + ), + ("object", bookwyrm.models.fields.CharField(max_length=255)), + ( + "origin", + bookwyrm.models.fields.CharField( + blank=True, default="", max_length=255, null=True + ), + ), + ( + "user", + bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + bases=(bookwyrm.models.activitypub_mixin.ActivityMixin, models.Model), + ), + migrations.CreateModel( + name="MoveUser", + fields=[ + ( + "move_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="bookwyrm.move", + ), + ), + ( + "target", + bookwyrm.models.fields.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="move_target", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + bases=("bookwyrm.move",), + ), + ] diff --git a/bookwyrm/migrations/0183_auto_20231105_1607.py b/bookwyrm/migrations/0183_auto_20231105_1607.py new file mode 100644 index 000000000..0c8376adc --- /dev/null +++ b/bookwyrm/migrations/0183_auto_20231105_1607.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.20 on 2023-11-05 16:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0182_auto_20231027_1122"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="is_deleted", + field=models.BooleanField(default=False), + ), + ] diff --git a/bookwyrm/migrations/0184_auto_20231106_0421.py b/bookwyrm/migrations/0184_auto_20231106_0421.py new file mode 100644 index 000000000..e8197dea1 --- /dev/null +++ b/bookwyrm/migrations/0184_auto_20231106_0421.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.20 on 2023-11-06 04:21 + +from django.db import migrations +from bookwyrm.models import User + + +def update_deleted_users(apps, schema_editor): + """Find all the users who are deleted, not just inactive, and set deleted""" + users = apps.get_model("bookwyrm", "User") + db_alias = schema_editor.connection.alias + users.objects.using(db_alias).filter( + is_active=False, + deactivation_reason__in=[ + "self_deletion", + "moderator_deletion", + ], + ).update(is_deleted=True) + + # differente rules for remote users + users.objects.using(db_alias).filter(is_active=False, local=False,).exclude( + deactivation_reason="moderator_deactivation", + ).update(is_deleted=True) + + +def erase_deleted_user_data(apps, schema_editor): + """Retroactively clear user data""" + for user in User.objects.filter(is_deleted=True): + user.erase_user_data() + user.save( + broadcast=False, + update_fields=["email", "avatar", "preview_image", "summary", "name"], + ) + user.erase_user_statuses(broadcast=False) + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0183_auto_20231105_1607"), + ] + + operations = [ + migrations.RunPython( + update_deleted_users, reverse_code=migrations.RunPython.noop + ), + migrations.RunPython( + erase_deleted_user_data, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 7b779190b..c455c751f 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -27,6 +27,8 @@ from .group import Group, GroupMember, GroupMemberInvitation from .import_job import ImportJob, ImportItem +from .move import MoveUser + from .site import SiteSettings, Theme, SiteInvite from .site import PasswordReset, InviteRequest from .announcement import Announcement diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 9e05c03af..e5941136f 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -366,9 +366,9 @@ class Edition(Book): # normalize isbn format if self.isbn_10: - self.isbn_10 = re.sub(r"[^0-9X]", "", self.isbn_10) + self.isbn_10 = normalize_isbn(self.isbn_10) if self.isbn_13: - self.isbn_13 = re.sub(r"[^0-9X]", "", self.isbn_13) + self.isbn_13 = normalize_isbn(self.isbn_13) # set rank self.edition_rank = self.get_rank() @@ -463,6 +463,11 @@ def isbn_13_to_10(isbn_13): return converted + str(checkdigit) +def normalize_isbn(isbn): + """Remove unexpected characters from ISBN 10 or 13""" + return re.sub(r"[^0-9X]", "", isbn) + + # pylint: disable=unused-argument @receiver(models.signals.post_save, sender=Edition) def preview_image(instance, *args, **kwargs): diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 28effaf9b..1e458c815 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -483,10 +483,12 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): image_slug = value # when it's an inline image (User avatar/icon, Book cover), it's a json # blob, but when it's an attached image, it's just a url - if hasattr(image_slug, "url"): - url = image_slug.url - elif isinstance(image_slug, str): + if isinstance(image_slug, str): url = image_slug + elif isinstance(image_slug, dict): + url = image_slug.get("url") + elif hasattr(image_slug, "url"): # Serialized to Image/Document object? + url = image_slug.url else: return None diff --git a/bookwyrm/models/move.py b/bookwyrm/models/move.py new file mode 100644 index 000000000..a5bf9d76d --- /dev/null +++ b/bookwyrm/models/move.py @@ -0,0 +1,72 @@ +""" move an object including migrating a user account """ +from django.core.exceptions import PermissionDenied +from django.db import models + +from bookwyrm import activitypub +from .activitypub_mixin import ActivityMixin +from .base_model import BookWyrmModel +from . import fields +from .notification import Notification + + +class Move(ActivityMixin, BookWyrmModel): + """migrating an activitypub user account""" + + user = fields.ForeignKey( + "User", on_delete=models.PROTECT, activitypub_field="actor" + ) + + object = fields.CharField( + max_length=255, + blank=False, + null=False, + activitypub_field="object", + ) + + origin = fields.CharField( + max_length=255, + blank=True, + null=True, + default="", + activitypub_field="origin", + ) + + activity_serializer = activitypub.Move + + +class MoveUser(Move): + """migrating an activitypub user account""" + + target = fields.ForeignKey( + "User", + on_delete=models.PROTECT, + related_name="move_target", + activitypub_field="target", + ) + + def save(self, *args, **kwargs): + """update user info and broadcast it""" + + # only allow if the source is listed in the target's alsoKnownAs + if self.user in self.target.also_known_as.all(): + + self.user.also_known_as.add(self.target.id) + self.user.update_active_date() + self.user.moved_to = self.target.remote_id + self.user.save(update_fields=["moved_to"]) + + if self.user.local: + kwargs[ + "broadcast" + ] = True # Only broadcast if we are initiating the Move + + super().save(*args, **kwargs) + + for follower in self.user.followers.all(): + if follower.local: + Notification.notify( + follower, self.user, notification_type=Notification.MOVE + ) + + else: + raise PermissionDenied() diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index 522038f9a..093c25c65 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -40,11 +40,14 @@ class Notification(BookWyrmModel): GROUP_NAME = "GROUP_NAME" GROUP_DESCRIPTION = "GROUP_DESCRIPTION" + # Migrations + MOVE = "MOVE" + # pylint: disable=line-too-long NotificationType = models.TextChoices( # there has got be a better way to do this "NotificationType", - f"{FAVORITE} {REPLY} {MENTION} {TAG} {FOLLOW} {FOLLOW_REQUEST} {BOOST} {IMPORT} {ADD} {REPORT} {LINK_DOMAIN} {INVITE} {ACCEPT} {JOIN} {LEAVE} {REMOVE} {GROUP_PRIVACY} {GROUP_NAME} {GROUP_DESCRIPTION}", + f"{FAVORITE} {REPLY} {MENTION} {TAG} {FOLLOW} {FOLLOW_REQUEST} {BOOST} {IMPORT} {ADD} {REPORT} {LINK_DOMAIN} {INVITE} {ACCEPT} {JOIN} {LEAVE} {REMOVE} {GROUP_PRIVACY} {GROUP_NAME} {GROUP_DESCRIPTION} {MOVE}", ) user = models.ForeignKey("User", on_delete=models.CASCADE) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 11646431b..cc44fe2bf 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -102,7 +102,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): if hasattr(self, "quotation"): self.quotation = None # pylint: disable=attribute-defined-outside-init self.deleted_date = timezone.now() - self.save() + self.save(*args, **kwargs) @property def recipients(self): diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 6e0912aec..75ca1d527 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -1,13 +1,14 @@ """ database schema for user data """ import re from urllib.parse import urlparse +from uuid import uuid4 from django.apps import apps from django.contrib.auth.models import AbstractUser from django.contrib.postgres.fields import ArrayField, CICharField from django.core.exceptions import PermissionDenied, ObjectDoesNotExist from django.dispatch import receiver -from django.db import models, transaction +from django.db import models, transaction, IntegrityError from django.utils import timezone from django.utils.translation import gettext_lazy as _ from model_utils import FieldTracker @@ -53,6 +54,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): username = fields.UsernameField() email = models.EmailField(unique=True, null=True) + is_deleted = models.BooleanField(default=False) key_pair = fields.OneToOneField( "KeyPair", @@ -140,6 +142,19 @@ class User(OrderedCollectionPageMixin, AbstractUser): theme = models.ForeignKey("Theme", null=True, blank=True, on_delete=models.SET_NULL) hide_follows = fields.BooleanField(default=False) + # migration fields + + moved_to = fields.RemoteIdField( + null=True, unique=False, activitypub_field="movedTo", deduplication_field=False + ) + also_known_as = fields.ManyToManyField( + "self", + symmetrical=False, + unique=False, + activitypub_field="alsoKnownAs", + deduplication_field=False, + ) + # options to turn features on and off show_goal = models.BooleanField(default=True) show_suggested_users = models.BooleanField(default=True) @@ -314,6 +329,8 @@ class User(OrderedCollectionPageMixin, AbstractUser): "schema": "http://schema.org#", "PropertyValue": "schema:PropertyValue", "value": "schema:value", + "alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"}, + "movedTo": {"@id": "as:movedTo", "@type": "@id"}, }, ] return activity_object @@ -379,9 +396,44 @@ class User(OrderedCollectionPageMixin, AbstractUser): """We don't actually delete the database entry""" # pylint: disable=attribute-defined-outside-init self.is_active = False - self.avatar = "" + self.allow_reactivation = False + self.is_deleted = True + + self.erase_user_data() + self.erase_user_statuses() + # skip the logic in this class's save() - super().save(*args, **kwargs) + super().save( + *args, + **kwargs, + ) + + def erase_user_data(self): + """Wipe a user's custom data""" + if not self.is_deleted: + raise IntegrityError( + "Trying to erase user data on user that is not deleted" + ) + + # mangle email address + self.email = f"{uuid4()}@deleted.user" + + # erase data fields + self.avatar = "" + self.preview_image = "" + self.summary = None + self.name = None + self.favorites.set([]) + + def erase_user_statuses(self, broadcast=True): + """Wipe the data on all the user's statuses""" + if not self.is_deleted: + raise IntegrityError( + "Trying to erase user data on user that is not deleted" + ) + + for status in self.status_set.all(): + status.delete(broadcast=broadcast) def deactivate(self): """Disable the user but allow them to reactivate""" diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index cb5a576a2..246db6d43 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -375,9 +375,9 @@ if USE_S3: AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID") AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY") AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME") - AWS_S3_CUSTOM_DOMAIN = env("AWS_S3_CUSTOM_DOMAIN") + AWS_S3_CUSTOM_DOMAIN = env("AWS_S3_CUSTOM_DOMAIN", None) AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", "") - AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL") + AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", None) AWS_DEFAULT_ACL = "public-read" AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"} # S3 Static settings diff --git a/bookwyrm/templates/book/cover_add_modal.html b/bookwyrm/templates/book/cover_add_modal.html index 8ca5bf2a8..89d870cd0 100644 --- a/bookwyrm/templates/book/cover_add_modal.html +++ b/bookwyrm/templates/book/cover_add_modal.html @@ -20,7 +20,7 @@
diff --git a/bookwyrm/templates/book/edit/edit_book_form.html b/bookwyrm/templates/book/edit/edit_book_form.html index 4cc3965e7..30fb00049 100644 --- a/bookwyrm/templates/book/edit/edit_book_form.html +++ b/bookwyrm/templates/book/edit/edit_book_form.html @@ -247,7 +247,7 @@
diff --git a/bookwyrm/templates/guided_tour/home.html b/bookwyrm/templates/guided_tour/home.html index be8d095af..a464206ef 100644 --- a/bookwyrm/templates/guided_tour/home.html +++ b/bookwyrm/templates/guided_tour/home.html @@ -99,7 +99,7 @@ homeTour.addSteps([ ], }, { - text: "{% trans 'Use the Feed, Lists and Discover links to discover the latest news from your feed, lists of books by topic, and the latest happenings on this Bookwyrm server!' %}", + text: "{% trans 'Use the Lists, Discover, and Your Books links to discover reading suggestions and the latest happenings on this server, or to see your catalogued books!' %}", title: "{% trans 'Navigation Bar' %}", attachTo: { element: checkResponsiveState('#tour-navbar-start'), @@ -197,7 +197,7 @@ homeTour.addSteps([ ], }, { - text: `{% trans "Your profile, books, direct messages, and settings can be accessed by clicking on your name in the menu here." %}

{% trans "Try selecting Profile from the drop down menu to continue the tour." %}

`, + text: `{% trans "Your profile, user directory, direct messages, and settings can be accessed by clicking on your name in the menu here." %}

{% trans "Try selecting Profile from the drop down menu to continue the tour." %}

`, title: "{% trans 'Profile and settings menu' %}", attachTo: { element: checkResponsiveState('#navbar-dropdown'), diff --git a/bookwyrm/templates/import/import.html b/bookwyrm/templates/import/import.html index ad857fb2e..2c3be9e07 100644 --- a/bookwyrm/templates/import/import.html +++ b/bookwyrm/templates/import/import.html @@ -21,7 +21,7 @@ {% blocktrans trimmed count days=import_limit_reset with display_size=import_size_limit|intcomma %} Currently, you are allowed to import {{ display_size }} books every {{ import_limit_reset }} day. {% plural %} - Currently, you are allowed to import {{ import_size_limit }} books every {{ import_limit_reset }} days. + Currently, you are allowed to import {{ display_size }} books every {{ import_limit_reset }} days. {% endblocktrans %}

{% blocktrans with display_left=allowed_imports|intcomma %}You have {{ display_left }} left.{% endblocktrans %}

diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index b8459856c..6283e61c4 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -27,6 +27,7 @@ @@ -173,11 +180,15 @@
- {# almost every view needs to know the user shelves #} - {% with request.user.shelf_set.all as user_shelves %} - {% block content %} - {% endblock %} - {% endwith %} + {% if request.user.moved_to %} + {% include "moved.html" %} + {% else %} + {# almost every view needs to know the user shelves #} + {% with request.user.shelf_set.all as user_shelves %} + {% block content %} + {% endblock %} + {% endwith %} + {% endif %}
diff --git a/bookwyrm/templates/moved.html b/bookwyrm/templates/moved.html new file mode 100644 index 000000000..545fc3d87 --- /dev/null +++ b/bookwyrm/templates/moved.html @@ -0,0 +1,52 @@ +{% load i18n %} +{% load static %} +{% load utilities %} + +
+
+
+
+
+
+ {{ request.user.alt_text }} +
+
+
+

{{ request.user.display_name }}

+

{{request.user.username}}

+
+
+ +
+

+ {% id_to_username request.user.moved_to as username %} + {% blocktrans trimmed with moved_to=user.moved_to %} + You have moved your account to {{ username }} + {% endblocktrans %} +

+

+ {% trans "You can undo the move to restore full functionality, but some followers may have already unfollowed this account." %} +

+
+
+
+
+
+ + {% csrf_token %} + + + +
+ {% csrf_token %} + +
+
+
+
+
+
diff --git a/bookwyrm/templates/notifications/item.html b/bookwyrm/templates/notifications/item.html index b53abe3d1..7e7f0da27 100644 --- a/bookwyrm/templates/notifications/item.html +++ b/bookwyrm/templates/notifications/item.html @@ -10,7 +10,9 @@ {% elif notification.notification_type == 'FOLLOW' %} {% include 'notifications/items/follow.html' %} {% elif notification.notification_type == 'FOLLOW_REQUEST' %} - {% include 'notifications/items/follow_request.html' %} + {% if notification.related_users.0.is_active %} + {% include 'notifications/items/follow_request.html' %} + {% endif %} {% elif notification.notification_type == 'IMPORT' %} {% include 'notifications/items/import.html' %} {% elif notification.notification_type == 'ADD' %} @@ -35,4 +37,6 @@ {% include 'notifications/items/update.html' %} {% elif notification.notification_type == 'GROUP_DESCRIPTION' %} {% include 'notifications/items/update.html' %} +{% elif notification.notification_type == 'MOVE' %} + {% include 'notifications/items/move_user.html' %} {% endif %} diff --git a/bookwyrm/templates/notifications/items/layout.html b/bookwyrm/templates/notifications/items/layout.html index 8acbb9fec..41353abcf 100644 --- a/bookwyrm/templates/notifications/items/layout.html +++ b/bookwyrm/templates/notifications/items/layout.html @@ -39,6 +39,8 @@ {% with related_user=related_users.0.display_name %} {% with related_user_link=related_users.0.local_path %} + {% with related_user_moved_to=related_users.0.moved_to %} + {% with related_user_username=related_users.0.username %} {% with second_user=related_users.1.display_name %} {% with second_user_link=related_users.1.local_path %} {% with other_user_count=related_user_count|add:"-1" %} @@ -50,6 +52,8 @@ {% endwith %} {% endwith %} {% endwith %} + {% endwith %} + {% endwith %} {% if related_status %} diff --git a/bookwyrm/templates/notifications/items/move_user.html b/bookwyrm/templates/notifications/items/move_user.html new file mode 100644 index 000000000..b94d96dc4 --- /dev/null +++ b/bookwyrm/templates/notifications/items/move_user.html @@ -0,0 +1,29 @@ +{% extends 'notifications/items/layout.html' %} + +{% load i18n %} +{% load utilities %} +{% load user_page_tags %} + +{% block primary_link %}{% spaceless %} + {{ notification.related_object.local_path }} +{% endspaceless %}{% endblock %} + +{% block icon %} + +{% endblock %} + +{% block description %} + {% if related_user_moved_to %} + {% id_to_username request.user.moved_to as username %} + {% blocktrans trimmed %} + {{ related_user }} has moved to {{ username }} + {% endblocktrans %} +
+ {% include 'snippets/move_user_buttons.html' with group=notification.related_group %} +
+ {% else %} + {% blocktrans trimmed %} + {{ related_user }} has undone their move + {% endblocktrans %} + {% endif %} +{% endblock %} diff --git a/bookwyrm/templates/preferences/alias_user.html b/bookwyrm/templates/preferences/alias_user.html new file mode 100644 index 000000000..e1e468208 --- /dev/null +++ b/bookwyrm/templates/preferences/alias_user.html @@ -0,0 +1,59 @@ +{% extends 'preferences/layout.html' %} +{% load i18n %} + +{% block title %}{% trans "Move Account" %}{% endblock %} + +{% block header %} +{% trans "Create Alias" %} +{% endblock %} + +{% block panel %} +
+

{% trans "Add another account as an alias" %}

+
+
+

+ {% trans "Marking another account as an alias is required if you want to move that account to this one." %} +

+

+ {% trans "This is a reversable action and will not change the functionality of this account." %} +

+
+
+ {% csrf_token %} +
+ + + {% include 'snippets/form_errors.html' with errors_list=form.username.errors id="desc_username" %} +
+
+ + + {% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %} +
+ +
+
+ {% if user.also_known_as.all.0 %} +
+

{% trans "Aliases" %}

+
+ + {% for alias in user.also_known_as.all %} + + + + + {% endfor %} +
{{ alias.username }} +
+ {% csrf_token %} + + +
+
+
+
+ {% endif %} +
+{% endblock %} diff --git a/bookwyrm/templates/preferences/layout.html b/bookwyrm/templates/preferences/layout.html index ca63ec93d..fb0b6fba6 100644 --- a/bookwyrm/templates/preferences/layout.html +++ b/bookwyrm/templates/preferences/layout.html @@ -23,6 +23,14 @@ {% url 'prefs-2fa' as url %} {% trans "Two Factor Authentication" %} +
  • + {% url 'prefs-alias' as url %} + {% trans "Aliases" %} +
  • +
  • + {% url 'prefs-move' as url %} + {% trans "Move Account" %} +
  • {% url 'prefs-delete' as url %} {% trans "Delete Account" %} diff --git a/bookwyrm/templates/preferences/move_user.html b/bookwyrm/templates/preferences/move_user.html new file mode 100644 index 000000000..47b370e82 --- /dev/null +++ b/bookwyrm/templates/preferences/move_user.html @@ -0,0 +1,43 @@ +{% extends 'preferences/layout.html' %} +{% load i18n %} + +{% block title %}{% trans "Move Account" %}{% endblock %} + +{% block header %} +{% trans "Move Account" %} +{% endblock %} + +{% block panel %} +
    +

    {% trans "Migrate account to another server" %}

    +
    +
    +

    + {% trans "Moving your account will notify all your followers and direct them to follow the new account." %} +

    +

    + {% blocktrans %} + {{ user }} will be marked as moved and will not be discoverable or usable unless you undo the move. + {% endblocktrans %} +

    +
    +
    +

    {% trans "Remember to add this user as an alias of the target account before you try to move." %}

    +
    +
    + {% csrf_token %} +
    + + + {% include 'snippets/form_errors.html' with errors_list=form.target.errors id="desc_target" %} +
    +
    + + + {% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %} +
    + +
    +
    +
    +{% endblock %} diff --git a/bookwyrm/templates/settings/celery.html b/bookwyrm/templates/settings/celery.html index 2f4a36ce9..b224e20cc 100644 --- a/bookwyrm/templates/settings/celery.html +++ b/bookwyrm/templates/settings/celery.html @@ -29,7 +29,7 @@
    -

    {% trans "Broadcasts" %}

    +

    {% trans "Broadcast" %}

    {{ queues.broadcast|intcomma }}

    diff --git a/bookwyrm/templates/settings/users/user_admin.html b/bookwyrm/templates/settings/users/user_admin.html index 9bc5805b1..cc5c51ba7 100644 --- a/bookwyrm/templates/settings/users/user_admin.html +++ b/bookwyrm/templates/settings/users/user_admin.html @@ -74,24 +74,7 @@ {{ user.created_date }} {{ user.last_active_date }} - {% if user.is_active %} - - {% trans "Active" %} - {% elif user.deactivation_reason == "moderator_deletion" or user.deactivation_reason == "self_deletion" %} - - {% trans "Deleted" %} - ({{ user.get_deactivation_reason_display }}) - {% else %} - - {% trans "Inactive" %} - ({{ user.get_deactivation_reason_display }}) - {% endif %} + {% include "snippets/user_active_tag.html" with user=user %} {% if status == "federated" %} diff --git a/bookwyrm/templates/settings/users/user_info.html b/bookwyrm/templates/settings/users/user_info.html index a1725ae36..f35c60db9 100644 --- a/bookwyrm/templates/settings/users/user_info.html +++ b/bookwyrm/templates/settings/users/user_info.html @@ -23,18 +23,7 @@

    {% trans "Status" %}

    - {% if user.is_active %} -

    - {% trans "Active" %} -

    - {% else %} -

    - {% trans "Inactive" %} - {% if user.deactivation_reason %} - ({% trans user.get_deactivation_reason_display %}) - {% endif %} -

    - {% endif %} + {% include "snippets/user_active_tag.html" with large=True %}

    {% if user.local %} {% trans "Local" %} diff --git a/bookwyrm/templates/shelf/shelf.html b/bookwyrm/templates/shelf/shelf.html index 7d0035ed3..a2410ef95 100644 --- a/bookwyrm/templates/shelf/shelf.html +++ b/bookwyrm/templates/shelf/shelf.html @@ -18,7 +18,22 @@ {% include 'user/books_header.html' %} - +{% if user.moved_to %} +

    +
    +

    + {% trans "You have have moved to" %} + {% id_to_username user.moved_to %} +

    +

    {% trans "You can undo this move to restore full functionality, but some followers may have already unfollowed this account." %}

    +
    + {% csrf_token %} + + +
    +
    +
    +{% else %}