mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-24 09:00:33 +00:00
Merge pull request #3076 from bookwyrm-social/move
Add Move activity for user migration (with small change)
This commit is contained in:
commit
0502f6ba42
30 changed files with 791 additions and 84 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
130
bookwyrm/migrations/0182_auto_20231027_1122.py
Normal file
130
bookwyrm/migrations/0182_auto_20231027_1122.py
Normal file
|
@ -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",),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
72
bookwyrm/models/move.py
Normal file
72
bookwyrm/models/move.py
Normal file
|
@ -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()
|
|
@ -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)
|
||||
|
|
|
@ -140,6 +140,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 +327,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
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<nav class="navbar" aria-label="main navigation">
|
||||
<div class="container">
|
||||
{% with notification_count=request.user.unread_notification_count has_unread_mentions=request.user.has_unread_mentions %}
|
||||
{% if not request.user.moved_to %}
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}" loading="lazy" decoding="async">
|
||||
|
@ -34,7 +35,7 @@
|
|||
<form class="navbar-item column is-align-items-start pt-5" action="{% url 'search' %}">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
{% if user.is_authenticated %}
|
||||
{% if request.user.is_authenticated %}
|
||||
{% trans "Search for a book, user, or list" as search_placeholder %}
|
||||
{% else %}
|
||||
{% trans "Search for a book" as search_placeholder %}
|
||||
|
@ -80,7 +81,6 @@
|
|||
</strong>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="navbar-menu" id="main_nav">
|
||||
<div class="navbar-start" id="tour-navbar-start">
|
||||
{% if request.user.is_authenticated %}
|
||||
|
@ -157,6 +157,13 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}" loading="lazy" decoding="async">
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -173,11 +180,15 @@
|
|||
|
||||
<main class="section is-flex-grow-1">
|
||||
<div class="container">
|
||||
{# 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 %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
|
52
bookwyrm/templates/moved.html
Normal file
52
bookwyrm/templates/moved.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load utilities %}
|
||||
|
||||
<div class="container my-6">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img src="{% if request.user.avatar %}{% get_media_prefix %}{{ request.user.avatar }}{% else %}{% static "images/default_avi.jpg" %}{% endif %}"
|
||||
{% if ariaHide %}aria-hidden="true"{% endif %}
|
||||
alt="{{ request.user.alt_text }}"
|
||||
loading="lazy"
|
||||
decoding="async">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">{{ request.user.display_name }}</p>
|
||||
<p class="subtitle is-6"><s>{{request.user.username}}</s></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notification is-warning">
|
||||
<p>
|
||||
{% id_to_username request.user.moved_to as username %}
|
||||
{% blocktrans trimmed with moved_to=user.moved_to %}
|
||||
<strong>You have moved your account</strong> to <a href="{{ moved_to }}">{{ username }}</a>
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
{% trans "You can undo the move to restore full functionality, but some followers may have already unfollowed this account." %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-justify-content-center">
|
||||
<div class="column is-one-quarter">
|
||||
<div class="level">
|
||||
<form class="level-left" name="remove-alias" action="{% url 'prefs-unmove' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="remote_id" id="remote_id" value="{{user.moved_to}}">
|
||||
<button type="submit" class="button is-medium is-danger">{% trans "Undo move" %}</button>
|
||||
</form>
|
||||
<form class="level-right" name="logout" action="{% url 'logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button is-medium is-primary">{% trans 'Log out' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -35,4 +35,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 %}
|
||||
|
|
|
@ -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 %}
|
||||
</div>
|
||||
|
||||
{% if related_status %}
|
||||
|
|
29
bookwyrm/templates/notifications/items/move_user.html
Normal file
29
bookwyrm/templates/notifications/items/move_user.html
Normal file
|
@ -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 %}
|
||||
<span class="icon icon-local"></span>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{% if related_user_moved_to %}
|
||||
{% id_to_username request.user.moved_to as username %}
|
||||
{% blocktrans trimmed %}
|
||||
{{ related_user }} has moved to <a href="{{ related_user_moved_to }}">{{ username }}</a>
|
||||
{% endblocktrans %}
|
||||
<div class="row shrink my-2">
|
||||
{% include 'snippets/move_user_buttons.html' with group=notification.related_group %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
{{ related_user }} has undone their move
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
59
bookwyrm/templates/preferences/alias_user.html
Normal file
59
bookwyrm/templates/preferences/alias_user.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
{% extends 'preferences/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Move Account" %}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% trans "Create Alias" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="block">
|
||||
<h2 class="title is-4">{% trans "Add another account as an alias" %}</h2>
|
||||
<div class="box">
|
||||
<div class="notification is-info is-light">
|
||||
<p>
|
||||
{% trans "Marking another account as an alias is required if you want to move that account to this one." %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "This is a reversable action and will not change the functionality of this account." %}
|
||||
</p>
|
||||
</div>
|
||||
<form name="alias-user" action="{% url 'prefs-alias' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_target">{% trans "Enter the username for the account you want to add as an alias e.g. <em>user@example.com </em>:" %}</label>
|
||||
<input class="input {% if form.username.errors %}is-danger{% endif %}" type="text" name="username" id="id_username" required aria-describedby="desc_username">
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.username.errors id="desc_username" %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_password">{% trans "Confirm your password:" %}</label>
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required aria-describedby="desc_password">
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<button type="submit" class="button is-success">{% trans "Create Alias" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if user.also_known_as.all.0 %}
|
||||
<div class="box">
|
||||
<h2 class="title is-4">{% trans "Aliases" %}</h2>
|
||||
<div class="table-container block">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
{% for alias in user.also_known_as.all %}
|
||||
<tr>
|
||||
<td>{{ alias.username }}</td>
|
||||
<td>
|
||||
<form name="remove-alias" action="{% url 'prefs-remove-alias' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="alias" id="alias" value="{{ alias.id }}">
|
||||
<button type="submit" class="button is-info">{% trans "Remove alias" %}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -23,6 +23,14 @@
|
|||
{% url 'prefs-2fa' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Two Factor Authentication" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% url 'prefs-alias' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Aliases" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% url 'prefs-move' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Move Account" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% url 'prefs-delete' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Delete Account" %}</a>
|
||||
|
|
43
bookwyrm/templates/preferences/move_user.html
Normal file
43
bookwyrm/templates/preferences/move_user.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{% extends 'preferences/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Move Account" %}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% trans "Move Account" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="block">
|
||||
<h2 class="title is-4">{% trans "Migrate account to another server" %}</h2>
|
||||
<div class="box">
|
||||
<div class="notification is-danger is-light">
|
||||
<p>
|
||||
{% trans "Moving your account will notify all your followers and direct them to follow the new account." %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
<strong>{{ user }}</strong> will be marked as moved and will not be discoverable or usable unless you undo the move.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="notification is-info is-light">
|
||||
<p>{% trans "Remember to add this user as an alias of the target account before you try to move." %}</p>
|
||||
</div>
|
||||
<form name="move-user" action="{% url 'prefs-move' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_target">{% trans "Enter the username for the account you want to move to e.g. <em>user@example.com </em>:" %}</label>
|
||||
<input class="input {% if form.target.errors %}is-danger{% endif %}" type="text" name="target" id="id_target" required aria-describedby="desc_target">
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.target.errors id="desc_target" %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_password">{% trans "Confirm your password:" %}</label>
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required aria-describedby="desc_password">
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<button type="submit" class="button is-danger">{% trans "Move Account" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -75,10 +75,17 @@
|
|||
<td>{{ user.last_active_date }}</td>
|
||||
<td>
|
||||
{% if user.is_active %}
|
||||
<span class="tag is-success" aria-hidden="true">
|
||||
<span class="icon icon-check"></span>
|
||||
</span>
|
||||
{% trans "Active" %}
|
||||
{% if user.moved_to %}
|
||||
<span class="tag is-info" aria-hidden="true">
|
||||
<span class="icon icon-x"></span>
|
||||
</span>
|
||||
{% trans "Moved" %}
|
||||
{% else %}
|
||||
<span class="tag is-success" aria-hidden="true">
|
||||
<span class="icon icon-check"></span>
|
||||
</span>
|
||||
{% trans "Active" %}
|
||||
{% endif %}
|
||||
{% elif user.deactivation_reason == "moderator_deletion" or user.deactivation_reason == "self_deletion" %}
|
||||
<span class="tag is-danger" aria-hidden="true">
|
||||
<span class="icon icon-x"></span>
|
||||
|
|
|
@ -24,9 +24,15 @@
|
|||
<h4 class="title is-4">{% trans "Status" %}</h4>
|
||||
<div class="box is-flex-grow-1 has-text-weight-bold">
|
||||
{% if user.is_active %}
|
||||
<p class="notification is-success">
|
||||
{% trans "Active" %}
|
||||
</p>
|
||||
{% if user.moved_to %}
|
||||
<p class="notification is-info">
|
||||
{% trans "Moved" %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="notification is-success">
|
||||
{% trans "Active" %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="notification is-warning">
|
||||
{% trans "Inactive" %}
|
||||
|
|
|
@ -18,7 +18,22 @@
|
|||
{% include 'user/books_header.html' %}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
{% if user.moved_to %}
|
||||
<div class="container my-6">
|
||||
<div class="notification is-info has-text-centered">
|
||||
<p>
|
||||
{% trans "You have have moved to" %}
|
||||
<a href="{{user.moved_to}}">{% id_to_username user.moved_to %}</a>
|
||||
</p>
|
||||
<p> {% trans "You can undo this move to restore full functionality, but some followers may have already unfollowed this account." %}</p>
|
||||
<form name="remove-alias" action="{% url 'prefs-unmove' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="remote_id" id="remote_id" value="{{user.moved_to}}">
|
||||
<button type="submit" class="button is-small">{% trans "Undo move" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'user-feed' user|username %}">{% trans "User profile" %}</a></li>
|
||||
|
@ -215,6 +230,7 @@
|
|||
<div>
|
||||
{% include 'snippets/pagination.html' with page=books path=request.path %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
|
13
bookwyrm/templates/snippets/move_user_buttons.html
Normal file
13
bookwyrm/templates/snippets/move_user_buttons.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
|
||||
{% if related_user_moved_to|user_from_remote_id not in request.user.following.all %}
|
||||
<div class="field is-grouped">
|
||||
<form action="{% url 'follow' %}" method="POST" class="interaction follow_{{ related_users.0.id }}" data-id="follow_{{ related_users.0.id }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{% id_to_username related_user_moved_to %}">
|
||||
<button class="button is-link is-small" type="submit">{% trans "Follow at new account" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
|
@ -5,6 +5,7 @@
|
|||
{% load markdown %}
|
||||
{% load layout %}
|
||||
{% load group_tags %}
|
||||
{% load user_page_tags %}
|
||||
|
||||
{% block title %}{{ user.display_name }}{% endblock %}
|
||||
|
||||
|
@ -27,7 +28,11 @@
|
|||
<div class="block">
|
||||
<div class="columns">
|
||||
<div class="column is-two-fifths">
|
||||
{% include 'user/user_preview.html' with user=user %}
|
||||
{% if user.moved_to %}
|
||||
{% include 'user/moved.html' with user=user %}
|
||||
{% else %}
|
||||
{% include 'user/user_preview.html' with user=user %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if user.summary %}
|
||||
|
@ -38,70 +43,83 @@
|
|||
{% endspaceless %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not is_self and request.user.is_authenticated %}
|
||||
{% include 'snippets/follow_button.html' with user=user %}
|
||||
{% endif %}
|
||||
{% if not is_self %}
|
||||
{% include 'ostatus/remote_follow_button.html' with user=user %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_self and user.active_follower_requests.all %}
|
||||
<div class="follow-requests">
|
||||
<h2>{% trans "Follow Requests" %}</h2>
|
||||
{% for requester in user.follower_requests.all %}
|
||||
<div class="row shrink">
|
||||
<p>
|
||||
<a href="{{ requester.local_path }}">{{ requester.display_name }}</a> ({{ requester.username }})
|
||||
</p>
|
||||
{% include 'snippets/follow_request_buttons.html' with user=requester %}
|
||||
<div>
|
||||
{% if user.moved_to %}
|
||||
<div class="container my-6">
|
||||
<div class="notification is-info has-text-centered">
|
||||
<p><em>{{ user.localname }}</em> {% trans "has moved to" %} <a href="{{user.moved_to}}">{% id_to_username user.moved_to %}</a></p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% if not is_self and request.user.is_authenticated %}
|
||||
{% include 'snippets/follow_button.html' with user=user %}
|
||||
{% endif %}
|
||||
{% if not is_self %}
|
||||
{% include 'ostatus/remote_follow_button.html' with user=user %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_self and user.active_follower_requests.all %}
|
||||
<div class="follow-requests">
|
||||
<h2>{% trans "Follow Requests" %}</h2>
|
||||
{% for requester in user.follower_requests.all %}
|
||||
<div class="row shrink">
|
||||
<p>
|
||||
<a href="{{ requester.local_path }}">{{ requester.display_name }}</a> ({{ requester.username }})
|
||||
</p>
|
||||
{% include 'snippets/follow_request_buttons.html' with user=requester %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% block tabs %}
|
||||
{% if not user.moved_to %}
|
||||
{% with user|username as username %}
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{% url 'user-feed' user|username as url %}
|
||||
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Activity" %}</a>
|
||||
</li>
|
||||
{% url 'user-reviews-comments' user|username as url %}
|
||||
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Reviews and Comments" %}</a>
|
||||
</li>
|
||||
{% if is_self or user.goal.exists %}
|
||||
{% now 'Y' as year %}
|
||||
{% url 'user-goal' user|username year as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %} id="tour-reading-goal">
|
||||
<a href="{{ url }}">{% trans "Reading Goal" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_self or user|has_groups %}
|
||||
{% url 'user-groups' user|username as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %} id="tour-groups-tab">
|
||||
<a href="{{ url }}">{% trans "Groups" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_self or user.list_set.exists %}
|
||||
{% url 'user-lists' user|username as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %} id="tour-lists-tab">
|
||||
<a href="{{ url }}">{% trans "Lists" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user.shelf_set.exists %}
|
||||
{% url 'user-shelves' user|username as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %} id="tour-shelves-tab">
|
||||
<a href="{{ url }}">{% trans "Books" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block tabs %}
|
||||
{% with user|username as username %}
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{% url 'user-feed' user|username as url %}
|
||||
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Activity" %}</a>
|
||||
</li>
|
||||
{% url 'user-reviews-comments' user|username as url %}
|
||||
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Reviews and Comments" %}</a>
|
||||
</li>
|
||||
{% if is_self or user.goal.exists %}
|
||||
{% now 'Y' as year %}
|
||||
{% url 'user-goal' user|username year as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %} id="tour-reading-goal">
|
||||
<a href="{{ url }}">{% trans "Reading Goal" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_self or user|has_groups %}
|
||||
{% url 'user-groups' user|username as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %} id="tour-groups-tab">
|
||||
<a href="{{ url }}">{% trans "Groups" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_self or user.list_set.exists %}
|
||||
{% url 'user-lists' user|username as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %} id="tour-lists-tab">
|
||||
<a href="{{ url }}">{% trans "Lists" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user.shelf_set.exists %}
|
||||
{% url 'user-shelves' user|username as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %} id="tour-shelves-tab">
|
||||
<a href="{{ url }}">{% trans "Books" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}{% endblock %}
|
||||
|
||||
{% if not user.moved_to %}
|
||||
{% block panel %}{% endblock %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
27
bookwyrm/templates/user/moved.html
Normal file
27
bookwyrm/templates/user/moved.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load utilities %}
|
||||
{% load markdown %}
|
||||
{% load layout %}
|
||||
{% load group_tags %}
|
||||
|
||||
|
||||
<div class="media block">
|
||||
<div class="media-left">
|
||||
<a href="{{ user.local_path }}">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p>
|
||||
{% if user.name %}{{ user.name }}{% else %}{{ user.localname }}{% endif %}
|
||||
{% if user.manually_approves_followers %}
|
||||
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
|
||||
<span class="is-sr-only">{% trans "Locked account" %}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{{ user.username }}</p>
|
||||
<p>{% blocktrans with date=user.created_date|naturaltime %}Joined {{ date }}{% endblocktrans %}</p>
|
||||
</div>
|
||||
</div>
|
|
@ -2,11 +2,13 @@
|
|||
import os
|
||||
import re
|
||||
from uuid import uuid4
|
||||
from urllib.parse import urlparse
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.templatetags.static import static
|
||||
|
||||
from bookwyrm.models import User
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
@ -29,6 +31,13 @@ def get_user_identifier(user):
|
|||
return user.localname if user.localname else user.username
|
||||
|
||||
|
||||
@register.filter(name="user_from_remote_id")
|
||||
def get_user_identifier_from_remote_id(remote_id):
|
||||
"""get the local user id from their remote id"""
|
||||
user = User.objects.get(remote_id=remote_id)
|
||||
return user if user else None
|
||||
|
||||
|
||||
@register.filter(name="book_title")
|
||||
def get_title(book, too_short=5):
|
||||
"""display the subtitle if the title is short"""
|
||||
|
@ -103,3 +112,16 @@ def get_isni(existing, author, autoescape=True):
|
|||
f'<input type="text" name="isni-for-{author.id}" value="{isni}" hidden>'
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=False)
|
||||
def id_to_username(user_id):
|
||||
"""given an arbitrary remote id, return the username"""
|
||||
if user_id:
|
||||
url = urlparse(user_id)
|
||||
domain = url.netloc
|
||||
parts = url.path.split("/")
|
||||
name = parts[-1]
|
||||
value = f"{name}@{domain}"
|
||||
|
||||
return value
|
||||
|
|
|
@ -88,9 +88,11 @@ class User(TestCase):
|
|||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"schema": "http://schema.org#",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"},
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"movedTo": {"@id": "as:movedTo", "@type": "@id"},
|
||||
"schema": "http://schema.org#",
|
||||
"value": "schema:value",
|
||||
},
|
||||
],
|
||||
|
|
|
@ -600,6 +600,12 @@ urlpatterns = [
|
|||
name="prompt-2fa",
|
||||
),
|
||||
re_path(r"^preferences/export/?$", views.Export.as_view(), name="prefs-export"),
|
||||
re_path(r"^preferences/move/?$", views.MoveUser.as_view(), name="prefs-move"),
|
||||
re_path(r"^preferences/alias/?$", views.AliasUser.as_view(), name="prefs-alias"),
|
||||
re_path(
|
||||
r"^preferences/remove-alias/?$", views.remove_alias, name="prefs-remove-alias"
|
||||
),
|
||||
re_path(r"^preferences/unmove/?$", views.unmove, name="prefs-unmove"),
|
||||
re_path(r"^preferences/delete/?$", views.DeleteUser.as_view(), name="prefs-delete"),
|
||||
re_path(
|
||||
r"^preferences/deactivate/?$",
|
||||
|
|
|
@ -37,6 +37,7 @@ from .admin.user_admin import UserAdmin, UserAdminList, ActivateUserAdmin
|
|||
from .preferences.change_password import ChangePassword
|
||||
from .preferences.edit_user import EditUser
|
||||
from .preferences.export import Export
|
||||
from .preferences.move_user import MoveUser, AliasUser, remove_alias, unmove
|
||||
from .preferences.delete_user import DeleteUser, DeactivateUser, ReactivateUser
|
||||
from .preferences.block import Block, unblock
|
||||
from .preferences.two_factor_auth import (
|
||||
|
|
111
bookwyrm/views/preferences/move_user.py
Normal file
111
bookwyrm/views/preferences/move_user.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
""" move your account somewhere else """
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.views.helpers import handle_remote_webfinger
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class MoveUser(View):
|
||||
"""move user view"""
|
||||
|
||||
def get(self, request):
|
||||
"""move page for a user"""
|
||||
data = {
|
||||
"form": forms.MoveUserForm(),
|
||||
"user": request.user,
|
||||
}
|
||||
return TemplateResponse(request, "preferences/move_user.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""Packing your stuff and moving house"""
|
||||
form = forms.MoveUserForm(request.POST, instance=request.user)
|
||||
user = models.User.objects.get(id=request.user.id)
|
||||
|
||||
if form.is_valid() and user.check_password(form.cleaned_data["password"]):
|
||||
username = form.cleaned_data["target"]
|
||||
target = handle_remote_webfinger(username)
|
||||
|
||||
try:
|
||||
models.MoveUser.objects.create(
|
||||
user=request.user, object=request.user.remote_id, target=target
|
||||
)
|
||||
|
||||
return redirect("user-feed", username=request.user.username)
|
||||
|
||||
except PermissionDenied:
|
||||
form.errors["target"] = [
|
||||
"Set this user as an alias on the user you are moving to first"
|
||||
]
|
||||
data = {"form": form, "user": request.user}
|
||||
return TemplateResponse(request, "preferences/move_user.html", data)
|
||||
|
||||
form.errors["password"] = ["Invalid password"]
|
||||
data = {"form": form, "user": request.user}
|
||||
return TemplateResponse(request, "preferences/move_user.html", data)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class AliasUser(View):
|
||||
"""alias user view"""
|
||||
|
||||
def get(self, request):
|
||||
"""move page for a user"""
|
||||
data = {
|
||||
"form": forms.AliasUserForm(),
|
||||
"user": request.user,
|
||||
}
|
||||
return TemplateResponse(request, "preferences/alias_user.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""Creating a nom de plume"""
|
||||
form = forms.AliasUserForm(request.POST, instance=request.user)
|
||||
user = models.User.objects.get(id=request.user.id)
|
||||
|
||||
if form.is_valid() and user.check_password(form.cleaned_data["password"]):
|
||||
username = form.cleaned_data["username"]
|
||||
remote_user = handle_remote_webfinger(username)
|
||||
|
||||
if remote_user is None:
|
||||
form.errors["username"] = ["Username does not exist"]
|
||||
data = {"form": form, "user": request.user}
|
||||
return TemplateResponse(request, "preferences/alias_user.html", data)
|
||||
|
||||
user.also_known_as.add(remote_user.id)
|
||||
|
||||
return redirect("prefs-alias")
|
||||
|
||||
form.errors["password"] = ["Invalid password"]
|
||||
data = {"form": form, "user": request.user}
|
||||
return TemplateResponse(request, "preferences/alias_user.html", data)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def remove_alias(request):
|
||||
"""remove an alias from the user profile"""
|
||||
|
||||
request.user.also_known_as.remove(request.POST["alias"])
|
||||
return redirect("prefs-alias")
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
def unmove(request):
|
||||
"""undo a user move"""
|
||||
target = get_object_or_404(models.User, remote_id=request.POST["remote_id"])
|
||||
move = get_object_or_404(models.MoveUser, target=target, user=request.user)
|
||||
move.delete()
|
||||
|
||||
request.user.moved_to = None
|
||||
request.user.save(update_fields=["moved_to"], broadcast=True)
|
||||
return redirect("prefs-alias")
|
|
@ -21,6 +21,7 @@ def webfinger(request):
|
|||
|
||||
username = resource.replace("acct:", "")
|
||||
user = get_object_or_404(models.User, username__iexact=username)
|
||||
href = user.moved_to if user.moved_to else user.remote_id
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
|
@ -29,7 +30,7 @@ def webfinger(request):
|
|||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": user.remote_id,
|
||||
"href": href,
|
||||
},
|
||||
{
|
||||
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||
|
|
Loading…
Reference in a new issue