Merge branch 'main' into file-resubmit

This commit is contained in:
Hugh Rundle 2023-11-11 13:14:52 +11:00 committed by GitHub
commit 1bedcdaebd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 1778 additions and 726 deletions

View file

@ -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)

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -45,5 +45,7 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(populate_sort_title),
migrations.RunPython(
populate_sort_title, reverse_code=migrations.RunPython.noop
),
]

View 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",),
),
]

View file

@ -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),
),
]

View file

@ -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
),
]

View file

@ -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

View file

@ -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):

View file

@ -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

72
bookwyrm/models/move.py Normal file
View 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()

View file

@ -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)

View file

@ -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):

View file

@ -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"""

View file

@ -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

View file

@ -20,7 +20,7 @@
</div>
<div class="column">
<label class="label" for="id_cover_url">
{% trans "Load cover from url:" %}
{% trans "Load cover from URL:" %}
</label>
<input class="input" name="cover-url" id="id_cover_url">
</div>

View file

@ -247,7 +247,7 @@
</div>
<div class="field">
<label class="label" for="id_cover_url">
{% trans "Load cover from url:" %}
{% trans "Load cover from URL:" %}
</label>
<input class="input" name="cover-url" id="id_cover_url" type="url" value="{{ cover_url|default:'' }}" aria-describedby="desc_cover">
</div>

View file

@ -99,7 +99,7 @@ homeTour.addSteps([
],
},
{
text: "{% trans 'Use the <strong>Feed</strong>, <strong>Lists</strong> and <strong>Discover</strong> 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 <strong>Lists</strong>, <strong>Discover</strong>, and <strong>Your Books</strong> 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." %} <p class="notification is-warning is-light mt-3">{% trans "Try selecting <strong>Profile</strong> from the drop down menu to continue the tour." %}</p>`,
text: `{% trans "Your profile, user directory, direct messages, and settings can be accessed by clicking on your name in the menu here." %} <p class="notification is-warning is-light mt-3">{% trans "Try selecting <strong>Profile</strong> from the drop down menu to continue the tour." %}</p>`,
title: "{% trans 'Profile and settings menu' %}",
attachTo: {
element: checkResponsiveState('#navbar-dropdown'),

View file

@ -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 %}
</p>
<p>{% blocktrans with display_left=allowed_imports|intcomma %}You have {{ display_left }} left.{% endblocktrans %}</p>

View file

@ -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,19 +81,18 @@
</strong>
</button>
</div>
<div class="navbar-menu" id="main_nav">
<div class="navbar-start" id="tour-navbar-start">
{% if request.user.is_authenticated %}
<a href="/#feed" class="navbar-item mt-3 py-0">
{% trans "Feed" %}
</a>
<a href="{% url 'lists' %}" class="navbar-item mt-3 py-0">
{% trans "Lists" %}
</a>
<a href="{% url 'discover' %}" class="navbar-item mt-3 py-0">
{% trans "Discover" %}
</a>
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item mt-3 py-0">
{% trans "Your Books" %}
</a>
{% endif %}
</div>
@ -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>

View 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>

View file

@ -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 %}

View file

@ -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 %}

View 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 %}

View 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 %}

View file

@ -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>

View 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 %}

View file

@ -29,7 +29,7 @@
</div>
<div class="column is-4">
<div class="notification">
<p class="header">{% trans "Broadcasts" %}</p>
<p class="header">{% trans "Broadcast" %}</p>
<p class="title is-5">{{ queues.broadcast|intcomma }}</p>
</div>
</div>

View file

@ -74,24 +74,7 @@
<td>{{ user.created_date }}</td>
<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" %}
{% 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>
</span>
{% trans "Deleted" %}
<span class="help">({{ user.get_deactivation_reason_display }})</span>
{% else %}
<span class="tag is-warning" aria-hidden="true">
<span class="icon icon-x"></span>
</span>
{% trans "Inactive" %}
<span class="help">({{ user.get_deactivation_reason_display }})</span>
{% endif %}
{% include "snippets/user_active_tag.html" with user=user %}
</td>
{% if status == "federated" %}
<td>

View file

@ -23,18 +23,7 @@
<div class="column is-flex is-flex-direction-column is-4">
<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>
{% else %}
<p class="notification is-warning">
{% trans "Inactive" %}
{% if user.deactivation_reason %}
<span class="help">({% trans user.get_deactivation_reason_display %})</span>
{% endif %}
</p>
{% endif %}
{% include "snippets/user_active_tag.html" with large=True %}
<p class="notification">
{% if user.local %}
{% trans "Local" %}

View file

@ -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 %}

View 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 %}

View file

@ -0,0 +1,17 @@
{% load i18n %}
{% if user.is_active %}
{% if user.moved_to %}
{% trans "Moved" as text %}
{% include "snippets/user_active_tag_item.html" with icon="x" text=text level="info" %}
{% else %}
{% trans "Active" as text %}
{% include "snippets/user_active_tag_item.html" with icon="check" text=text level="success" %}
{% endif %}
{% elif user.is_deleted %}
{% trans "Deleted" as text %}
{% include "snippets/user_active_tag_item.html" with icon="x" text=text level="danger" deactivation_reason=user.get_deactivation_reason_display %}
{% else %}
{% trans "Inactive" as text %}
{% include "snippets/user_active_tag_item.html" with icon="x" text=text level="warning" deactivation_reason=user.get_deactivation_reason_display %}
{% endif %}

View file

@ -0,0 +1,19 @@
{% if large %}
<p class="notification is-{{ level }}">
<span class="icon icon-{{ icon }}" aria-hidden="true"></span>
{{ text }}
{% if deactivation_reason %}
<span class="help">({{ deactivation_reason }})</span>
{% endif %}
</p>
{% else %}
<span class="tag is-{{ level }}" aria-hidden="true">
<span class="icon icon-{{ icon }}"></span>
</span>
{{ text }}
{% endif %}

View file

@ -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 %}

View 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>

View file

@ -34,11 +34,6 @@
{% trans "Directory" %}
</a>
</li>
<li role="menuitem">
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
{% trans 'Your Books' %}
</a>
</li>
<li role="menuitem">
<a href="{% url 'direct-messages' %}" class="navbar-item">
{% trans "Direct Messages" %}

View file

@ -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

View file

@ -0,0 +1,121 @@
""" testing migrations """
from unittest.mock import patch
from django.test import TestCase
from django.db.migrations.executor import MigrationExecutor
from django.db import connection
from bookwyrm import models
from bookwyrm.management.commands import initdb
from bookwyrm.settings import DOMAIN
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
class EraseDeletedUserDataMigration(TestCase):
migrate_from = "0183_auto_20231105_1607"
migrate_to = "0184_auto_20231106_0421"
# pylint: disable=invalid-name
def setUp(self):
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
self.active_user = models.User.objects.create_user(
f"activeuser@{DOMAIN}",
"activeuser@activeuser.activeuser",
"activeuserword",
local=True,
localname="active",
name="a name",
)
self.inactive_user = models.User.objects.create_user(
f"inactiveuser@{DOMAIN}",
"inactiveuser@inactiveuser.inactiveuser",
"inactiveuserword",
local=True,
localname="inactive",
is_active=False,
deactivation_reason="self_deactivation",
name="name name",
)
self.deleted_user = models.User.objects.create_user(
f"deleteduser@{DOMAIN}",
"deleteduser@deleteduser.deleteduser",
"deleteduserword",
local=True,
localname="deleted",
is_active=False,
deactivation_reason="self_deletion",
name="cool name",
)
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
), patch("bookwyrm.activitystreams.add_status_task.delay"):
self.active_status = models.Status.objects.create(
user=self.active_user, content="don't delete me"
)
self.inactive_status = models.Status.objects.create(
user=self.inactive_user, content="also don't delete me"
)
self.deleted_status = models.Status.objects.create(
user=self.deleted_user, content="yes, delete me"
)
initdb.init_groups()
initdb.init_permissions()
self.migrate_from = [("bookwyrm", self.migrate_from)]
self.migrate_to = [("bookwyrm", self.migrate_to)]
executor = MigrationExecutor(connection)
old_apps = executor.loader.project_state(self.migrate_from).apps
# Reverse to the original migration
executor.migrate(self.migrate_from)
self.setUpBeforeMigration(old_apps)
# Run the migration to test
executor = MigrationExecutor(connection)
executor.loader.build_graph() # reload.
with patch("bookwyrm.activitystreams.remove_status_task.delay"):
executor.migrate(self.migrate_to)
self.apps = executor.loader.project_state(self.migrate_to).apps
def setUpBeforeMigration(self, apps):
pass
def test_user_data_deleted(self):
"""Make sure that only the right data was deleted"""
self.active_user.refresh_from_db()
self.inactive_user.refresh_from_db()
self.deleted_user.refresh_from_db()
self.active_status.refresh_from_db()
self.inactive_status.refresh_from_db()
self.deleted_status.refresh_from_db()
self.assertTrue(self.active_user.is_active)
self.assertFalse(self.active_user.is_deleted)
self.assertEqual(self.active_user.name, "a name")
self.assertNotEqual(self.deleted_user.email, "activeuser@activeuser.activeuser")
self.assertFalse(self.active_status.deleted)
self.assertEqual(self.active_status.content, "don't delete me")
self.assertFalse(self.inactive_user.is_active)
self.assertFalse(self.inactive_user.is_deleted)
self.assertEqual(self.inactive_user.name, "name name")
self.assertNotEqual(
self.deleted_user.email, "inactiveuser@inactiveuser.inactiveuser"
)
self.assertFalse(self.inactive_status.deleted)
self.assertEqual(self.inactive_status.content, "also don't delete me")
self.assertFalse(self.deleted_user.is_active)
self.assertTrue(self.deleted_user.is_deleted)
self.assertIsNone(self.deleted_user.name)
self.assertNotEqual(
self.deleted_user.email, "deleteduser@deleteduser.deleteduser"
)
self.assertTrue(self.deleted_status.deleted)
self.assertIsNone(self.deleted_status.content)

View file

@ -119,6 +119,25 @@ class ActivitypubMixins(TestCase):
result = models.Edition.find_existing({"openlibraryKey": "OL1234"})
self.assertEqual(result, book)
def test_find_existing_with_id(self, *_):
"""make sure that an "id" field won't produce a match"""
book = models.Edition.objects.create(title="Test edition")
result = models.Edition.find_existing({"id": book.id})
self.assertIsNone(result)
def test_find_existing_with_id_and_match(self, *_):
"""make sure that an "id" field won't produce a match"""
book = models.Edition.objects.create(title="Test edition")
matching_book = models.Edition.objects.create(
title="Another test edition", openlibrary_key="OL1234"
)
result = models.Edition.find_existing(
{"id": book.id, "openlibraryKey": "OL1234"}
)
self.assertEqual(result, matching_book)
def test_get_recipients_public_object(self, *_):
"""determines the recipients for an object's broadcast"""
MockSelf = namedtuple("Self", ("privacy"))

View file

@ -11,7 +11,7 @@ from django.test import TestCase
from django.utils import timezone
from bookwyrm import models, settings
from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10
from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10, normalize_isbn
from bookwyrm.settings import ENABLE_THUMBNAIL_GENERATION
@ -72,6 +72,10 @@ class Book(TestCase):
isbn_10 = isbn_13_to_10(isbn_13)
self.assertEqual(isbn_10, "178816167X")
def test_normalize_isbn(self):
"""Remove misc characters from ISBNs"""
self.assertEqual(normalize_isbn("978-0-4633461-1-2"), "9780463346112")
def test_get_edition_info(self):
"""text slug about an edition"""
book = models.Edition.objects.create(title="Test Edition")

View file

@ -1,7 +1,9 @@
""" testing models """
import json
from unittest.mock import patch
from django.contrib.auth.models import Group
from django.db import IntegrityError
from django.test import TestCase
import responses
@ -9,9 +11,11 @@ from bookwyrm import models
from bookwyrm.management.commands import initdb
from bookwyrm.settings import USE_HTTPS, DOMAIN
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
class User(TestCase):
protocol = "https://" if USE_HTTPS else "http://"
# pylint: disable=invalid-name
@ -26,6 +30,7 @@ class User(TestCase):
local=True,
localname="mouse",
name="hi",
summary="a summary",
bookwyrm_user=False,
)
self.another_user = models.User.objects.create_user(
@ -88,9 +93,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",
},
],
@ -216,19 +223,71 @@ class User(TestCase):
@patch("bookwyrm.suggested_users.remove_user_task.delay")
def test_delete_user(self, _):
"""deactivate a user"""
"""permanently delete a user"""
self.assertTrue(self.user.is_active)
self.assertEqual(self.user.name, "hi")
self.assertEqual(self.user.summary, "a summary")
self.assertEqual(self.user.email, "mouse@mouse.mouse")
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as broadcast_mock:
) as broadcast_mock, patch(
"bookwyrm.models.user.User.erase_user_statuses"
) as erase_statuses_mock:
self.user.delete()
self.assertEqual(erase_statuses_mock.call_count, 1)
# make sure the deletion is broadcast
self.assertEqual(broadcast_mock.call_count, 1)
activity = json.loads(broadcast_mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["object"], self.user.remote_id)
self.user.refresh_from_db()
# the user's account data should be deleted
self.assertIsNone(self.user.name)
self.assertIsNone(self.user.summary)
self.assertNotEqual(self.user.email, "mouse@mouse.mouse")
self.assertFalse(self.user.is_active)
@patch("bookwyrm.suggested_users.remove_user_task.delay")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_status_task.delay")
@patch("bookwyrm.activitystreams.remove_status_task.delay")
def test_delete_user_erase_statuses(self, *_):
"""erase user statuses when user is deleted"""
status = models.Status.objects.create(user=self.user, content="hello")
self.assertFalse(status.deleted)
self.assertIsNotNone(status.content)
self.assertIsNone(status.deleted_date)
self.user.delete()
status.refresh_from_db()
self.assertTrue(status.deleted)
self.assertIsNone(status.content)
self.assertIsNotNone(status.deleted_date)
@patch("bookwyrm.suggested_users.remove_user_task.delay")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_status_task.delay")
def test_delete_user_erase_statuses_invalid(self, *_):
"""erase user statuses when user is deleted"""
status = models.Status.objects.create(user=self.user, content="hello")
self.assertFalse(status.deleted)
self.assertIsNotNone(status.content)
self.assertIsNone(status.deleted_date)
self.user.deactivate()
with self.assertRaises(IntegrityError):
self.user.erase_user_statuses()
status.refresh_from_db()
self.assertFalse(status.deleted)
self.assertIsNotNone(status.content)
self.assertIsNone(status.deleted_date)
def test_admins_no_admins(self):
"""list of admins"""
result = models.User.admins()

View file

@ -29,3 +29,10 @@ class TestISBN(TestCase):
self.assertEqual(hyphenator.hyphenate("9786769533251"), "9786769533251")
# 979-8 (United States) 2300000-3499999 (unassigned)
self.assertEqual(hyphenator.hyphenate("9798311111111"), "9798311111111")
def test_isbn_hyphenation_invalid_data(self):
"""Make sure not to throw an error when a bad ISBN is found"""
# no action taken
self.assertEqual(hyphenator.hyphenate("978-0-4633461-1-2"), "978-0-4633461-1-2")
self.assertEqual(hyphenator.hyphenate("9-0-4633461-1-2"), "9-0-4633461-1-2")
self.assertEqual(hyphenator.hyphenate("90463346112"), "90463346112")

View file

@ -11,6 +11,7 @@ from bookwyrm import models, views
class InboxActivities(TestCase):
"""inbox tests"""
# pylint: disable=invalid-name
def setUp(self):
"""basic user and book data"""
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
@ -97,7 +98,8 @@ class InboxActivities(TestCase):
self.assertEqual(models.Notification.objects.get(), notif)
@patch("bookwyrm.suggested_users.remove_user_task.delay")
def test_delete_user(self, _):
@patch("bookwyrm.activitystreams.remove_status_task.delay")
def test_delete_user(self, *_):
"""delete a user"""
self.assertTrue(models.User.objects.get(username="rat@example.com").is_active)
activity = {

View file

@ -420,21 +420,25 @@ http://www.fish.com/"""
'okay\n\n<a href="http://www.fish.com/">www.fish.com/</a>',
)
def test_format_links_parens(self, *_):
"""find and format urls into a tags"""
url = "http://www.fish.com/"
self.assertEqual(
views.status.format_links(f"({url})"),
f'(<a href="{url}">www.fish.com/</a>)',
)
def test_format_links_punctuation(self, *_):
"""dont take trailing punctuation into account pls"""
url = "http://www.fish.com/"
self.assertEqual(
views.status.format_links(f"{url}."),
f'<a href="{url}">www.fish.com/</a>.',
)
"""test many combinations of brackets, URLs, and punctuation"""
url = "https://bookwyrm.social"
html = f'<a href="{url}">bookwyrm.social</a>'
test_table = [
("punct", f"text and {url}.", f"text and {html}."),
("multi_punct", f"text, then {url}?...", f"text, then {html}?..."),
("bracket_punct", f"here ({url}).", f"here ({html})."),
("punct_bracket", f"there [{url}?]", f"there [{html}?]"),
("punct_bracket_punct", f"not here? ({url}!).", f"not here? ({html}!)."),
(
"multi_punct_bracket",
f"not there ({url}...);",
f"not there ({html}...);",
),
]
for desc, text, output in test_table:
with self.subTest(desc=desc):
self.assertEqual(views.status.format_links(text), output)
def test_format_links_special_chars(self, *_):
"""find and format urls into a tags"""
@ -464,6 +468,13 @@ http://www.fish.com/"""
views.status.format_links(url), f'<a href="{url}">{url[8:]}</a>'
)
def test_format_links_ignore_non_urls(self, *_):
"""formating links should leave plain text untouced"""
text_elision = "> “The distinction is significant.” [...]" # bookwyrm#2993
text_quoteparens = "some kind of gene-editing technology (?)" # bookwyrm#3049
self.assertEqual(views.status.format_links(text_elision), text_elision)
self.assertEqual(views.status.format_links(text_quoteparens), text_quoteparens)
def test_format_mentions_with_at_symbol_links(self, *_):
"""A link with an @username shouldn't treat the username as a mention"""
content = "a link to https://example.com/user/@mouse"

View file

@ -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/?$",

View file

@ -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 (

View file

@ -110,20 +110,20 @@ class ClearCeleryForm(forms.Form):
queues = forms.MultipleChoiceField(
label="Queues",
choices=[
(LOW, "Low prioirty"),
(LOW, "Low priority"),
(MEDIUM, "Medium priority"),
(HIGH, "High priority"),
(STREAMS, "Streams"),
(IMAGES, "Images"),
(SUGGESTED_USERS, "Suggested users"),
(EMAIL, "Email"),
(BROADCAST, "Broadcast"),
(CONNECTORS, "Connectors"),
(LISTS, "Lists"),
(INBOX, "Inbox"),
(EMAIL, "Email"),
(IMAGES, "Images"),
(IMPORTS, "Imports"),
(IMPORT_TRIGGERED, "Import triggered"),
(BROADCAST, "Broadcasts"),
(INBOX, "Inbox"),
(LISTS, "Lists"),
(MISC, "Misc"),
(STREAMS, "Streams"),
(SUGGESTED_USERS, "Suggested users"),
],
widget=forms.CheckboxSelectMultiple,
)

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

View file

@ -1,7 +1,6 @@
""" what are we here for if not for posting """
import re
import logging
from urllib.parse import urlparse
from django.contrib.auth.decorators import login_required
from django.core.validators import URLValidator
@ -297,65 +296,51 @@ def find_or_create_hashtags(content):
def format_links(content):
"""detect and format links"""
validator = URLValidator()
formatted_content = ""
validator = URLValidator(["http", "https"])
schema_re = re.compile(r"\bhttps?://")
split_content = re.split(r"(\s+)", content)
for potential_link in split_content:
if not potential_link:
for i, potential_link in enumerate(split_content):
if not schema_re.search(potential_link):
continue
wrapped = _wrapped(potential_link)
if wrapped:
wrapper_close = potential_link[-1]
formatted_content += potential_link[0]
potential_link = potential_link[1:-1]
ends_with_punctuation = _ends_with_punctuation(potential_link)
if ends_with_punctuation:
punctuation_glyph = potential_link[-1]
potential_link = potential_link[0:-1]
# Strip surrounding brackets and trailing punctuation.
prefix, potential_link, suffix = _unwrap(potential_link)
try:
# raises an error on anything that's not a valid link
validator(potential_link)
# use everything but the scheme in the presentation of the link
url = urlparse(potential_link)
link = url.netloc + url.path + url.params
if url.query != "":
link += "?" + url.query
if url.fragment != "":
link += "#" + url.fragment
formatted_content += f'<a href="{potential_link}">{link}</a>'
link = schema_re.sub("", potential_link)
split_content[i] = f'{prefix}<a href="{potential_link}">{link}</a>{suffix}'
except (ValidationError, UnicodeError):
formatted_content += potential_link
pass
if wrapped:
formatted_content += wrapper_close
if ends_with_punctuation:
formatted_content += punctuation_glyph
return formatted_content
return "".join(split_content)
def _wrapped(text):
"""check if a line of text is wrapped"""
wrappers = [("(", ")"), ("[", "]"), ("{", "}")]
for wrapper in wrappers:
def _unwrap(text):
"""split surrounding brackets and trailing punctuation from a string of text"""
punct = re.compile(r'([.,;:!?"’”»]+)$')
prefix = suffix = ""
if punct.search(text):
# Move punctuation to suffix segment.
text, suffix, _ = punct.split(text)
for wrapper in ("()", "[]", "{}"):
if text[0] == wrapper[0] and text[-1] == wrapper[-1]:
return True
return False
# Split out wrapping chars.
suffix = text[-1] + suffix
prefix, text = text[:1], text[1:-1]
break # Nested wrappers not supported atm.
if punct.search(text):
# Move inner punctuation to suffix segment.
text, inner_punct, _ = punct.split(text)
suffix = inner_punct + suffix
def _ends_with_punctuation(text):
"""check if a line of text ends with a punctuation glyph"""
glyphs = [".", ",", ";", ":", "!", "?", "", "", '"', "»"]
for glyph in glyphs:
if text[-1] == glyph:
return True
return False
return prefix, text, suffix
def to_markdown(content):

View file

@ -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",

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 06:50\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-11 06:52\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Catalan\n"
"Language: ca\n"
@ -1372,8 +1372,8 @@ msgstr "Edicions de %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Edicions de <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr "Edicions de <a href=\"%(work_path)s\"><i>\"%(work_title)s\"</i></a>"
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,15 +2805,10 @@ msgstr "Fitxer CSV no vàlid"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgstr[0] "\n"
"Actualment, es permet la importació de %(display_size)s llibres cada %(import_limit_reset)s dies. "
msgstr[1] ""
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] "Actualment, se't permet la importació de %(import_size_limit)s llibres cada %(import_limit_reset)s dies."
#: bookwyrm/templates/import/import.html:27
#, python-format

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 16:03\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 18:13\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: German\n"
"Language: de\n"
@ -1372,8 +1372,8 @@ msgstr "Ausgaben von %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Ausgaben von <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,15 +2805,10 @@ msgstr "Keine gültige CSV-Datei"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] "\n"
"Momentan darfst du alle %(import_limit_reset)s Tage %(import_size_limit)s Bücher importieren. "
msgstr[1] ""
#: bookwyrm/templates/import/import.html:27
#, python-format

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 00:08\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 19:32\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Esperanto\n"
"Language: eo\n"
@ -1372,8 +1372,8 @@ msgstr "Eldonoj de %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Eldonoj de <a href=\"%(work_path)s\">«%(work_title)s»</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr "Eldonoj de <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,14 +2805,10 @@ msgstr "La CSV-a dosiero ne validas"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgstr[0] ""
msgstr[1] ""
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] "Aktuale vi rajtas importi %(display_size)s librojn ĉiun %(import_limit_reset)s tagon."
msgstr[1] "Aktuale vi rajtas importi %(import_size_limit)s librojn ĉiujn %(import_limit_reset)s tagojn."
#: bookwyrm/templates/import/import.html:27
#, python-format

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 14:47\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-30 00:47\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Spanish\n"
"Language: es\n"
@ -1035,7 +1035,7 @@ msgstr "Tus citas"
#: bookwyrm/templates/book/book.html:360
msgid "Subjects"
msgstr "Sujetos"
msgstr "Temas"
#: bookwyrm/templates/book/book.html:372
msgid "Places"
@ -1372,8 +1372,8 @@ msgstr "Ediciones de %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Ediciones de <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr "Ediciones de <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,18 +2805,10 @@ msgstr "No es un archivo CSV válido"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgstr[0] "\n"
" Actualmente, puedes importar %(display_size)s libros cada %(import_limit_reset)s días.\n"
" "
msgstr[1] "\n"
" Actualmente, puedes importar %(import_size_limit)s libros cada %(import_limit_reset)s días.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] "Actualmente, puedes importar %(display_size)s libros cada %(import_limit_reset)s días."
msgstr[1] "Actualmente, puedes importar %(import_size_limit)s libros cada %(import_limit_reset)s días."
#: bookwyrm/templates/import/import.html:27
#, python-format
@ -6162,7 +6154,7 @@ msgstr "Apoya a %(site_name)s en <a href=\"%(support_link)s\" target=\"_blank\"
#: bookwyrm/templates/snippets/footer.html:49
msgid "BookWyrm's source code is freely available. You can contribute or report issues on <a href=\"https://github.com/bookwyrm-social/bookwyrm\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">GitHub</a>."
msgstr "BookWyrm es software libre y de código abierto. Puedes contribuir o reportar problemas en <a href=\"https://github.com/bookwyrm-social/bookwyrm\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">GitHub</a>."
msgstr "BookWyrm es software de código abierto. Puedes contribuir o reportar problemas en <a href=\"https://github.com/bookwyrm-social/bookwyrm\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">GitHub</a>."
#: bookwyrm/templates/snippets/form_rate_stars.html:20
#: bookwyrm/templates/snippets/stars.html:23

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 00:08\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 18:13\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Basque\n"
"Language: eu\n"
@ -1372,8 +1372,8 @@ msgstr "%(book_title)s(r)en edizioak"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "<a href=\"%(work_path)s\">\"%(work_title)s\"</a>-ren edizioak"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,12 +2805,8 @@ msgstr "CSV fitxategia ez da baliozkoa"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-29 23:37\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 18:13\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Finnish\n"
"Language: fi\n"
@ -1372,8 +1372,8 @@ msgstr "Kirjan %(book_title)s laitokset"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Kirjan <a href=\"%(work_path)s\">\"%(work_title)s\"</a> laitokset"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,12 +2805,8 @@ msgstr "Epäkelpo CSV-tiedosto"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 00:08\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-31 20:26\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: French\n"
"Language: fr\n"
@ -310,47 +310,47 @@ msgstr "Commentaire"
#: bookwyrm/models/report.py:85
msgid "Resolved report"
msgstr ""
msgstr "Signalement résolu"
#: bookwyrm/models/report.py:86
msgid "Re-opened report"
msgstr ""
msgstr "Ouvrir le signalement de nouveau"
#: bookwyrm/models/report.py:87
msgid "Messaged reporter"
msgstr ""
msgstr "Rapporteur contacté"
#: bookwyrm/models/report.py:88
msgid "Messaged reported user"
msgstr ""
msgstr "Compte signalé contacté"
#: bookwyrm/models/report.py:89
msgid "Suspended user"
msgstr ""
msgstr "Compte suspendu"
#: bookwyrm/models/report.py:90
msgid "Un-suspended user"
msgstr ""
msgstr "Compte nonsuspendu"
#: bookwyrm/models/report.py:91
msgid "Changed user permission level"
msgstr ""
msgstr "Niveau des permissions utilisateur modifié"
#: bookwyrm/models/report.py:92
msgid "Deleted user account"
msgstr ""
msgstr "Compte supprimé"
#: bookwyrm/models/report.py:93
msgid "Blocked domain"
msgstr ""
msgstr "Domaine bloqué"
#: bookwyrm/models/report.py:94
msgid "Approved domain"
msgstr ""
msgstr "Domaine approuvé"
#: bookwyrm/models/report.py:95
msgid "Deleted item"
msgstr ""
msgstr "Item supprimé"
#: bookwyrm/models/user.py:32 bookwyrm/templates/book/book.html:307
msgid "Reviews"
@ -378,7 +378,7 @@ msgstr "Accueil"
#: bookwyrm/settings.py:224
msgid "Books Timeline"
msgstr "Actualité de mes livres"
msgstr "Mon fil dactualité littéraire"
#: bookwyrm/settings.py:224
#: bookwyrm/templates/guided_tour/user_profile.html:101
@ -434,7 +434,7 @@ msgstr "Lietuvių (Lituanien)"
#: bookwyrm/settings.py:307
msgid "Nederlands (Dutch)"
msgstr ""
msgstr "PaysBas (Néerlandais)"
#: bookwyrm/settings.py:308
msgid "Norsk (Norwegian)"
@ -1076,11 +1076,11 @@ msgstr "ISBN:"
#: bookwyrm/templates/book/book_identifiers.html:12
#: bookwyrm/templates/book/book_identifiers.html:13
msgid "Copy ISBN"
msgstr ""
msgstr "Copier lISBN"
#: bookwyrm/templates/book/book_identifiers.html:16
msgid "Copied ISBN!"
msgstr ""
msgstr "ISBN copié!"
#: bookwyrm/templates/book/book_identifiers.html:23
#: bookwyrm/templates/book/edit/edit_book_form.html:352
@ -1245,7 +1245,7 @@ msgstr "Titre:"
#: bookwyrm/templates/book/edit/edit_book_form.html:35
msgid "Sort Title:"
msgstr ""
msgstr "Titre de tri :"
#: bookwyrm/templates/book/edit/edit_book_form.html:44
msgid "Subtitle:"
@ -1372,8 +1372,8 @@ msgstr "Éditions de %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Éditions de <a href=\"%(work_path)s\">« %(work_title)s»</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr "Éditions de <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2268,7 +2268,7 @@ msgstr "Responsable"
#: bookwyrm/templates/groups/user_groups.html:35
msgid "No groups found."
msgstr ""
msgstr "Aucun groupe trouvé."
#: bookwyrm/templates/guided_tour/book.html:10
msgid "This is home page of a book. Let's see what you can do while you're here!"
@ -2805,19 +2805,15 @@ msgstr "Fichier CSV non valide"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgstr[0] ""
msgstr[1] ""
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] "Vous êtes actuellement autorisé à importer %(display_size)s livres tous les %(import_limit_reset)s jours."
msgstr[1] "Vous avez le droit dimporter %(display_size)s livres chaque %(import_limit_reset)s jours actuellement."
#: bookwyrm/templates/import/import.html:27
#, python-format
msgid "You have %(display_left)s left."
msgstr ""
msgstr "Encore %(display_left)s."
#: bookwyrm/templates/import/import.html:34
#, python-format
@ -3483,7 +3479,7 @@ msgstr "Sauvegardé"
#: bookwyrm/templates/lists/list_items.html:50
msgid "No lists found."
msgstr ""
msgstr "Aucune liste trouvée."
#: bookwyrm/templates/lists/lists.html:14 bookwyrm/templates/user/lists.html:14
msgid "Your Lists"
@ -4575,7 +4571,7 @@ msgstr "Queues"
#: bookwyrm/templates/settings/celery.html:26
msgid "Streams"
msgstr ""
msgstr "Flux"
#: bookwyrm/templates/settings/celery.html:32
msgid "Broadcasts"
@ -4583,15 +4579,15 @@ msgstr "Diffusion"
#: bookwyrm/templates/settings/celery.html:38
msgid "Inbox"
msgstr ""
msgstr "Boîte de réception"
#: bookwyrm/templates/settings/celery.html:51
msgid "Import triggered"
msgstr ""
msgstr "Import déclenché"
#: bookwyrm/templates/settings/celery.html:57
msgid "Connectors"
msgstr ""
msgstr "Connecteurs"
#: bookwyrm/templates/settings/celery.html:64
#: bookwyrm/templates/settings/site.html:91
@ -4600,7 +4596,7 @@ msgstr "Images"
#: bookwyrm/templates/settings/celery.html:70
msgid "Suggested Users"
msgstr ""
msgstr "Comptes suggérés"
#: bookwyrm/templates/settings/celery.html:83
#: bookwyrm/templates/settings/invites/manage_invite_requests.html:43
@ -4610,7 +4606,7 @@ msgstr "Email"
#: bookwyrm/templates/settings/celery.html:89
msgid "Misc"
msgstr ""
msgstr "Divers"
#: bookwyrm/templates/settings/celery.html:96
msgid "Low priority"
@ -5424,22 +5420,22 @@ msgstr "Liens signalés"
#: bookwyrm/templates/settings/reports/report.html:66
msgid "Moderation Activity"
msgstr ""
msgstr "Activité de la modération"
#: bookwyrm/templates/settings/reports/report.html:73
#, python-format
msgid "<a href=\"%(user_link)s\">%(user)s</a> opened this report"
msgstr ""
msgstr "<a href=\"%(user_link)s\">%(user)s</a> a ouvert ce signalement"
#: bookwyrm/templates/settings/reports/report.html:86
#, python-format
msgid "<a href=\"%(user_link)s\">%(user)s</a> commented on this report:"
msgstr ""
msgstr "<a href=\"%(user_link)s\">%(user)s</a> a commenté ce signalement :"
#: bookwyrm/templates/settings/reports/report.html:90
#, python-format
msgid "<a href=\"%(user_link)s\">%(user)s</a> took an action on this report:"
msgstr ""
msgstr "<a href=\"%(user_link)s\">%(user)s</a> a traité ce signalement :"
#: bookwyrm/templates/settings/reports/report_header.html:6
#, python-format
@ -5463,7 +5459,7 @@ msgstr "Signalement #%(report_id)s : compte @%(username)s"
#: bookwyrm/templates/settings/reports/report_links_table.html:19
msgid "Approve domain"
msgstr ""
msgstr "Approuver le domaine"
#: bookwyrm/templates/settings/reports/report_links_table.html:26
msgid "Block domain"
@ -6053,7 +6049,7 @@ msgstr "Commentaire:"
#: bookwyrm/templates/snippets/create_status/post_options_block.html:19
msgid "Update"
msgstr ""
msgstr "Mettre à jour"
#: bookwyrm/templates/snippets/create_status/post_options_block.html:21
msgid "Post"
@ -6711,8 +6707,8 @@ msgstr "A rejoint ce serveur %(date)s"
#, python-format
msgid "%(display_count)s follower"
msgid_plural "%(display_count)s followers"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "%(display_count)s abonné⋅e"
msgstr[1] "%(display_count)s abonné⋅es"
#: bookwyrm/templates/user/user_preview.html:31
#, python-format

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 04:25\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-20 13:05\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Galician\n"
"Language: gl\n"
@ -497,7 +497,7 @@ msgstr "Acerca de"
#: bookwyrm/templates/get_started/layout.html:22
#, python-format
msgid "Welcome to %(site_name)s!"
msgstr "Sexas ben vida a %(site_name)s!"
msgstr "Recibe a benvida a %(site_name)s!"
#: bookwyrm/templates/about/about.html:25
#, python-format
@ -1372,8 +1372,8 @@ msgstr "Edicións de %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Edicións de <a href=\"%(work_path)s\">%(work_title)s</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr "Edicións de <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,18 +2805,10 @@ msgstr "Non é un ficheiro CSV válido"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgstr[0] "\n"
" Actualmente, tes permiso para importar %(display_size)s libros cada %(import_limit_reset)s día.\n"
" "
msgstr[1] "\n"
" Actualmente, tes permiso para importar %(import_size_limit)s libros cada %(import_limit_reset)s días.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] "Actualmente podes importar %(display_size)s libros cada %(import_limit_reset)s día."
msgstr[1] "Actualmente podes importar %(import_size_limit)s libros cada %(import_limit_reset)s días."
#: bookwyrm/templates/import/import.html:27
#, python-format

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 09:30\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 19:32\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Italian\n"
"Language: it\n"
@ -310,15 +310,15 @@ msgstr "Commenta"
#: bookwyrm/models/report.py:85
msgid "Resolved report"
msgstr ""
msgstr "Segnalazione risolta"
#: bookwyrm/models/report.py:86
msgid "Re-opened report"
msgstr ""
msgstr "Segnalazione riaperta"
#: bookwyrm/models/report.py:87
msgid "Messaged reporter"
msgstr ""
msgstr "Messaggio inviato al segnalatore"
#: bookwyrm/models/report.py:88
msgid "Messaged reported user"
@ -326,11 +326,11 @@ msgstr ""
#: bookwyrm/models/report.py:89
msgid "Suspended user"
msgstr ""
msgstr "Utente sospeso"
#: bookwyrm/models/report.py:90
msgid "Un-suspended user"
msgstr ""
msgstr "Utente riattivato"
#: bookwyrm/models/report.py:91
msgid "Changed user permission level"
@ -1372,8 +1372,8 @@ msgstr "Edizioni di %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Edizioni di <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr "Edizioni di <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,14 +2805,10 @@ msgstr "Non è un file di csv valido"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""
msgstr[1] "Al momento puoi importare %(import_size_limit)s libri ogni %(import_limit_reset)s giorni."
#: bookwyrm/templates/import/import.html:27
#, python-format

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 00:08\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 18:13\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Lithuanian\n"
"Language: lt\n"
@ -1384,8 +1384,8 @@ msgstr "Knygos %(book_title)s leidimai"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "<a href=\"%(work_path)s\">\"%(work_title)s\"</a> leidimai"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2825,12 +2825,8 @@ msgstr "Netinkamas CSV failas"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 08:16\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 19:32\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Dutch\n"
"Language: nl\n"
@ -1372,8 +1372,8 @@ msgstr "Edities van %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Edities van <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr "Edities van <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,16 +2805,10 @@ msgstr "Geen geldig CSV-bestand"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgstr[0] "\n"
"Momenteel mag je elke %(import_limit_reset)s dag %(display_size)s boeken importeren. "
msgstr[1] "\n"
"Momenteel mag je elke %(import_limit_reset)s dagen %(import_size_limit)s boeken importeren. "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] "Momenteel mag je %(display_size)s boek importeren elke %(import_limit_reset)s dagen."
msgstr[1] "Momenteel mag je %(import_size_limit)s boeken importeren elke %(import_limit_reset)s dagen."
#: bookwyrm/templates/import/import.html:27
#, python-format

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 00:08\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-09 19:52\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Norwegian\n"
"Language: no\n"
@ -314,31 +314,31 @@ msgstr ""
#: bookwyrm/models/report.py:86
msgid "Re-opened report"
msgstr ""
msgstr "Gjenåpnet rapport"
#: bookwyrm/models/report.py:87
msgid "Messaged reporter"
msgstr ""
msgstr "Melding sendt til rapportør"
#: bookwyrm/models/report.py:88
msgid "Messaged reported user"
msgstr ""
msgstr "Melding sendt til rapportert bruker"
#: bookwyrm/models/report.py:89
msgid "Suspended user"
msgstr ""
msgstr "Deaktivert bruker"
#: bookwyrm/models/report.py:90
msgid "Un-suspended user"
msgstr ""
msgstr "Reaktivert bruker"
#: bookwyrm/models/report.py:91
msgid "Changed user permission level"
msgstr ""
msgstr "Endret brukerens rettighetsnivå"
#: bookwyrm/models/report.py:92
msgid "Deleted user account"
msgstr ""
msgstr "Slettet brukerkonto"
#: bookwyrm/models/report.py:93
msgid "Blocked domain"
@ -1372,8 +1372,8 @@ msgstr "Utgaver av %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Utgaver av <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,12 +2805,8 @@ msgstr "Ikke en gyldig CSV-fil"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 00:08\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-03 01:28\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Polish\n"
"Language: pl\n"
@ -350,7 +350,7 @@ msgstr ""
#: bookwyrm/models/report.py:95
msgid "Deleted item"
msgstr ""
msgstr "Usunięty element"
#: bookwyrm/models/user.py:32 bookwyrm/templates/book/book.html:307
msgid "Reviews"
@ -434,7 +434,7 @@ msgstr "Lietuvių (Litewski)"
#: bookwyrm/settings.py:307
msgid "Nederlands (Dutch)"
msgstr ""
msgstr "Holenderski"
#: bookwyrm/settings.py:308
msgid "Norsk (Norwegian)"
@ -502,7 +502,7 @@ msgstr "Witaj na %(site_name)s!"
#: bookwyrm/templates/about/about.html:25
#, python-format
msgid "%(site_name)s is part of <em>BookWyrm</em>, a network of independent, self-directed communities for readers. While you can interact seamlessly with users anywhere in the <a href=\"https://joinbookwyrm.com/instances/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">BookWyrm network</a>, this community is unique."
msgstr ""
msgstr "%(site_name)s jest częścią <em>BookWyrm</em>, sieci niezależnych, samostanowiących społeczności czytelników. Możesz beproblemowo wchodzić w interakcje z użytkownikami gdziekolwiek w <a href=\"https://joinbookwyrm.com/instances/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"> sieci BookWyrm</a>, ta społeczność jest wyjątkowa."
#: bookwyrm/templates/about/about.html:45
#, python-format
@ -521,7 +521,7 @@ msgstr "<a href=\"%(book_path)s\"><em>%(title)s</em></a> ma najbardziej podzielo
#: bookwyrm/templates/about/about.html:94
msgid "Track your reading, talk about books, write reviews, and discover what to read next. Always ad-free, anti-corporate, and community-oriented, BookWyrm is human-scale software, designed to stay small and personal. If you have feature requests, bug reports, or grand dreams, <a href=\"https://joinbookwyrm.com/get-involved\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">reach out</a> and make yourself heard."
msgstr ""
msgstr "Śledź swój postęp czytelniczy, rozmawiaj o książkach, pisz opinie i odkrywaj co czytać następne. Na zawsze bez reklam, antykorporacyjne i skierowane w stronę społeczności, BookWyrm jest programem dla ludzi, stworzonym, by pozostać małym i personalnym. Jeśli masz pomysł, zauważył_ś błąd, albo masz wielkie marzenie, <a href=\"https://joinbookwyrm.com/get-involved\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"> złoś się </a> i pozwól się wysłuchać."
#: bookwyrm/templates/about/about.html:105
msgid "Meet your admins"
@ -672,7 +672,7 @@ msgstr "Przekłada się to na średnio %(pages)s stron na książkę."
#, python-format
msgid "(No page data was available for %(no_page_number)s book)"
msgid_plural "(No page data was available for %(no_page_number)s books)"
msgstr[0] ""
msgstr[0] "(Nie mamy informacji o liczbie stron dla książki %(no_page_number)s)"
msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
@ -778,7 +778,7 @@ msgstr "Zobacz wpis ISNI"
#: bookwyrm/templates/author/author.html:95
#: bookwyrm/templates/book/book.html:173
msgid "View on ISFDB"
msgstr ""
msgstr "Zobacz na ISFDB"
#: bookwyrm/templates/author/author.html:100
#: bookwyrm/templates/author/sync_modal.html:5
@ -1116,7 +1116,7 @@ msgstr ""
#: bookwyrm/templates/book/book_identifiers.html:51
msgid "Goodreads:"
msgstr ""
msgstr "Goodreads:"
#: bookwyrm/templates/book/cover_add_modal.html:5
msgid "Add cover"
@ -1257,7 +1257,7 @@ msgstr "Tytuł:"
#: bookwyrm/templates/book/edit/edit_book_form.html:35
msgid "Sort Title:"
msgstr ""
msgstr "Sortuj Według Tytułu:"
#: bookwyrm/templates/book/edit/edit_book_form.html:44
msgid "Subtitle:"
@ -1384,8 +1384,8 @@ msgstr "Edycje %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Edycje <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr "Edycje <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -1567,7 +1567,7 @@ msgstr ""
#: bookwyrm/templates/book/series.html:27
#, python-format
msgid "Book %(series_number)s"
msgstr ""
msgstr "Książka%(series_number)s"
#: bookwyrm/templates/book/series.html:27
msgid "Unsorted Book"
@ -2825,12 +2825,8 @@ msgstr "To nie jest prawidłowy plik CSV"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 18:50\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 18:13\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt\n"
@ -1372,8 +1372,8 @@ msgstr "Edições de %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Edições de <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,12 +2805,8 @@ msgstr ""
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 00:08\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 18:13\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Portuguese\n"
"Language: pt\n"
@ -1372,8 +1372,8 @@ msgstr "Edições de %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Edições de <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,12 +2805,8 @@ msgstr "Não é um ficheiro CSV válido"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 00:08\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 18:13\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Romanian\n"
"Language: ro\n"
@ -1378,8 +1378,8 @@ msgstr "Ediții ale %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Ediții ale <a href=\"%(work_path)s\">%(work_title)s</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2815,12 +2815,8 @@ msgstr ""
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 09:30\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 18:13\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Swedish\n"
"Language: sv\n"
@ -1372,8 +1372,8 @@ msgstr "Utgåvor av %(book_title)s"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "Utgåvor av <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2805,12 +2805,8 @@ msgstr "Inte en giltig CSV-fil"
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 00:08\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-02 18:13\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Chinese Simplified\n"
"Language: zh\n"
@ -1366,8 +1366,8 @@ msgstr "%(book_title)s 的各版本"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "<a href=\"%(work_path)s\">《%(work_title)s》</a> 的各版本"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2795,12 +2795,8 @@ msgstr ""
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
#: bookwyrm/templates/import/import.html:27

View file

@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"PO-Revision-Date: 2023-09-28 00:08\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2023-10-29 07:42\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Chinese Traditional\n"
"Language: zh\n"
@ -173,7 +173,7 @@ msgstr ""
#: bookwyrm/models/book.py:283
msgid "Audiobook"
msgstr ""
msgstr "有聲書"
#: bookwyrm/models/book.py:284
msgid "eBook"
@ -181,15 +181,15 @@ msgstr "電子書"
#: bookwyrm/models/book.py:285
msgid "Graphic novel"
msgstr ""
msgstr "圖像小說"
#: bookwyrm/models/book.py:286
msgid "Hardcover"
msgstr ""
msgstr "精裝書"
#: bookwyrm/models/book.py:287
msgid "Paperback"
msgstr ""
msgstr "平裝書"
#: bookwyrm/models/federated_server.py:11
#: bookwyrm/templates/settings/federation/edit_instance.html:55
@ -267,15 +267,15 @@ msgstr "活躍"
#: bookwyrm/models/import_job.py:49 bookwyrm/templates/import/import.html:172
msgid "Complete"
msgstr ""
msgstr "已完成"
#: bookwyrm/models/import_job.py:50
msgid "Stopped"
msgstr ""
msgstr "已停止"
#: bookwyrm/models/import_job.py:83 bookwyrm/models/import_job.py:91
msgid "Import stopped"
msgstr ""
msgstr "匯入已停止"
#: bookwyrm/models/import_job.py:363 bookwyrm/models/import_job.py:388
msgid "Error loading book"
@ -287,20 +287,20 @@ msgstr ""
#: bookwyrm/models/link.py:51
msgid "Free"
msgstr ""
msgstr "免費"
#: bookwyrm/models/link.py:52
msgid "Purchasable"
msgstr ""
msgstr "可購買"
#: bookwyrm/models/link.py:53
msgid "Available for loan"
msgstr ""
msgstr "可借閱"
#: bookwyrm/models/link.py:70
#: bookwyrm/templates/settings/link_domains/link_domains.html:23
msgid "Approved"
msgstr ""
msgstr "已核准"
#: bookwyrm/models/report.py:84
#: bookwyrm/templates/settings/reports/report.html:115
@ -310,11 +310,11 @@ msgstr "評論"
#: bookwyrm/models/report.py:85
msgid "Resolved report"
msgstr ""
msgstr "已處理的舉報"
#: bookwyrm/models/report.py:86
msgid "Re-opened report"
msgstr ""
msgstr "已重新打開的舉報"
#: bookwyrm/models/report.py:87
msgid "Messaged reporter"
@ -358,15 +358,15 @@ msgstr "書評"
#: bookwyrm/models/user.py:33
msgid "Comments"
msgstr ""
msgstr "評論"
#: bookwyrm/models/user.py:34
msgid "Quotations"
msgstr ""
msgstr "引用"
#: bookwyrm/models/user.py:35
msgid "Everything else"
msgstr ""
msgstr "所有其他內容"
#: bookwyrm/settings.py:223
msgid "Home Timeline"
@ -378,7 +378,7 @@ msgstr "主頁"
#: bookwyrm/settings.py:224
msgid "Books Timeline"
msgstr ""
msgstr "書目時間線"
#: bookwyrm/settings.py:224
#: bookwyrm/templates/guided_tour/user_profile.html:101
@ -394,7 +394,7 @@ msgstr "English英語"
#: bookwyrm/settings.py:297
msgid "Català (Catalan)"
msgstr ""
msgstr "Català (加泰羅尼亞語)"
#: bookwyrm/settings.py:298
msgid "Deutsch (German)"
@ -402,7 +402,7 @@ msgstr "Deutsch德語"
#: bookwyrm/settings.py:299
msgid "Esperanto (Esperanto)"
msgstr ""
msgstr "Esperanto (世界語)"
#: bookwyrm/settings.py:300
msgid "Español (Spanish)"
@ -410,19 +410,19 @@ msgstr "Español西班牙語"
#: bookwyrm/settings.py:301
msgid "Euskara (Basque)"
msgstr ""
msgstr "Euskara (巴斯克語)"
#: bookwyrm/settings.py:302
msgid "Galego (Galician)"
msgstr ""
msgstr "Galego (加利西亞語)"
#: bookwyrm/settings.py:303
msgid "Italiano (Italian)"
msgstr ""
msgstr "Italiano (意大利語)"
#: bookwyrm/settings.py:304
msgid "Suomi (Finnish)"
msgstr ""
msgstr "Suomi (芬蘭語)"
#: bookwyrm/settings.py:305
msgid "Français (French)"
@ -430,35 +430,35 @@ msgstr "Français法語"
#: bookwyrm/settings.py:306
msgid "Lietuvių (Lithuanian)"
msgstr ""
msgstr "Lietuvių (立陶宛語)"
#: bookwyrm/settings.py:307
msgid "Nederlands (Dutch)"
msgstr ""
msgstr "Nederlands (荷蘭語)"
#: bookwyrm/settings.py:308
msgid "Norsk (Norwegian)"
msgstr ""
msgstr "Norsk (挪威語)"
#: bookwyrm/settings.py:309
msgid "Polski (Polish)"
msgstr ""
msgstr "Polski (波蘭語)"
#: bookwyrm/settings.py:310
msgid "Português do Brasil (Brazilian Portuguese)"
msgstr ""
msgstr "Português do Brasil (巴西葡萄牙語)"
#: bookwyrm/settings.py:311
msgid "Português Europeu (European Portuguese)"
msgstr ""
msgstr "Português Europeu (歐洲葡萄牙語)"
#: bookwyrm/settings.py:312
msgid "Română (Romanian)"
msgstr ""
msgstr "Română (羅馬尼亞語)"
#: bookwyrm/settings.py:313
msgid "Svenska (Swedish)"
msgstr ""
msgstr "Svenska (瑞典語)"
#: bookwyrm/settings.py:314
msgid "简体中文 (Simplified Chinese)"
@ -491,7 +491,7 @@ msgstr "某些東西出錯了!抱歉。"
#: bookwyrm/templates/about/about.html:9
#: bookwyrm/templates/about/layout.html:35
msgid "About"
msgstr ""
msgstr "關於"
#: bookwyrm/templates/about/about.html:21
#: bookwyrm/templates/get_started/layout.html:22
@ -502,12 +502,12 @@ msgstr "歡迎來到 %(site_name)s"
#: bookwyrm/templates/about/about.html:25
#, python-format
msgid "%(site_name)s is part of <em>BookWyrm</em>, a network of independent, self-directed communities for readers. While you can interact seamlessly with users anywhere in the <a href=\"https://joinbookwyrm.com/instances/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">BookWyrm network</a>, this community is unique."
msgstr ""
msgstr "%(site_name)s 是 <em>BookWyrm</em> 的一部分,這是一個為讀者建立的獨立、自我導向的社區網絡。雖然您可以在 <a href=\"https://joinbookwyrm.com/instances/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">BookWyrm 網絡</a>中任何地方的用戶無縫互動,但這個社區是獨一無二的。"
#: bookwyrm/templates/about/about.html:45
#, python-format
msgid "<a href=\"%(book_path)s\"><em>%(title)s</em></a> is %(site_name)s's most beloved book, with an average rating of %(rating)s out of 5."
msgstr ""
msgstr "<a href=\"%(book_path)s\"><em>%(title)s</em></a> 是 %(site_name)s 最受歡迎的書,平均得分為 %(rating)s滿分五分"
#: bookwyrm/templates/about/about.html:64
#, python-format
@ -703,13 +703,13 @@ msgstr[0] ""
#: bookwyrm/templates/annual_summary/layout.html:211
msgid "Way to go!"
msgstr ""
msgstr "還不錯!"
#: bookwyrm/templates/annual_summary/layout.html:226
#, python-format
msgid "%(display_name)s left %(ratings_total)s rating, <br />their average rating is %(rating_average)s"
msgid_plural "%(display_name)s left %(ratings_total)s ratings, <br />their average rating is %(rating_average)s"
msgstr[0] ""
msgstr[0] "%(display_name)s 留下了 %(ratings_total)s 條評分,<br />他的平均評分是 %(rating_average)s"
#: bookwyrm/templates/annual_summary/layout.html:240
msgid "Their best rated review"
@ -732,7 +732,7 @@ msgstr "編輯作者"
#: bookwyrm/templates/author/author.html:36
msgid "Author details"
msgstr ""
msgstr "作者詳情"
#: bookwyrm/templates/author/author.html:40
#: bookwyrm/templates/author/edit_author.html:42
@ -749,7 +749,7 @@ msgstr "逝世:"
#: bookwyrm/templates/author/author.html:66
msgid "External links"
msgstr ""
msgstr "外部連結"
#: bookwyrm/templates/author/author.html:71
msgid "Wikipedia"
@ -757,23 +757,23 @@ msgstr "維基百科"
#: bookwyrm/templates/author/author.html:79
msgid "Website"
msgstr ""
msgstr "網站"
#: bookwyrm/templates/author/author.html:87
msgid "View ISNI record"
msgstr ""
msgstr "查看 ISNI 記錄"
#: bookwyrm/templates/author/author.html:95
#: bookwyrm/templates/book/book.html:173
msgid "View on ISFDB"
msgstr ""
msgstr "在 ISFDB 查看"
#: bookwyrm/templates/author/author.html:100
#: bookwyrm/templates/author/sync_modal.html:5
#: bookwyrm/templates/book/book.html:140
#: bookwyrm/templates/book/sync_modal.html:5
msgid "Load data"
msgstr ""
msgstr "載入資料"
#: bookwyrm/templates/author/author.html:104
#: bookwyrm/templates/book/book.html:144
@ -787,15 +787,15 @@ msgstr "在 Inventaire 檢視"
#: bookwyrm/templates/author/author.html:135
msgid "View on LibraryThing"
msgstr ""
msgstr "在 LibraryThing 查看"
#: bookwyrm/templates/author/author.html:143
msgid "View on Goodreads"
msgstr ""
msgstr "在 Goodreads 查看"
#: bookwyrm/templates/author/author.html:151
msgid "View ISFDB entry"
msgstr ""
msgstr "查看 ISFDB 條目"
#: bookwyrm/templates/author/author.html:166
#, python-format
@ -849,7 +849,7 @@ msgstr "維基百科連結:"
#: bookwyrm/templates/author/edit_author.html:60
msgid "Website:"
msgstr ""
msgstr "網站:"
#: bookwyrm/templates/author/edit_author.html:65
msgid "Birth date:"
@ -883,11 +883,11 @@ msgstr "Goodreads key:"
#: bookwyrm/templates/author/edit_author.html:109
msgid "ISFDB:"
msgstr ""
msgstr "ISFDB"
#: bookwyrm/templates/author/edit_author.html:116
msgid "ISNI:"
msgstr ""
msgstr "ISNI"
#: bookwyrm/templates/author/edit_author.html:126
#: bookwyrm/templates/book/book.html:220
@ -953,7 +953,7 @@ msgstr "確認"
#: bookwyrm/templates/book/book.html:20
msgid "Unable to connect to remote source."
msgstr ""
msgstr "無法連接到遠程數據源。"
#: bookwyrm/templates/book/book.html:71 bookwyrm/templates/book/book.html:72
msgid "Edit Book"
@ -961,7 +961,7 @@ msgstr "編輯書目"
#: bookwyrm/templates/book/book.html:97 bookwyrm/templates/book/book.html:100
msgid "Click to add cover"
msgstr ""
msgstr "點擊添加封面"
#: bookwyrm/templates/book/book.html:106
msgid "Failed to load cover"
@ -969,7 +969,7 @@ msgstr "載入封面失敗"
#: bookwyrm/templates/book/book.html:117
msgid "Click to enlarge"
msgstr ""
msgstr "點擊放大"
#: bookwyrm/templates/book/book.html:196
#, python-format
@ -991,7 +991,7 @@ msgstr "描述:"
#, python-format
msgid "%(count)s edition"
msgid_plural "%(count)s editions"
msgstr[0] ""
msgstr[0] "%(count)s 版次"
#: bookwyrm/templates/book/book.html:246
msgid "You have shelved this edition in:"
@ -1070,11 +1070,11 @@ msgstr "ISBN:"
#: bookwyrm/templates/book/book_identifiers.html:12
#: bookwyrm/templates/book/book_identifiers.html:13
msgid "Copy ISBN"
msgstr ""
msgstr "複製ISBN"
#: bookwyrm/templates/book/book_identifiers.html:16
msgid "Copied ISBN!"
msgstr ""
msgstr "已複製ISBN"
#: bookwyrm/templates/book/book_identifiers.html:23
#: bookwyrm/templates/book/edit/edit_book_form.html:352
@ -1089,16 +1089,16 @@ msgstr "ASIN:"
#: bookwyrm/templates/book/book_identifiers.html:37
#: bookwyrm/templates/book/edit/edit_book_form.html:370
msgid "Audible ASIN:"
msgstr ""
msgstr "Audible ASIN"
#: bookwyrm/templates/book/book_identifiers.html:44
#: bookwyrm/templates/book/edit/edit_book_form.html:379
msgid "ISFDB ID:"
msgstr ""
msgstr "ISFDB ID"
#: bookwyrm/templates/book/book_identifiers.html:51
msgid "Goodreads:"
msgstr ""
msgstr "Goodreads"
#: bookwyrm/templates/book/cover_add_modal.html:5
msgid "Add cover"
@ -1116,7 +1116,7 @@ msgstr "從網址載入封面:"
#: bookwyrm/templates/book/cover_show_modal.html:6
msgid "Book cover preview"
msgstr ""
msgstr "書籍封面預覽"
#: bookwyrm/templates/book/cover_show_modal.html:11
#: bookwyrm/templates/components/inline_form.html:8
@ -1310,16 +1310,16 @@ msgstr "新增作者:"
#: bookwyrm/templates/book/edit/edit_book_form.html:211
#: bookwyrm/templates/book/edit/edit_book_form.html:214
msgid "Add Author"
msgstr ""
msgstr "新增作者"
#: bookwyrm/templates/book/edit/edit_book_form.html:212
#: bookwyrm/templates/book/edit/edit_book_form.html:215
msgid "Jane Doe"
msgstr ""
msgstr "陳大文"
#: bookwyrm/templates/book/edit/edit_book_form.html:221
msgid "Add Another Author"
msgstr ""
msgstr "新增其他作者"
#: bookwyrm/templates/book/edit/edit_book_form.html:231
#: bookwyrm/templates/shelf/shelf.html:147
@ -1337,7 +1337,7 @@ msgstr "格式:"
#: bookwyrm/templates/book/edit/edit_book_form.html:280
msgid "Format details:"
msgstr ""
msgstr "裝訂詳情:"
#: bookwyrm/templates/book/edit/edit_book_form.html:291
msgid "Pages:"
@ -1366,8 +1366,8 @@ msgstr "%(book_title)s 的各版本"
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgstr "<a href=\"%(work_path)s\">\"%(work_title)s\"</a> 的各版本"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
msgid "Can't find the edition you're looking for?"
@ -2795,12 +2795,8 @@ msgstr ""
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid "\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural "\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
#: bookwyrm/templates/import/import.html:27

View file

@ -2,7 +2,7 @@ aiohttp==3.8.5
bleach==5.0.1
celery==5.2.7
colorthief==0.2.1
Django==3.2.20
Django==3.2.23
django-celery-beat==2.4.0
bw-file-resubmit==0.6.0rc2
django-compressor==4.3.1