Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2023-04-25 17:43:00 -07:00
commit a65e6ce423
125 changed files with 1380 additions and 979 deletions

View file

@ -8,7 +8,7 @@ USE_HTTPS=true
DOMAIN=your.domain.here DOMAIN=your.domain.here
EMAIL=your@email.here EMAIL=your@email.here
# Instance defualt language (see options at bookwyrm/settings.py "LANGUAGES" # Instance default language (see options at bookwyrm/settings.py "LANGUAGES"
LANGUAGE_CODE="en-us" LANGUAGE_CODE="en-us"
# Used for deciding which editions to prefer # Used for deciding which editions to prefer
DEFAULT_LANGUAGE="English" DEFAULT_LANGUAGE="English"

View file

@ -127,7 +127,7 @@ class ActivityObject:
if ( if (
allow_create allow_create
and hasattr(model, "ignore_activity") and hasattr(model, "ignore_activity")
and model.ignore_activity(self) and model.ignore_activity(self, allow_external_connections)
): ):
return None return None
@ -241,7 +241,7 @@ class ActivityObject:
return data return data
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
@transaction.atomic @transaction.atomic
def set_related_field( def set_related_field(
model_name, origin_model_name, related_field_name, related_remote_id, data model_name, origin_model_name, related_field_name, related_remote_id, data
@ -384,7 +384,8 @@ def get_activitypub_data(url):
resp = requests.get( resp = requests.get(
url, url,
headers={ headers={
"Accept": "application/json; charset=utf-8", # pylint: disable=line-too-long
"Accept": 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
"Date": now, "Date": now,
"Signature": make_signature("get", sender, url, now), "Signature": make_signature("get", sender, url, now),
}, },

View file

@ -38,11 +38,14 @@ class ActivityStream(RedisStore):
def add_status(self, status, increment_unread=False): def add_status(self, status, increment_unread=False):
"""add a status to users' feeds""" """add a status to users' feeds"""
audience = self.get_audience(status)
# the pipeline contains all the add-to-stream activities # the pipeline contains all the add-to-stream activities
pipeline = self.add_object_to_related_stores(status, execute=False) pipeline = self.add_object_to_stores(
status, self.get_stores_for_users(audience), execute=False
)
if increment_unread: if increment_unread:
for user_id in self.get_audience(status): for user_id in audience:
# add to the unread status count # add to the unread status count
pipeline.incr(self.unread_id(user_id)) pipeline.incr(self.unread_id(user_id))
# add to the unread status count for status type # add to the unread status count for status type
@ -102,9 +105,16 @@ class ActivityStream(RedisStore):
"""go from zero to a timeline""" """go from zero to a timeline"""
self.populate_store(self.stream_id(user.id)) self.populate_store(self.stream_id(user.id))
@tracer.start_as_current_span("ActivityStream._get_audience")
def _get_audience(self, status): # pylint: disable=no-self-use def _get_audience(self, status): # pylint: disable=no-self-use
"""given a status, what users should see it""" """given a status, what users should see it, excluding the author"""
# direct messages don't appeard in feeds, direct comments/reviews/etc do trace.get_current_span().set_attribute("status_type", status.status_type)
trace.get_current_span().set_attribute("status_privacy", status.privacy)
trace.get_current_span().set_attribute(
"status_reply_parent_privacy",
status.reply_parent.privacy if status.reply_parent else None,
)
# direct messages don't appear in feeds, direct comments/reviews/etc do
if status.privacy == "direct" and status.status_type == "Note": if status.privacy == "direct" and status.status_type == "Note":
return [] return []
@ -119,15 +129,13 @@ class ActivityStream(RedisStore):
# only visible to the poster and mentioned users # only visible to the poster and mentioned users
if status.privacy == "direct": if status.privacy == "direct":
audience = audience.filter( audience = audience.filter(
Q(id=status.user.id) # if the user is the post's author Q(id__in=status.mention_users.all()) # if the user is mentioned
| Q(id__in=status.mention_users.all()) # if the user is mentioned
) )
# don't show replies to statuses the user can't see # don't show replies to statuses the user can't see
elif status.reply_parent and status.reply_parent.privacy == "followers": elif status.reply_parent and status.reply_parent.privacy == "followers":
audience = audience.filter( audience = audience.filter(
Q(id=status.user.id) # if the user is the post's author Q(id=status.reply_parent.user.id) # if the user is the OG author
| Q(id=status.reply_parent.user.id) # if the user is the OG author
| ( | (
Q(following=status.user) & Q(following=status.reply_parent.user) Q(following=status.user) & Q(following=status.reply_parent.user)
) # if the user is following both authors ) # if the user is following both authors
@ -136,8 +144,7 @@ class ActivityStream(RedisStore):
# only visible to the poster's followers and tagged users # only visible to the poster's followers and tagged users
elif status.privacy == "followers": elif status.privacy == "followers":
audience = audience.filter( audience = audience.filter(
Q(id=status.user.id) # if the user is the post's author Q(following=status.user) # if the user is following the author
| Q(following=status.user) # if the user is following the author
) )
return audience.distinct() return audience.distinct()
@ -145,10 +152,15 @@ class ActivityStream(RedisStore):
def get_audience(self, status): def get_audience(self, status):
"""given a status, what users should see it""" """given a status, what users should see it"""
trace.get_current_span().set_attribute("stream_id", self.key) trace.get_current_span().set_attribute("stream_id", self.key)
return [user.id for user in self._get_audience(status)] audience = self._get_audience(status)
status_author = models.User.objects.filter(
is_active=True, local=True, id=status.user.id
)
return list({user.id for user in list(audience) + list(status_author)})
def get_stores_for_object(self, obj): def get_stores_for_users(self, user_ids):
return [self.stream_id(user_id) for user_id in self.get_audience(obj)] """convert a list of user ids into redis store ids"""
return [self.stream_id(user_id) for user_id in user_ids]
def get_statuses_for_user(self, user): # pylint: disable=no-self-use def get_statuses_for_user(self, user): # pylint: disable=no-self-use
"""given a user, what statuses should they see on this stream""" """given a user, what statuses should they see on this stream"""
@ -173,11 +185,13 @@ class HomeStream(ActivityStream):
audience = super()._get_audience(status) audience = super()._get_audience(status)
if not audience: if not audience:
return [] return []
# if the user is the post's author
ids_self = [user.id for user in audience.filter(Q(id=status.user.id))]
# if the user is following the author # if the user is following the author
ids_following = [user.id for user in audience.filter(Q(following=status.user))] audience = audience.filter(following=status.user)
return ids_self + ids_following # if the user is the post's author
status_author = models.User.objects.filter(
is_active=True, local=True, id=status.user.id
)
return list({user.id for user in list(audience) + list(status_author)})
def get_statuses_for_user(self, user): def get_statuses_for_user(self, user):
return models.Status.privacy_filter( return models.Status.privacy_filter(
@ -197,11 +211,11 @@ class LocalStream(ActivityStream):
key = "local" key = "local"
def _get_audience(self, status): def get_audience(self, status):
# this stream wants no part in non-public statuses # this stream wants no part in non-public statuses
if status.privacy != "public" or not status.user.local: if status.privacy != "public" or not status.user.local:
return [] return []
return super()._get_audience(status) return super().get_audience(status)
def get_statuses_for_user(self, user): def get_statuses_for_user(self, user):
# all public statuses by a local user # all public statuses by a local user
@ -218,13 +232,6 @@ class BooksStream(ActivityStream):
def _get_audience(self, status): def _get_audience(self, status):
"""anyone with the mentioned book on their shelves""" """anyone with the mentioned book on their shelves"""
# only show public statuses on the books feed,
# and only statuses that mention books
if status.privacy != "public" or not (
status.mention_books.exists() or hasattr(status, "book")
):
return []
work = ( work = (
status.book.parent_work status.book.parent_work
if hasattr(status, "book") if hasattr(status, "book")
@ -236,6 +243,16 @@ class BooksStream(ActivityStream):
return [] return []
return audience.filter(shelfbook__book__parent_work=work).distinct() return audience.filter(shelfbook__book__parent_work=work).distinct()
def get_audience(self, status):
# only show public statuses on the books feed,
# and only statuses that mention books
if status.privacy != "public" or not (
status.mention_books.exists() or hasattr(status, "book")
):
return []
return super().get_audience(status)
def get_statuses_for_user(self, user): def get_statuses_for_user(self, user):
"""any public status that mentions the user's books""" """any public status that mentions the user's books"""
books = user.shelfbook_set.values_list( books = user.shelfbook_set.values_list(
@ -480,7 +497,7 @@ def remove_statuses_on_unshelve(sender, instance, *args, **kwargs):
# ---- TASKS # ---- TASKS
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def add_book_statuses_task(user_id, book_id): def add_book_statuses_task(user_id, book_id):
"""add statuses related to a book on shelve""" """add statuses related to a book on shelve"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
@ -488,7 +505,7 @@ def add_book_statuses_task(user_id, book_id):
BooksStream().add_book_statuses(user, book) BooksStream().add_book_statuses(user, book)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def remove_book_statuses_task(user_id, book_id): def remove_book_statuses_task(user_id, book_id):
"""remove statuses about a book from a user's books feed""" """remove statuses about a book from a user's books feed"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
@ -496,7 +513,7 @@ def remove_book_statuses_task(user_id, book_id):
BooksStream().remove_book_statuses(user, book) BooksStream().remove_book_statuses(user, book)
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def populate_stream_task(stream, user_id): def populate_stream_task(stream, user_id):
"""background task for populating an empty activitystream""" """background task for populating an empty activitystream"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
@ -504,7 +521,7 @@ def populate_stream_task(stream, user_id):
stream.populate_streams(user) stream.populate_streams(user)
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def remove_status_task(status_ids): def remove_status_task(status_ids):
"""remove a status from any stream it might be in""" """remove a status from any stream it might be in"""
# this can take an id or a list of ids # this can take an id or a list of ids
@ -514,10 +531,12 @@ def remove_status_task(status_ids):
for stream in streams.values(): for stream in streams.values():
for status in statuses: for status in statuses:
stream.remove_object_from_related_stores(status) stream.remove_object_from_stores(
status, stream.get_stores_for_users(stream.get_audience(status))
)
@app.task(queue=HIGH, ignore_result=True) @app.task(queue=HIGH)
def add_status_task(status_id, increment_unread=False): def add_status_task(status_id, increment_unread=False):
"""add a status to any stream it should be in""" """add a status to any stream it should be in"""
status = models.Status.objects.select_subclasses().get(id=status_id) status = models.Status.objects.select_subclasses().get(id=status_id)
@ -529,7 +548,7 @@ def add_status_task(status_id, increment_unread=False):
stream.add_status(status, increment_unread=increment_unread) stream.add_status(status, increment_unread=increment_unread)
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def remove_user_statuses_task(viewer_id, user_id, stream_list=None): def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
"""remove all statuses by a user from a viewer's stream""" """remove all statuses by a user from a viewer's stream"""
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values() stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
@ -539,7 +558,7 @@ def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
stream.remove_user_statuses(viewer, user) stream.remove_user_statuses(viewer, user)
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def add_user_statuses_task(viewer_id, user_id, stream_list=None): def add_user_statuses_task(viewer_id, user_id, stream_list=None):
"""add all statuses by a user to a viewer's stream""" """add all statuses by a user to a viewer's stream"""
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values() stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
@ -549,7 +568,7 @@ def add_user_statuses_task(viewer_id, user_id, stream_list=None):
stream.add_user_statuses(viewer, user) stream.add_user_statuses(viewer, user)
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def handle_boost_task(boost_id): def handle_boost_task(boost_id):
"""remove the original post and other, earlier boosts""" """remove the original post and other, earlier boosts"""
instance = models.Status.objects.get(id=boost_id) instance = models.Status.objects.get(id=boost_id)
@ -563,10 +582,10 @@ def handle_boost_task(boost_id):
for stream in streams.values(): for stream in streams.values():
# people who should see the boost (not people who see the original status) # people who should see the boost (not people who see the original status)
audience = stream.get_stores_for_object(instance) audience = stream.get_stores_for_users(stream.get_audience(instance))
stream.remove_object_from_related_stores(boosted, stores=audience) stream.remove_object_from_stores(boosted, audience)
for status in old_versions: for status in old_versions:
stream.remove_object_from_related_stores(status, stores=audience) stream.remove_object_from_stores(status, audience)
def get_status_type(status): def get_status_type(status):

View file

@ -40,6 +40,7 @@ class BookwyrmConfig(AppConfig):
from bookwyrm.telemetry import open_telemetry from bookwyrm.telemetry import open_telemetry
open_telemetry.instrumentDjango() open_telemetry.instrumentDjango()
open_telemetry.instrumentPostgres()
if settings.ENABLE_PREVIEW_IMAGES and settings.FONTS: if settings.ENABLE_PREVIEW_IMAGES and settings.FONTS:
# Download any fonts that we don't have yet # Download any fonts that we don't have yet

View file

@ -4,13 +4,16 @@ from urllib.parse import quote_plus
import imghdr import imghdr
import logging import logging
import re import re
import asyncio
import requests
from requests.exceptions import RequestException
import aiohttp
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction from django.db import transaction
import requests
from requests.exceptions import RequestException
from bookwyrm import activitypub, models, settings from bookwyrm import activitypub, models, settings
from bookwyrm.settings import USER_AGENT
from .connector_manager import load_more_data, ConnectorException, raise_not_valid_url from .connector_manager import load_more_data, ConnectorException, raise_not_valid_url
from .format_mappings import format_mappings from .format_mappings import format_mappings
@ -52,11 +55,44 @@ class AbstractMinimalConnector(ABC):
return f"{self.search_url}{quote_plus(query)}" return f"{self.search_url}{quote_plus(query)}"
def process_search_response(self, query, data, min_confidence): def process_search_response(self, query, data, min_confidence):
"""Format the search results based on the formt of the query""" """Format the search results based on the format of the query"""
if maybe_isbn(query): if maybe_isbn(query):
return list(self.parse_isbn_search_data(data))[:10] return list(self.parse_isbn_search_data(data))[:10]
return list(self.parse_search_data(data, min_confidence))[:10] return list(self.parse_search_data(data, min_confidence))[:10]
async def get_results(self, session, url, min_confidence, query):
"""try this specific connector"""
# pylint: disable=line-too-long
headers = {
"Accept": (
'application/json, application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'
),
"User-Agent": USER_AGENT,
}
params = {"min_confidence": min_confidence}
try:
async with session.get(url, headers=headers, params=params) as response:
if not response.ok:
logger.info("Unable to connect to %s: %s", url, response.reason)
return
try:
raw_data = await response.json()
except aiohttp.client_exceptions.ContentTypeError as err:
logger.exception(err)
return
return {
"connector": self,
"results": self.process_search_response(
query, raw_data, min_confidence
),
}
except asyncio.TimeoutError:
logger.info("Connection timed out for url: %s", url)
except aiohttp.ClientError as err:
logger.info(err)
@abstractmethod @abstractmethod
def get_or_create_book(self, remote_id): def get_or_create_book(self, remote_id):
"""pull up a book record by whatever means possible""" """pull up a book record by whatever means possible"""
@ -321,7 +357,7 @@ def infer_physical_format(format_text):
def unique_physical_format(format_text): def unique_physical_format(format_text):
"""only store the format if it isn't diretly in the format mappings""" """only store the format if it isn't directly in the format mappings"""
format_text = format_text.lower() format_text = format_text.lower()
if format_text in format_mappings: if format_text in format_mappings:
# try a direct match, so saving this would be redundant # try a direct match, so saving this would be redundant

View file

@ -12,7 +12,7 @@ from django.db.models import signals
from requests import HTTPError from requests import HTTPError
from bookwyrm import book_search, models from bookwyrm import book_search, models
from bookwyrm.settings import SEARCH_TIMEOUT, USER_AGENT from bookwyrm.settings import SEARCH_TIMEOUT
from bookwyrm.tasks import app, LOW from bookwyrm.tasks import app, LOW
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -22,40 +22,6 @@ class ConnectorException(HTTPError):
"""when the connector can't do what was asked""" """when the connector can't do what was asked"""
async def get_results(session, url, min_confidence, query, connector):
"""try this specific connector"""
# pylint: disable=line-too-long
headers = {
"Accept": (
'application/json, application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'
),
"User-Agent": USER_AGENT,
}
params = {"min_confidence": min_confidence}
try:
async with session.get(url, headers=headers, params=params) as response:
if not response.ok:
logger.info("Unable to connect to %s: %s", url, response.reason)
return
try:
raw_data = await response.json()
except aiohttp.client_exceptions.ContentTypeError as err:
logger.exception(err)
return
return {
"connector": connector,
"results": connector.process_search_response(
query, raw_data, min_confidence
),
}
except asyncio.TimeoutError:
logger.info("Connection timed out for url: %s", url)
except aiohttp.ClientError as err:
logger.info(err)
async def async_connector_search(query, items, min_confidence): async def async_connector_search(query, items, min_confidence):
"""Try a number of requests simultaneously""" """Try a number of requests simultaneously"""
timeout = aiohttp.ClientTimeout(total=SEARCH_TIMEOUT) timeout = aiohttp.ClientTimeout(total=SEARCH_TIMEOUT)
@ -64,7 +30,7 @@ async def async_connector_search(query, items, min_confidence):
for url, connector in items: for url, connector in items:
tasks.append( tasks.append(
asyncio.ensure_future( asyncio.ensure_future(
get_results(session, url, min_confidence, query, connector) connector.get_results(session, url, min_confidence, query)
) )
) )
@ -73,7 +39,7 @@ async def async_connector_search(query, items, min_confidence):
def search(query, min_confidence=0.1, return_first=False): def search(query, min_confidence=0.1, return_first=False):
"""find books based on arbitary keywords""" """find books based on arbitrary keywords"""
if not query: if not query:
return [] return []
results = [] results = []
@ -143,7 +109,7 @@ def get_or_create_connector(remote_id):
return load_connector(connector_info) return load_connector(connector_info)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def load_more_data(connector_id, book_id): def load_more_data(connector_id, book_id):
"""background the work of getting all 10,000 editions of LoTR""" """background the work of getting all 10,000 editions of LoTR"""
connector_info = models.Connector.objects.get(id=connector_id) connector_info = models.Connector.objects.get(id=connector_id)
@ -152,7 +118,7 @@ def load_more_data(connector_id, book_id):
connector.expand_book_data(book) connector.expand_book_data(book)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def create_edition_task(connector_id, work_id, data): def create_edition_task(connector_id, work_id, data):
"""separate task for each of the 10,000 editions of LoTR""" """separate task for each of the 10,000 editions of LoTR"""
connector_info = models.Connector.objects.get(id=connector_id) connector_info = models.Connector.objects.get(id=connector_id)

View file

@ -97,7 +97,7 @@ class Connector(AbstractConnector):
) )
def parse_isbn_search_data(self, data): def parse_isbn_search_data(self, data):
"""got some daaaata""" """got some data"""
results = data.get("entities") results = data.get("entities")
if not results: if not results:
return return

View file

@ -75,7 +75,7 @@ def format_email(email_name, data):
return (subject, html_content, text_content) return (subject, html_content, text_content)
@app.task(queue=HIGH, ignore_result=True) @app.task(queue=HIGH)
def send_email(recipient, subject, html_content, text_content): def send_email(recipient, subject, html_content, text_content):
"""use a task to send the email""" """use a task to send the email"""
email = EmailMultiAlternatives( email = EmailMultiAlternatives(

View file

@ -15,7 +15,7 @@ from .custom_form import CustomForm, StyledForm
# pylint: disable=missing-class-docstring # pylint: disable=missing-class-docstring
class ExpiryWidget(widgets.Select): class ExpiryWidget(widgets.Select):
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
"""human-readable exiration time buckets""" """human-readable expiration time buckets"""
selected_string = super().value_from_datadict(data, files, name) selected_string = super().value_from_datadict(data, files, name)
if selected_string == "day": if selected_string == "day":

View file

@ -19,7 +19,7 @@ class LibrarythingImporter(Importer):
normalized = {k: remove_brackets(entry.get(v)) for k, v in mappings.items()} normalized = {k: remove_brackets(entry.get(v)) for k, v in mappings.items()}
isbn_13 = normalized.get("isbn_13") isbn_13 = normalized.get("isbn_13")
isbn_13 = isbn_13.split(", ") if isbn_13 else [] isbn_13 = isbn_13.split(", ") if isbn_13 else []
normalized["isbn_13"] = isbn_13[1] if len(isbn_13) > 0 else None normalized["isbn_13"] = isbn_13[1] if len(isbn_13) > 1 else None
return normalized return normalized
def get_shelf(self, normalized_row): def get_shelf(self, normalized_row):

View file

@ -24,8 +24,7 @@ class ListsStream(RedisStore):
def add_list(self, book_list): def add_list(self, book_list):
"""add a list to users' feeds""" """add a list to users' feeds"""
# the pipeline contains all the add-to-stream activities self.add_object_to_stores(book_list, self.get_stores_for_object(book_list))
self.add_object_to_related_stores(book_list)
def add_user_lists(self, viewer, user): def add_user_lists(self, viewer, user):
"""add a user's lists to another user's feed""" """add a user's lists to another user's feed"""
@ -86,18 +85,19 @@ class ListsStream(RedisStore):
if group: if group:
audience = audience.filter( audience = audience.filter(
Q(id=book_list.user.id) # if the user is the list's owner Q(id=book_list.user.id) # if the user is the list's owner
| Q(following=book_list.user) # if the user is following the pwmer | Q(following=book_list.user) # if the user is following the owner
# if a user is in the group # if a user is in the group
| Q(memberships__group__id=book_list.group.id) | Q(memberships__group__id=book_list.group.id)
) )
else: else:
audience = audience.filter( audience = audience.filter(
Q(id=book_list.user.id) # if the user is the list's owner Q(id=book_list.user.id) # if the user is the list's owner
| Q(following=book_list.user) # if the user is following the pwmer | Q(following=book_list.user) # if the user is following the owner
) )
return audience.distinct() return audience.distinct()
def get_stores_for_object(self, obj): def get_stores_for_object(self, obj):
"""the stores that an object belongs in"""
return [self.stream_id(u) for u in self.get_audience(obj)] return [self.stream_id(u) for u in self.get_audience(obj)]
def get_lists_for_user(self, user): # pylint: disable=no-self-use def get_lists_for_user(self, user): # pylint: disable=no-self-use
@ -217,14 +217,14 @@ def add_list_on_account_create_command(user_id):
# ---- TASKS # ---- TASKS
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def populate_lists_task(user_id): def populate_lists_task(user_id):
"""background task for populating an empty list stream""" """background task for populating an empty list stream"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
ListsStream().populate_lists(user) ListsStream().populate_lists(user)
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def remove_list_task(list_id, re_add=False): def remove_list_task(list_id, re_add=False):
"""remove a list from any stream it might be in""" """remove a list from any stream it might be in"""
stores = models.User.objects.filter(local=True, is_active=True).values_list( stores = models.User.objects.filter(local=True, is_active=True).values_list(
@ -233,20 +233,20 @@ def remove_list_task(list_id, re_add=False):
# delete for every store # delete for every store
stores = [ListsStream().stream_id(idx) for idx in stores] stores = [ListsStream().stream_id(idx) for idx in stores]
ListsStream().remove_object_from_related_stores(list_id, stores=stores) ListsStream().remove_object_from_stores(list_id, stores)
if re_add: if re_add:
add_list_task.delay(list_id) add_list_task.delay(list_id)
@app.task(queue=HIGH, ignore_result=True) @app.task(queue=HIGH)
def add_list_task(list_id): def add_list_task(list_id):
"""add a list to any stream it should be in""" """add a list to any stream it should be in"""
book_list = models.List.objects.get(id=list_id) book_list = models.List.objects.get(id=list_id)
ListsStream().add_list(book_list) ListsStream().add_list(book_list)
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def remove_user_lists_task(viewer_id, user_id, exclude_privacy=None): def remove_user_lists_task(viewer_id, user_id, exclude_privacy=None):
"""remove all lists by a user from a viewer's stream""" """remove all lists by a user from a viewer's stream"""
viewer = models.User.objects.get(id=viewer_id) viewer = models.User.objects.get(id=viewer_id)
@ -254,7 +254,7 @@ def remove_user_lists_task(viewer_id, user_id, exclude_privacy=None):
ListsStream().remove_user_lists(viewer, user, exclude_privacy=exclude_privacy) ListsStream().remove_user_lists(viewer, user, exclude_privacy=exclude_privacy)
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def add_user_lists_task(viewer_id, user_id): def add_user_lists_task(viewer_id, user_id):
"""add all lists by a user to a viewer's stream""" """add all lists by a user to a viewer's stream"""
viewer = models.User.objects.get(id=viewer_id) viewer = models.User.objects.get(id=viewer_id)

View file

@ -3,38 +3,7 @@ merge book data objects """
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import Count from django.db.models import Count
from bookwyrm import models from bookwyrm import models
from bookwyrm.management.merge import merge_objects
def update_related(canonical, obj):
"""update all the models with fk to the object being removed"""
# move related models to canonical
related_models = [
(r.remote_field.name, r.related_model) for r in canonical._meta.related_objects
]
for (related_field, related_model) in related_models:
related_objs = related_model.objects.filter(**{related_field: obj})
for related_obj in related_objs:
print("replacing in", related_model.__name__, related_field, related_obj.id)
try:
setattr(related_obj, related_field, canonical)
related_obj.save()
except TypeError:
getattr(related_obj, related_field).add(canonical)
getattr(related_obj, related_field).remove(obj)
def copy_data(canonical, obj):
"""try to get the most data possible"""
for data_field in obj._meta.get_fields():
if not hasattr(data_field, "activitypub_field"):
continue
data_value = getattr(obj, data_field.name)
if not data_value:
continue
if not getattr(canonical, data_field.name):
print("setting data field", data_field.name, data_value)
setattr(canonical, data_field.name, data_value)
canonical.save()
def dedupe_model(model): def dedupe_model(model):
@ -61,19 +30,16 @@ def dedupe_model(model):
print("keeping", canonical.remote_id) print("keeping", canonical.remote_id)
for obj in objs[1:]: for obj in objs[1:]:
print(obj.remote_id) print(obj.remote_id)
copy_data(canonical, obj) merge_objects(canonical, obj)
update_related(canonical, obj)
# remove the outdated entry
obj.delete()
class Command(BaseCommand): class Command(BaseCommand):
"""dedplucate allllll the book data models""" """deduplicate allllll the book data models"""
help = "merges duplicate book data" help = "merges duplicate book data"
# pylint: disable=no-self-use,unused-argument # pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options): def handle(self, *args, **options):
"""run deudplications""" """run deduplications"""
dedupe_model(models.Edition) dedupe_model(models.Edition)
dedupe_model(models.Work) dedupe_model(models.Work)
dedupe_model(models.Author) dedupe_model(models.Author)

View file

@ -0,0 +1,12 @@
""" PROCEED WITH CAUTION: uses deduplication fields to permanently
merge author data objects """
from bookwyrm import models
from bookwyrm.management.merge_command import MergeCommand
class Command(MergeCommand):
"""merges two authors by ID"""
help = "merges specified authors into one"
MODEL = models.Author

View file

@ -0,0 +1,12 @@
""" PROCEED WITH CAUTION: uses deduplication fields to permanently
merge edition data objects """
from bookwyrm import models
from bookwyrm.management.merge_command import MergeCommand
class Command(MergeCommand):
"""merges two editions by ID"""
help = "merges specified editions into one"
MODEL = models.Edition

View file

@ -33,10 +33,10 @@ def remove_editions():
class Command(BaseCommand): class Command(BaseCommand):
"""dedplucate allllll the book data models""" """deduplicate allllll the book data models"""
help = "merges duplicate book data" help = "merges duplicate book data"
# pylint: disable=no-self-use,unused-argument # pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options): def handle(self, *args, **options):
"""run deudplications""" """run deduplications"""
remove_editions() remove_editions()

View file

@ -9,7 +9,7 @@ class Command(BaseCommand):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def handle(self, *args, **options): def handle(self, *args, **options):
"""reveoke nonessential low priority tasks""" """revoke nonessential low priority tasks"""
types = [ types = [
"bookwyrm.preview_images.generate_edition_preview_image_task", "bookwyrm.preview_images.generate_edition_preview_image_task",
"bookwyrm.preview_images.generate_user_preview_image_task", "bookwyrm.preview_images.generate_user_preview_image_task",

View file

@ -0,0 +1,50 @@
from django.db.models import ManyToManyField
def update_related(canonical, obj):
"""update all the models with fk to the object being removed"""
# move related models to canonical
related_models = [
(r.remote_field.name, r.related_model) for r in canonical._meta.related_objects
]
for (related_field, related_model) in related_models:
# Skip the ManyToMany fields that arent auto-created. These
# should have a corresponding OneToMany field in the model for
# the linking table anyway. If we update it through that model
# instead then we wont lose the extra fields in the linking
# table.
related_field_obj = related_model._meta.get_field(related_field)
if isinstance(related_field_obj, ManyToManyField):
through = related_field_obj.remote_field.through
if not through._meta.auto_created:
continue
related_objs = related_model.objects.filter(**{related_field: obj})
for related_obj in related_objs:
print("replacing in", related_model.__name__, related_field, related_obj.id)
try:
setattr(related_obj, related_field, canonical)
related_obj.save()
except TypeError:
getattr(related_obj, related_field).add(canonical)
getattr(related_obj, related_field).remove(obj)
def copy_data(canonical, obj):
"""try to get the most data possible"""
for data_field in obj._meta.get_fields():
if not hasattr(data_field, "activitypub_field"):
continue
data_value = getattr(obj, data_field.name)
if not data_value:
continue
if not getattr(canonical, data_field.name):
print("setting data field", data_field.name, data_value)
setattr(canonical, data_field.name, data_value)
canonical.save()
def merge_objects(canonical, obj):
copy_data(canonical, obj)
update_related(canonical, obj)
# remove the outdated entry
obj.delete()

View file

@ -0,0 +1,29 @@
from bookwyrm.management.merge import merge_objects
from django.core.management.base import BaseCommand
class MergeCommand(BaseCommand):
"""base class for merge commands"""
def add_arguments(self, parser):
"""add the arguments for this command"""
parser.add_argument("--canonical", type=int, required=True)
parser.add_argument("--other", type=int, required=True)
# pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options):
"""merge the two objects"""
model = self.MODEL
try:
canonical = model.objects.get(id=options["canonical"])
except model.DoesNotExist:
print("canonical book doesnt exist!")
return
try:
other = model.objects.get(id=options["other"])
except model.DoesNotExist:
print("other book doesnt exist!")
return
merge_objects(canonical, other)

View file

@ -1467,7 +1467,7 @@ class Migration(migrations.Migration):
( (
"expiry", "expiry",
models.DateTimeField( models.DateTimeField(
default=bookwyrm.models.site.get_passowrd_reset_expiry default=bookwyrm.models.site.get_password_reset_expiry
), ),
), ),
( (

View file

@ -6,7 +6,7 @@ from bookwyrm.connectors.abstract_connector import infer_physical_format
def infer_format(app_registry, schema_editor): def infer_format(app_registry, schema_editor):
"""set the new phsyical format field based on existing format data""" """set the new physical format field based on existing format data"""
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
editions = ( editions = (

View file

@ -5,7 +5,7 @@ from bookwyrm.settings import DOMAIN
def remove_self_connector(app_registry, schema_editor): def remove_self_connector(app_registry, schema_editor):
"""set the new phsyical format field based on existing format data""" """set the new physical format field based on existing format data"""
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
app_registry.get_model("bookwyrm", "Connector").objects.using(db_alias).filter( app_registry.get_model("bookwyrm", "Connector").objects.using(db_alias).filter(
connector_file="self_connector" connector_file="self_connector"

View file

@ -25,7 +25,7 @@ from bookwyrm.tasks import app, MEDIUM, BROADCAST
from bookwyrm.models.fields import ImageField, ManyToManyField from bookwyrm.models.fields import ImageField, ManyToManyField
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# I tried to separate these classes into mutliple files but I kept getting # I tried to separate these classes into multiple files but I kept getting
# circular import errors so I gave up. I'm sure it could be done though! # circular import errors so I gave up. I'm sure it could be done though!
PropertyField = namedtuple("PropertyField", ("set_activity_from_field")) PropertyField = namedtuple("PropertyField", ("set_activity_from_field"))
@ -91,7 +91,7 @@ class ActivitypubMixin:
@classmethod @classmethod
def find_existing(cls, data): def find_existing(cls, data):
"""compare data to fields that can be used for deduplation. """compare data to fields that can be used for deduplication.
This always includes remote_id, but can also be unique identifiers This always includes remote_id, but can also be unique identifiers
like an isbn for an edition""" like an isbn for an edition"""
filters = [] filters = []
@ -234,8 +234,8 @@ class ObjectMixin(ActivitypubMixin):
activity = self.to_create_activity(user) activity = self.to_create_activity(user)
self.broadcast(activity, user, software=software, queue=priority) self.broadcast(activity, user, software=software, queue=priority)
except AttributeError: except AttributeError:
# janky as heck, this catches the mutliple inheritence chain # janky as heck, this catches the multiple inheritance chain
# for boosts and ignores this auxilliary broadcast # for boosts and ignores this auxiliary broadcast
return return
return return
@ -311,7 +311,7 @@ class OrderedCollectionPageMixin(ObjectMixin):
@property @property
def collection_remote_id(self): def collection_remote_id(self):
"""this can be overriden if there's a special remote id, ie outbox""" """this can be overridden if there's a special remote id, ie outbox"""
return self.remote_id return self.remote_id
def to_ordered_collection( def to_ordered_collection(
@ -339,7 +339,7 @@ class OrderedCollectionPageMixin(ObjectMixin):
activity["id"] = remote_id activity["id"] = remote_id
paginated = Paginator(queryset, PAGE_LENGTH) paginated = Paginator(queryset, PAGE_LENGTH)
# add computed fields specific to orderd collections # add computed fields specific to ordered collections
activity["totalItems"] = paginated.count activity["totalItems"] = paginated.count
activity["first"] = f"{remote_id}?page=1" activity["first"] = f"{remote_id}?page=1"
activity["last"] = f"{remote_id}?page={paginated.num_pages}" activity["last"] = f"{remote_id}?page={paginated.num_pages}"
@ -405,7 +405,7 @@ class CollectionItemMixin(ActivitypubMixin):
# first off, we want to save normally no matter what # first off, we want to save normally no matter what
super().save(*args, **kwargs) super().save(*args, **kwargs)
# list items can be updateda, normally you would only broadcast on created # list items can be updated, normally you would only broadcast on created
if not broadcast or not self.user.local: if not broadcast or not self.user.local:
return return
@ -506,7 +506,7 @@ def unfurl_related_field(related_field, sort_field=None):
return related_field.remote_id return related_field.remote_id
@app.task(queue=BROADCAST, ignore_result=True) @app.task(queue=BROADCAST)
def broadcast_task(sender_id: int, activity: str, recipients: List[str]): def broadcast_task(sender_id: int, activity: str, recipients: List[str]):
"""the celery task for broadcast""" """the celery task for broadcast"""
user_model = apps.get_model("bookwyrm.User", require_ready=True) user_model = apps.get_model("bookwyrm.User", require_ready=True)
@ -565,7 +565,7 @@ async def sign_and_send(
def to_ordered_collection_page( def to_ordered_collection_page(
queryset, remote_id, id_only=False, page=1, pure=False, **kwargs queryset, remote_id, id_only=False, page=1, pure=False, **kwargs
): ):
"""serialize and pagiante a queryset""" """serialize and paginate a queryset"""
paginated = Paginator(queryset, PAGE_LENGTH) paginated = Paginator(queryset, PAGE_LENGTH)
activity_page = paginated.get_page(page) activity_page = paginated.get_page(page)

View file

@ -24,7 +24,7 @@ class AnnualGoal(BookWyrmModel):
) )
class Meta: class Meta:
"""unqiueness constraint""" """uniqueness constraint"""
unique_together = ("user", "year") unique_together = ("user", "year")

View file

@ -65,7 +65,7 @@ class AutoMod(AdminModel):
created_by = models.ForeignKey("User", on_delete=models.PROTECT) created_by = models.ForeignKey("User", on_delete=models.PROTECT)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def automod_task(): def automod_task():
"""Create reports""" """Create reports"""
if not AutoMod.objects.exists(): if not AutoMod.objects.exists():

View file

@ -321,7 +321,7 @@ class Edition(Book):
def get_rank(self): def get_rank(self):
"""calculate how complete the data is on this edition""" """calculate how complete the data is on this edition"""
rank = 0 rank = 0
# big ups for havinga cover # big ups for having a cover
rank += int(bool(self.cover)) * 3 rank += int(bool(self.cover)) * 3
# is it in the instance's preferred language? # is it in the instance's preferred language?
rank += int(bool(DEFAULT_LANGUAGE in self.languages)) rank += int(bool(DEFAULT_LANGUAGE in self.languages))

View file

@ -20,8 +20,9 @@ class Favorite(ActivityMixin, BookWyrmModel):
activity_serializer = activitypub.Like activity_serializer = activitypub.Like
# pylint: disable=unused-argument
@classmethod @classmethod
def ignore_activity(cls, activity): def ignore_activity(cls, activity, allow_external_connections=True):
"""don't bother with incoming favs of unknown statuses""" """don't bother with incoming favs of unknown statuses"""
return not Status.objects.filter(remote_id=activity.object).exists() return not Status.objects.filter(remote_id=activity.object).exists()

View file

@ -71,11 +71,11 @@ class ActivitypubFieldMixin:
def set_field_from_activity( def set_field_from_activity(
self, instance, data, overwrite=True, allow_external_connections=True self, instance, data, overwrite=True, allow_external_connections=True
): ):
"""helper function for assinging a value to the field. Returns if changed""" """helper function for assigning a value to the field. Returns if changed"""
try: try:
value = getattr(data, self.get_activitypub_field()) value = getattr(data, self.get_activitypub_field())
except AttributeError: except AttributeError:
# masssively hack-y workaround for boosts # massively hack-y workaround for boosts
if self.get_activitypub_field() != "attributedTo": if self.get_activitypub_field() != "attributedTo":
raise raise
value = getattr(data, "actor") value = getattr(data, "actor")
@ -221,7 +221,7 @@ PrivacyLevels = [
class PrivacyField(ActivitypubFieldMixin, models.CharField): class PrivacyField(ActivitypubFieldMixin, models.CharField):
"""this maps to two differente activitypub fields""" """this maps to two different activitypub fields"""
public = "https://www.w3.org/ns/activitystreams#Public" public = "https://www.w3.org/ns/activitystreams#Public"
@ -431,7 +431,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
def set_field_from_activity( def set_field_from_activity(
self, instance, data, save=True, overwrite=True, allow_external_connections=True self, instance, data, save=True, overwrite=True, allow_external_connections=True
): ):
"""helper function for assinging a value to the field""" """helper function for assigning a value to the field"""
value = getattr(data, self.get_activitypub_field()) value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity( formatted = self.field_from_activity(
value, allow_external_connections=allow_external_connections value, allow_external_connections=allow_external_connections

View file

@ -252,9 +252,12 @@ class ImportItem(models.Model):
@property @property
def rating(self): def rating(self):
"""x/5 star rating for a book""" """x/5 star rating for a book"""
if self.normalized_data.get("rating"): if not self.normalized_data.get("rating"):
return None
try:
return float(self.normalized_data.get("rating")) return float(self.normalized_data.get("rating"))
return None except ValueError:
return None
@property @property
def date_added(self): def date_added(self):
@ -327,7 +330,7 @@ class ImportItem(models.Model):
) )
@app.task(queue=IMPORTS, ignore_result=True) @app.task(queue=IMPORTS)
def start_import_task(job_id): def start_import_task(job_id):
"""trigger the child tasks for each row""" """trigger the child tasks for each row"""
job = ImportJob.objects.get(id=job_id) job = ImportJob.objects.get(id=job_id)
@ -346,7 +349,7 @@ def start_import_task(job_id):
job.save() job.save()
@app.task(queue=IMPORTS, ignore_result=True) @app.task(queue=IMPORTS)
def import_item_task(item_id): def import_item_task(item_id):
"""resolve a row into a book""" """resolve a row into a book"""
item = ImportItem.objects.get(id=item_id) item = ImportItem.objects.get(id=item_id)

View file

@ -31,7 +31,7 @@ class Link(ActivitypubMixin, BookWyrmModel):
@property @property
def name(self): def name(self):
"""link name via the assocaited domain""" """link name via the associated domain"""
return self.domain.name return self.domain.name
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View file

@ -284,7 +284,7 @@ def notify_user_on_list_item_add(sender, instance, created, *args, **kwargs):
return return
list_owner = instance.book_list.user list_owner = instance.book_list.user
# create a notification if somoene ELSE added to a local user's list # create a notification if someone ELSE added to a local user's list
if list_owner.local and list_owner != instance.user: if list_owner.local and list_owner != instance.user:
# keep the related_user singular, group the items # keep the related_user singular, group the items
Notification.notify_list_item(list_owner, instance) Notification.notify_list_item(list_owner, instance)

View file

@ -8,7 +8,7 @@ from .base_model import BookWyrmModel
class ProgressMode(models.TextChoices): class ProgressMode(models.TextChoices):
"""types of prgress available""" """types of progress available"""
PAGE = "PG", "page" PAGE = "PG", "page"
PERCENT = "PCT", "percent" PERCENT = "PCT", "percent"

View file

@ -34,7 +34,7 @@ class UserRelationship(BookWyrmModel):
@property @property
def recipients(self): def recipients(self):
"""the remote user needs to recieve direct broadcasts""" """the remote user needs to receive direct broadcasts"""
return [u for u in [self.user_subject, self.user_object] if not u.local] return [u for u in [self.user_subject, self.user_object] if not u.local]
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View file

@ -80,7 +80,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
raise PermissionDenied() raise PermissionDenied()
class Meta: class Meta:
"""user/shelf unqiueness""" """user/shelf uniqueness"""
unique_together = ("user", "identifier") unique_together = ("user", "identifier")

View file

@ -209,7 +209,7 @@ class InviteRequest(BookWyrmModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_passowrd_reset_expiry(): def get_password_reset_expiry():
"""give people a limited time to use the link""" """give people a limited time to use the link"""
now = timezone.now() now = timezone.now()
return now + datetime.timedelta(days=1) return now + datetime.timedelta(days=1)
@ -219,7 +219,7 @@ class PasswordReset(models.Model):
"""gives someone access to create an account on the instance""" """gives someone access to create an account on the instance"""
code = models.CharField(max_length=32, default=new_access_code) code = models.CharField(max_length=32, default=new_access_code)
expiry = models.DateTimeField(default=get_passowrd_reset_expiry) expiry = models.DateTimeField(default=get_password_reset_expiry)
user = models.OneToOneField(User, on_delete=models.CASCADE) user = models.OneToOneField(User, on_delete=models.CASCADE)
def valid(self): def valid(self):

View file

@ -116,10 +116,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
return list(set(mentions)) return list(set(mentions))
@classmethod @classmethod
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements def ignore_activity(
cls, activity, allow_external_connections=True
): # pylint: disable=too-many-return-statements
"""keep notes if they are replies to existing statuses""" """keep notes if they are replies to existing statuses"""
if activity.type == "Announce": if activity.type == "Announce":
boosted = activitypub.resolve_remote_id(activity.object, get_activity=True) boosted = activitypub.resolve_remote_id(
activity.object,
get_activity=True,
allow_external_connections=allow_external_connections,
)
if not boosted: if not boosted:
# if we can't load the status, definitely ignore it # if we can't load the status, definitely ignore it
return True return True

View file

@ -469,7 +469,7 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def set_remote_server(user_id): def set_remote_server(user_id):
"""figure out the user's remote server in the background""" """figure out the user's remote server in the background"""
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
@ -513,7 +513,7 @@ def get_or_create_remote_server(domain, refresh=False):
return server return server
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def get_remote_reviews(outbox): def get_remote_reviews(outbox):
"""ingest reviews by a new remote bookwyrm user""" """ingest reviews by a new remote bookwyrm user"""
outbox_page = outbox + "?page=true&type=Review" outbox_page = outbox + "?page=true&type=Review"

View file

@ -420,7 +420,7 @@ def save_and_cleanup(image, instance=None):
# pylint: disable=invalid-name # pylint: disable=invalid-name
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def generate_site_preview_image_task(): def generate_site_preview_image_task():
"""generate preview_image for the website""" """generate preview_image for the website"""
if not settings.ENABLE_PREVIEW_IMAGES: if not settings.ENABLE_PREVIEW_IMAGES:
@ -445,7 +445,7 @@ def generate_site_preview_image_task():
# pylint: disable=invalid-name # pylint: disable=invalid-name
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def generate_edition_preview_image_task(book_id): def generate_edition_preview_image_task(book_id):
"""generate preview_image for a book""" """generate preview_image for a book"""
if not settings.ENABLE_PREVIEW_IMAGES: if not settings.ENABLE_PREVIEW_IMAGES:
@ -470,7 +470,7 @@ def generate_edition_preview_image_task(book_id):
save_and_cleanup(image, instance=book) save_and_cleanup(image, instance=book)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def generate_user_preview_image_task(user_id): def generate_user_preview_image_task(user_id):
"""generate preview_image for a user""" """generate preview_image for a user"""
if not settings.ENABLE_PREVIEW_IMAGES: if not settings.ENABLE_PREVIEW_IMAGES:
@ -496,7 +496,7 @@ def generate_user_preview_image_task(user_id):
save_and_cleanup(image, instance=user) save_and_cleanup(image, instance=user)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def remove_user_preview_image_task(user_id): def remove_user_preview_image_task(user_id):
"""remove preview_image for a user""" """remove preview_image for a user"""
if not settings.ENABLE_PREVIEW_IMAGES: if not settings.ENABLE_PREVIEW_IMAGES:

View file

@ -16,12 +16,12 @@ class RedisStore(ABC):
"""the object and rank""" """the object and rank"""
return {obj.id: self.get_rank(obj)} return {obj.id: self.get_rank(obj)}
def add_object_to_related_stores(self, obj, execute=True): def add_object_to_stores(self, obj, stores, execute=True):
"""add an object to all suitable stores""" """add an object to a given set of stores"""
value = self.get_value(obj) value = self.get_value(obj)
# we want to do this as a bulk operation, hence "pipeline" # we want to do this as a bulk operation, hence "pipeline"
pipeline = r.pipeline() pipeline = r.pipeline()
for store in self.get_stores_for_object(obj): for store in stores:
# add the status to the feed # add the status to the feed
pipeline.zadd(store, value) pipeline.zadd(store, value)
# trim the store # trim the store
@ -32,14 +32,14 @@ class RedisStore(ABC):
# and go! # and go!
return pipeline.execute() return pipeline.execute()
def remove_object_from_related_stores(self, obj, stores=None): # pylint: disable=no-self-use
def remove_object_from_stores(self, obj, stores):
"""remove an object from all stores""" """remove an object from all stores"""
# if the stoers are provided, the object can just be an id # if the stores are provided, the object can just be an id
if stores and isinstance(obj, int): if stores and isinstance(obj, int):
obj_id = obj obj_id = obj
else: else:
obj_id = obj.id obj_id = obj.id
stores = self.get_stores_for_object(obj) if stores is None else stores
pipeline = r.pipeline() pipeline = r.pipeline()
for store in stores: for store in stores:
pipeline.zrem(store, -1, obj_id) pipeline.zrem(store, -1, obj_id)
@ -82,10 +82,6 @@ class RedisStore(ABC):
def get_objects_for_store(self, store): def get_objects_for_store(self, store):
"""a queryset of what should go in a store, used for populating it""" """a queryset of what should go in a store, used for populating it"""
@abstractmethod
def get_stores_for_object(self, obj):
"""the stores that an object belongs in"""
@abstractmethod @abstractmethod
def get_rank(self, obj): def get_rank(self, obj):
"""how to rank an object""" """how to rank an object"""

View file

@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
env = Env() env = Env()
env.read_env() env.read_env()
DOMAIN = env("DOMAIN") DOMAIN = env("DOMAIN")
VERSION = "0.6.1" VERSION = "0.6.2"
RELEASE_API = env( RELEASE_API = env(
"RELEASE_API", "RELEASE_API",
@ -22,7 +22,7 @@ RELEASE_API = env(
PAGE_LENGTH = env.int("PAGE_LENGTH", 15) PAGE_LENGTH = env.int("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "a7d4e720" JS_CACHE = "ea91d7df"
# email # email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
@ -226,7 +226,7 @@ STREAMS = [
# total time in seconds that the instance will spend searching connectors # total time in seconds that the instance will spend searching connectors
SEARCH_TIMEOUT = env.int("SEARCH_TIMEOUT", 8) SEARCH_TIMEOUT = env.int("SEARCH_TIMEOUT", 8)
# timeout for a query to an individual connector # timeout for a query to an individual connector
QUERY_TIMEOUT = env.int("QUERY_TIMEOUT", 5) QUERY_TIMEOUT = env.int("INTERACTIVE_QUERY_TIMEOUT", env.int("QUERY_TIMEOUT", 5))
# Redis cache backend # Redis cache backend
if env.bool("USE_DUMMY_CACHE", False): if env.bool("USE_DUMMY_CACHE", False):

View file

@ -5,7 +5,7 @@
* - .book-cover is positioned and sized based on its container. * - .book-cover is positioned and sized based on its container.
* *
* To have the cover within specific dimensions, specify a width or height for * To have the cover within specific dimensions, specify a width or height for
* standard bulmas named breapoints: * standard bulmas named breakpoints:
* *
* `is-(w|h)-(auto|xs|s|m|l|xl|xxl)[-(mobile|tablet|desktop)]` * `is-(w|h)-(auto|xs|s|m|l|xl|xxl)[-(mobile|tablet|desktop)]`
* *
@ -43,7 +43,7 @@
max-height: 100%; max-height: 100%;
/* Useful when stretching under-sized images. */ /* Useful when stretching under-sized images. */
image-rendering: optimizequality; image-rendering: optimizeQuality;
image-rendering: smooth; image-rendering: smooth;
} }

View file

@ -5,7 +5,7 @@ let BookWyrm = new (class {
constructor() { constructor() {
this.MAX_FILE_SIZE_BYTES = 10 * 1000000; this.MAX_FILE_SIZE_BYTES = 10 * 1000000;
this.initOnDOMLoaded(); this.initOnDOMLoaded();
this.initReccuringTasks(); this.initRecurringTasks();
this.initEventListeners(); this.initEventListeners();
} }
@ -77,7 +77,7 @@ let BookWyrm = new (class {
/** /**
* Execute recurring tasks. * Execute recurring tasks.
*/ */
initReccuringTasks() { initRecurringTasks() {
// Polling // Polling
document.querySelectorAll("[data-poll]").forEach((liveArea) => this.polling(liveArea)); document.querySelectorAll("[data-poll]").forEach((liveArea) => this.polling(liveArea));
} }

View file

@ -2,7 +2,7 @@
"use strict"; "use strict";
/** /**
* Remoev input field * Remove input field
* *
* @param {event} the button click event * @param {event} the button click event
*/ */

View file

@ -4,13 +4,16 @@ import logging
from django.dispatch import receiver from django.dispatch import receiver
from django.db import transaction from django.db import transaction
from django.db.models import signals, Count, Q, Case, When, IntegerField from django.db.models import signals, Count, Q, Case, When, IntegerField
from opentelemetry import trace
from bookwyrm import models from bookwyrm import models
from bookwyrm.redis_store import RedisStore, r from bookwyrm.redis_store import RedisStore, r
from bookwyrm.tasks import app, LOW, MEDIUM from bookwyrm.tasks import app, LOW, MEDIUM
from bookwyrm.telemetry import open_telemetry
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
tracer = open_telemetry.tracer()
class SuggestedUsers(RedisStore): class SuggestedUsers(RedisStore):
@ -49,30 +52,34 @@ class SuggestedUsers(RedisStore):
) )
def get_stores_for_object(self, obj): def get_stores_for_object(self, obj):
"""the stores that an object belongs in"""
return [self.store_id(u) for u in self.get_users_for_object(obj)] return [self.store_id(u) for u in self.get_users_for_object(obj)]
def get_users_for_object(self, obj): # pylint: disable=no-self-use def get_users_for_object(self, obj): # pylint: disable=no-self-use
"""given a user, who might want to follow them""" """given a user, who might want to follow them"""
return models.User.objects.filter(local=True,).exclude( return models.User.objects.filter(local=True, is_active=True).exclude(
Q(id=obj.id) | Q(followers=obj) | Q(id__in=obj.blocks.all()) | Q(blocks=obj) Q(id=obj.id) | Q(followers=obj) | Q(id__in=obj.blocks.all()) | Q(blocks=obj)
) )
@tracer.start_as_current_span("SuggestedUsers.rerank_obj")
def rerank_obj(self, obj, update_only=True): def rerank_obj(self, obj, update_only=True):
"""update all the instances of this user with new ranks""" """update all the instances of this user with new ranks"""
trace.get_current_span().set_attribute("update_only", update_only)
pipeline = r.pipeline() pipeline = r.pipeline()
for store_user in self.get_users_for_object(obj): for store_user in self.get_users_for_object(obj):
annotated_user = get_annotated_users( with tracer.start_as_current_span("SuggestedUsers.rerank_obj/user") as _:
store_user, annotated_user = get_annotated_users(
id=obj.id, store_user,
).first() id=obj.id,
if not annotated_user: ).first()
continue if not annotated_user:
continue
pipeline.zadd( pipeline.zadd(
self.store_id(store_user), self.store_id(store_user),
self.get_value(annotated_user), self.get_value(annotated_user),
xx=update_only, xx=update_only,
) )
pipeline.execute() pipeline.execute()
def rerank_user_suggestions(self, user): def rerank_user_suggestions(self, user):
@ -237,41 +244,45 @@ def domain_level_update(sender, instance, created, update_fields=None, **kwargs)
# ------------------- TASKS # ------------------- TASKS
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def rerank_suggestions_task(user_id): def rerank_suggestions_task(user_id):
"""do the hard work in celery""" """do the hard work in celery"""
suggested_users.rerank_user_suggestions(user_id) suggested_users.rerank_user_suggestions(user_id)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def rerank_user_task(user_id, update_only=False): def rerank_user_task(user_id, update_only=False):
"""do the hard work in celery""" """do the hard work in celery"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
suggested_users.rerank_obj(user, update_only=update_only) suggested_users.rerank_obj(user, update_only=update_only)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def remove_user_task(user_id): def remove_user_task(user_id):
"""do the hard work in celery""" """do the hard work in celery"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
suggested_users.remove_object_from_related_stores(user) suggested_users.remove_object_from_stores(
user, suggested_users.get_stores_for_object(user)
)
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def remove_suggestion_task(user_id, suggested_user_id): def remove_suggestion_task(user_id, suggested_user_id):
"""remove a specific user from a specific user's suggestions""" """remove a specific user from a specific user's suggestions"""
suggested_user = models.User.objects.get(id=suggested_user_id) suggested_user = models.User.objects.get(id=suggested_user_id)
suggested_users.remove_suggestion(user_id, suggested_user) suggested_users.remove_suggestion(user_id, suggested_user)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def bulk_remove_instance_task(instance_id): def bulk_remove_instance_task(instance_id):
"""remove a bunch of users from recs""" """remove a bunch of users from recs"""
for user in models.User.objects.filter(federated_server__id=instance_id): for user in models.User.objects.filter(federated_server__id=instance_id):
suggested_users.remove_object_from_related_stores(user) suggested_users.remove_object_from_stores(
user, suggested_users.get_stores_for_object(user)
)
@app.task(queue=LOW, ignore_result=True) @app.task(queue=LOW)
def bulk_add_instance_task(instance_id): def bulk_add_instance_task(instance_id):
"""remove a bunch of users from recs""" """remove a bunch of users from recs"""
for user in models.User.objects.filter(federated_server__id=instance_id): for user in models.User.objects.filter(federated_server__id=instance_id):

View file

@ -22,6 +22,12 @@ def instrumentDjango():
DjangoInstrumentor().instrument() DjangoInstrumentor().instrument()
def instrumentPostgres():
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
Psycopg2Instrumentor().instrument()
def instrumentCelery(): def instrumentCelery():
from opentelemetry.instrumentation.celery import CeleryInstrumentor from opentelemetry.instrumentation.celery import CeleryInstrumentor
from celery.signals import worker_process_init from celery.signals import worker_process_init

View file

@ -4,6 +4,7 @@
{% load humanize %} {% load humanize %}
{% load utilities %} {% load utilities %}
{% load static %} {% load static %}
{% load shelf_tags %}
{% block title %}{{ book|book_title }}{% endblock %} {% block title %}{{ book|book_title }}{% endblock %}
@ -46,7 +47,13 @@
<meta itemprop="isPartOf" content="{{ book.series | escape }}"> <meta itemprop="isPartOf" content="{{ book.series | escape }}">
<meta itemprop="volumeNumber" content="{{ book.series_number }}"> <meta itemprop="volumeNumber" content="{{ book.series_number }}">
(<a href="{% url 'book-series-by' book.authors.first.id %}?series_name={{ book.series }}">{{ book.series }}{% if book.series_number %} #{{ book.series_number }}{% endif %}</a>) {% if book.authors.exists %}
<a href="{% url 'book-series-by' book.authors.first.id %}?series_name={{ book.series }}">
{% endif %}
{{ book.series }}{% if book.series_number %} #{{ book.series_number }}{% endif %}
{% if book.authors.exists %}
</a>
{% endif %}
{% endif %} {% endif %}
</p> </p>
{% endif %} {% endif %}
@ -239,7 +246,7 @@
<ul> <ul>
{% for shelf in user_shelfbooks %} {% for shelf in user_shelfbooks %}
<li class="box"> <li class="box">
<a href="{{ shelf.shelf.local_path }}">{{ shelf.shelf.name }}</a> <a href="{{ shelf.shelf.local_path }}">{{ shelf.shelf|translate_shelf_name }}</a>
<div class="is-pulled-right"> <div class="is-pulled-right">
{% include 'snippets/shelf_selector.html' with shelf=shelf.shelf class="is-small" readthrough=readthrough %} {% include 'snippets/shelf_selector.html' with shelf=shelf.shelf class="is-small" readthrough=readthrough %}
</div> </div>

View file

@ -30,7 +30,7 @@
<fieldset name="books" class="columns is-mobile"> <fieldset name="books" class="columns is-mobile">
{% if book_results %} {% if book_results %}
<div class="column is-narrow"> <div class="column is-narrow">
<p class="help mb-0">Search results</p> <p class="help mb-0">{% trans "Search results" %}</p>
<div class="columns is-mobile"> <div class="columns is-mobile">
{% for book in book_results %} {% for book in book_results %}

View file

@ -5,7 +5,7 @@
<div class="block"> <div class="block">
<h2 class="title is-4">{% trans "Who to follow" %}</h2> <h2 class="title is-4">{% trans "Who to follow" %}</h2>
<p class="subtitle is-6">You can follow users on other BookWyrm instances and federated services like Mastodon.</p> <p class="subtitle is-6">{% trans "You can follow users on other BookWyrm instances and federated services like Mastodon." %}</p>
<form class="field has-addons" method="get" action="{% url 'get-started-users' %}"> <form class="field has-addons" method="get" action="{% url 'get-started-users' %}">
<div class="control"> <div class="control">
<input type="text" name="query" value="{{ request.GET.query }}" class="input" placeholder="{% trans 'Search for a user' %}" aria-label="{% trans 'Search for a user' %}"> <input type="text" name="query" value="{{ request.GET.query }}" class="input" placeholder="{% trans 'Search for a user' %}" aria-label="{% trans 'Search for a user' %}">

View file

@ -3,7 +3,7 @@
{% if list.curation == 'group' %} {% if list.curation == 'group' %}
{% blocktrans with username=list.user.display_name userpath=list.user.local_path groupname=list.group.name grouppath=list.group.local_path %}Created by <a href="{{ userpath }}">{{ username }}</a> and managed by <a href="{{ grouppath }}">{{ groupname }}</a>{% endblocktrans %} {% blocktrans with username=list.user.display_name userpath=list.user.local_path groupname=list.group.name grouppath=list.group.local_path %}Created by <a href="{{ userpath }}">{{ username }}</a> and managed by <a href="{{ grouppath }}">{{ groupname }}</a>{% endblocktrans %}
{% elif list.curation != 'open' %} {% elif list.curation == 'curated' %}
{% blocktrans with username=list.user.display_name path=list.user.local_path %}Created and curated by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %} {% blocktrans with username=list.user.display_name path=list.user.local_path %}Created and curated by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %}
{% else %} {% else %}
{% blocktrans with username=list.user.display_name path=list.user.local_path %}Created by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %} {% blocktrans with username=list.user.display_name path=list.user.local_path %}Created by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %}

View file

@ -133,7 +133,7 @@
</div> </div>
{% url 'user-shelves' request.user.localname as path %} {% url 'user-shelves' request.user.localname as path %}
<p class="notification is-light"> <p class="notification is-light">
{% blocktrans %}Looking for shelf privacy? You can set a sepearate visibility level for each of your shelves. Go to <a href="{{ path }}">Your Books</a>, pick a shelf from the tab bar, and click "Edit shelf."{% endblocktrans %} {% blocktrans %}Looking for shelf privacy? You can set a separate visibility level for each of your shelves. Go to <a href="{{ path }}">Your Books</a>, pick a shelf from the tab bar, and click "Edit shelf."{% endblocktrans %}
</p> </p>
</div> </div>
</section> </section>

View file

@ -29,7 +29,7 @@
</div> </div>
<div class="control"> <div class="control">
<button type="submit" class="button is-primary"> <button type="submit" class="button is-primary">
<span>Search</span> <span>{% trans "Search" %}</span>
<span class="icon icon-search" aria-hidden="true"></span> <span class="icon icon-search" aria-hidden="true"></span>
</button> </button>
</div> </div>

View file

@ -116,6 +116,35 @@
</div> </div>
{% endif %} {% endif %}
<section class="block">
<div class="content"><h2>{% trans "Clear Queues" %}</h2></div>
<div class="content notification is-warning is-flex is-align-items-start">
<span class="icon icon-warning is-size-4 pr-3" aria-hidden="true"></span>
{% trans "Clearing queues can cause serious problems including data loss! Only play with this if you really know what you're doing. You must shut down the Celery worker before you do this." %}
</div>
<form action="{% url 'settings-celery' %}" method="post">
{% csrf_token %}
<div class="columns">
<div class="column is-3">
<div class="content"><h3>{{ form.queues.label_tag }}</h3></div>
{{ form.queues }}
</div>
<div class="column is-9">
<div class="content"><h3>{{ form.tasks.label_tag }}</h3></div>
{{ form.tasks }}
</div>
</div>
<div class="buttons is-right">
<button type="submit" class="button is-danger">{% trans "Clear Queues" %}</button>
</div>
</form>
</section>
{% if errors %} {% if errors %}
<div class="block content"> <div class="block content">
<h2>{% trans "Errors" %}</h2> <h2>{% trans "Errors" %}</h2>

View file

@ -16,7 +16,7 @@
<p class="title is-5">{{ users|intcomma }}</p> <p class="title is-5">{{ users|intcomma }}</p>
</div> </div>
</div> </div>
<div class="column is-3-desktop is-6-mobil is-flexe"> <div class="column is-3-desktop is-6-mobile is-flex">
<div class="notification is-flex-grow-1"> <div class="notification is-flex-grow-1">
<h3>{% trans "Active this month" %}</h3> <h3>{% trans "Active this month" %}</h3>
<p class="title is-5">{{ active_users|intcomma }}</p> <p class="title is-5">{{ active_users|intcomma }}</p>

View file

@ -18,7 +18,7 @@ def get_book_description(book):
if book.description: if book.description:
return book.description return book.description
if book.parent_work: if book.parent_work:
# this shoud always be true # this should always be true
return book.parent_work.description return book.parent_work.description
return None return None

View file

@ -9,6 +9,15 @@ from bookwyrm.utils import cache
register = template.Library() register = template.Library()
SHELF_NAMES = {
"all": _("All books"),
"to-read": _("To Read"),
"reading": _("Currently Reading"),
"read": _("Read"),
"stopped-reading": _("Stopped Reading"),
}
@register.filter(name="is_book_on_shelf") @register.filter(name="is_book_on_shelf")
def get_is_book_on_shelf(book, shelf): def get_is_book_on_shelf(book, shelf):
"""is a book on a shelf""" """is a book on a shelf"""
@ -37,20 +46,16 @@ def get_next_shelf(current_shelf):
@register.filter(name="translate_shelf_name") @register.filter(name="translate_shelf_name")
def get_translated_shelf_name(shelf): def get_translated_shelf_name(shelf):
"""produced translated shelf nidentifierame""" """produce translated shelf identifiername"""
if not shelf: if not shelf:
return "" return ""
# support obj or dict # support obj or dict
identifier = shelf["identifier"] if isinstance(shelf, dict) else shelf.identifier identifier = shelf["identifier"] if isinstance(shelf, dict) else shelf.identifier
if identifier == "all":
return _("All books") try:
if identifier == "to-read": return SHELF_NAMES[identifier]
return _("To Read") except KeyError:
if identifier == "reading": return shelf["name"] if isinstance(shelf, dict) else shelf.name
return _("Currently Reading")
if identifier == "read":
return _("Read")
return shelf["name"] if isinstance(shelf, dict) else shelf.name
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)

View file

@ -19,7 +19,7 @@ def get_uuid(identifier):
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def join(*args): def join(*args):
"""concatenate an arbitary set of values""" """concatenate an arbitrary set of values"""
return "_".join(str(a) for a in args) return "_".join(str(a) for a in args)

View file

@ -19,7 +19,7 @@ class Author(TestCase):
) )
def test_serialize_model(self): def test_serialize_model(self):
"""check presense of author fields""" """check presence of author fields"""
activity = self.author.to_activity() activity = self.author.to_activity()
self.assertEqual(activity["id"], self.author.remote_id) self.assertEqual(activity["id"], self.author.remote_id)
self.assertIsInstance(activity["aliases"], list) self.assertIsInstance(activity["aliases"], list)

View file

@ -59,7 +59,7 @@ class BaseActivity(TestCase):
self.assertIsInstance(representative, models.User) self.assertIsInstance(representative, models.User)
def test_init(self, *_): def test_init(self, *_):
"""simple successfuly init""" """simple successfully init"""
instance = ActivityObject(id="a", type="b") instance = ActivityObject(id="a", type="b")
self.assertTrue(hasattr(instance, "id")) self.assertTrue(hasattr(instance, "id"))
self.assertTrue(hasattr(instance, "type")) self.assertTrue(hasattr(instance, "type"))

View file

@ -1,4 +1,4 @@
""" quotation activty object serializer class """ """ quotation activity object serializer class """
import json import json
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
@ -30,7 +30,7 @@ class Quotation(TestCase):
self.status_data = json.loads(datafile.read_bytes()) self.status_data = json.loads(datafile.read_bytes())
def test_quotation_activity(self): def test_quotation_activity(self):
"""create a Quoteation ap object from json""" """create a Quotation ap object from json"""
quotation = activitypub.Quotation(**self.status_data) quotation = activitypub.Quotation(**self.status_data)
self.assertEqual(quotation.type, "Quotation") self.assertEqual(quotation.type, "Quotation")

View file

@ -50,7 +50,7 @@ class Activitystreams(TestCase):
self.assertEqual(args[1], self.book) self.assertEqual(args[1], self.book)
def test_remove_book_statuses_task(self): def test_remove_book_statuses_task(self):
"""remove stauses related to a book""" """remove statuses related to a book"""
with patch("bookwyrm.activitystreams.BooksStream.remove_book_statuses") as mock: with patch("bookwyrm.activitystreams.BooksStream.remove_book_statuses") as mock:
activitystreams.remove_book_statuses_task(self.local_user.id, self.book.id) activitystreams.remove_book_statuses_task(self.local_user.id, self.book.id)
self.assertTrue(mock.called) self.assertTrue(mock.called)
@ -75,7 +75,7 @@ class Activitystreams(TestCase):
def test_remove_status_task(self): def test_remove_status_task(self):
"""remove a status from all streams""" """remove a status from all streams"""
with patch( with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" "bookwyrm.activitystreams.ActivityStream.remove_object_from_stores"
) as mock: ) as mock:
activitystreams.remove_status_task(self.status.id) activitystreams.remove_status_task(self.status.id)
self.assertEqual(mock.call_count, 3) self.assertEqual(mock.call_count, 3)
@ -132,8 +132,8 @@ class Activitystreams(TestCase):
self.assertEqual(args[0], self.local_user) self.assertEqual(args[0], self.local_user)
self.assertEqual(args[1], self.another_user) self.assertEqual(args[1], self.another_user)
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_stores")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_boost_to_another_timeline(self, *_): def test_boost_to_another_timeline(self, *_):
"""boost from a non-follower doesn't remove original status from feed""" """boost from a non-follower doesn't remove original status from feed"""
@ -144,7 +144,7 @@ class Activitystreams(TestCase):
user=self.another_user, user=self.another_user,
) )
with patch( with patch(
"bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores" "bookwyrm.activitystreams.HomeStream.remove_object_from_stores"
) as mock: ) as mock:
activitystreams.handle_boost_task(boost.id) activitystreams.handle_boost_task(boost.id)
@ -152,10 +152,10 @@ class Activitystreams(TestCase):
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)
call_args = mock.call_args call_args = mock.call_args
self.assertEqual(call_args[0][0], status) self.assertEqual(call_args[0][0], status)
self.assertEqual(call_args[1]["stores"], [f"{self.another_user.id}-home"]) self.assertEqual(call_args[0][1], [f"{self.another_user.id}-home"])
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_stores")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_boost_to_another_timeline_remote(self, *_): def test_boost_to_another_timeline_remote(self, *_):
"""boost from a remote non-follower doesn't remove original status from feed""" """boost from a remote non-follower doesn't remove original status from feed"""
@ -166,7 +166,7 @@ class Activitystreams(TestCase):
user=self.remote_user, user=self.remote_user,
) )
with patch( with patch(
"bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores" "bookwyrm.activitystreams.HomeStream.remove_object_from_stores"
) as mock: ) as mock:
activitystreams.handle_boost_task(boost.id) activitystreams.handle_boost_task(boost.id)
@ -174,10 +174,10 @@ class Activitystreams(TestCase):
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)
call_args = mock.call_args call_args = mock.call_args
self.assertEqual(call_args[0][0], status) self.assertEqual(call_args[0][0], status)
self.assertEqual(call_args[1]["stores"], []) self.assertEqual(call_args[0][1], [])
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_stores")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_boost_to_following_timeline(self, *_): def test_boost_to_following_timeline(self, *_):
"""add a boost and deduplicate the boosted status on the timeline""" """add a boost and deduplicate the boosted status on the timeline"""
@ -189,17 +189,17 @@ class Activitystreams(TestCase):
user=self.another_user, user=self.another_user,
) )
with patch( with patch(
"bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores" "bookwyrm.activitystreams.HomeStream.remove_object_from_stores"
) as mock: ) as mock:
activitystreams.handle_boost_task(boost.id) activitystreams.handle_boost_task(boost.id)
self.assertTrue(mock.called) self.assertTrue(mock.called)
call_args = mock.call_args call_args = mock.call_args
self.assertEqual(call_args[0][0], status) self.assertEqual(call_args[0][0], status)
self.assertTrue(f"{self.another_user.id}-home" in call_args[1]["stores"]) self.assertTrue(f"{self.another_user.id}-home" in call_args[0][1])
self.assertTrue(f"{self.local_user.id}-home" in call_args[1]["stores"]) self.assertTrue(f"{self.local_user.id}-home" in call_args[0][1])
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_stores")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_boost_to_same_timeline(self, *_): def test_boost_to_same_timeline(self, *_):
"""add a boost and deduplicate the boosted status on the timeline""" """add a boost and deduplicate the boosted status on the timeline"""
@ -210,10 +210,10 @@ class Activitystreams(TestCase):
user=self.local_user, user=self.local_user,
) )
with patch( with patch(
"bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores" "bookwyrm.activitystreams.HomeStream.remove_object_from_stores"
) as mock: ) as mock:
activitystreams.handle_boost_task(boost.id) activitystreams.handle_boost_task(boost.id)
self.assertTrue(mock.called) self.assertTrue(mock.called)
call_args = mock.call_args call_args = mock.call_args
self.assertEqual(call_args[0][0], status) self.assertEqual(call_args[0][0], status)
self.assertEqual(call_args[1]["stores"], [f"{self.local_user.id}-home"]) self.assertEqual(call_args[0][1], [f"{self.local_user.id}-home"])

View file

@ -46,7 +46,7 @@ class Openlibrary(TestCase):
data = {"key": "/work/OL1234W"} data = {"key": "/work/OL1234W"}
result = self.connector.get_remote_id_from_data(data) result = self.connector.get_remote_id_from_data(data)
self.assertEqual(result, "https://openlibrary.org/work/OL1234W") self.assertEqual(result, "https://openlibrary.org/work/OL1234W")
# error handlding # error handling
with self.assertRaises(ConnectorException): with self.assertRaises(ConnectorException):
self.connector.get_remote_id_from_data({}) self.connector.get_remote_id_from_data({})

View file

@ -59,7 +59,7 @@ class Activitystreams(TestCase):
def test_remove_list_task(self, *_): def test_remove_list_task(self, *_):
"""remove a list from all streams""" """remove a list from all streams"""
with patch( with patch(
"bookwyrm.lists_stream.ListsStream.remove_object_from_related_stores" "bookwyrm.lists_stream.ListsStream.remove_object_from_stores"
) as mock: ) as mock:
lists_stream.remove_list_task(self.list.id) lists_stream.remove_list_task(self.list.id)
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)

View file

@ -245,7 +245,7 @@ class ActivitypubMixins(TestCase):
# ObjectMixin # ObjectMixin
def test_object_save_create(self, *_): def test_object_save_create(self, *_):
"""should save uneventufully when broadcast is disabled""" """should save uneventfully when broadcast is disabled"""
class Success(Exception): class Success(Exception):
"""this means we got to the right method""" """this means we got to the right method"""
@ -276,7 +276,7 @@ class ActivitypubMixins(TestCase):
ObjectModel(user=None).save() ObjectModel(user=None).save()
def test_object_save_update(self, *_): def test_object_save_update(self, *_):
"""should save uneventufully when broadcast is disabled""" """should save uneventfully when broadcast is disabled"""
class Success(Exception): class Success(Exception):
"""this means we got to the right method""" """this means we got to the right method"""

View file

@ -51,7 +51,7 @@ class BaseModel(TestCase):
def test_set_remote_id(self): def test_set_remote_id(self):
"""this function sets remote ids after creation""" """this function sets remote ids after creation"""
# using Work because it BookWrymModel is abstract and this requires save # using Work because it BookWyrmModel is abstract and this requires save
# Work is a relatively not-fancy model. # Work is a relatively not-fancy model.
instance = models.Work.objects.create(title="work title") instance = models.Work.objects.create(title="work title")
instance.remote_id = None instance.remote_id = None

View file

@ -29,7 +29,7 @@ from bookwyrm.settings import DOMAIN
@patch("bookwyrm.activitystreams.populate_stream_task.delay") @patch("bookwyrm.activitystreams.populate_stream_task.delay")
@patch("bookwyrm.lists_stream.populate_lists_task.delay") @patch("bookwyrm.lists_stream.populate_lists_task.delay")
class ModelFields(TestCase): class ModelFields(TestCase):
"""overwrites standard model feilds to work with activitypub""" """overwrites standard model fields to work with activitypub"""
def test_validate_remote_id(self, *_): def test_validate_remote_id(self, *_):
"""should look like a url""" """should look like a url"""
@ -125,7 +125,7 @@ class ModelFields(TestCase):
instance.run_validators("@example.com") instance.run_validators("@example.com")
instance.run_validators("mouse@examplecom") instance.run_validators("mouse@examplecom")
instance.run_validators("one two@fish.aaaa") instance.run_validators("one two@fish.aaaa")
instance.run_validators("a*&@exampke.com") instance.run_validators("a*&@example.com")
instance.run_validators("trailingwhite@example.com ") instance.run_validators("trailingwhite@example.com ")
self.assertIsNone(instance.run_validators("mouse@example.com")) self.assertIsNone(instance.run_validators("mouse@example.com"))
self.assertIsNone(instance.run_validators("mo-2use@ex3ample.com")) self.assertIsNone(instance.run_validators("mo-2use@ex3ample.com"))
@ -292,7 +292,7 @@ class ModelFields(TestCase):
self.assertEqual(value.name, "MOUSE?? MOUSE!!") self.assertEqual(value.name, "MOUSE?? MOUSE!!")
def test_foreign_key_from_activity_dict(self, *_): def test_foreign_key_from_activity_dict(self, *_):
"""test recieving activity json""" """test receiving activity json"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes()) userdata = json.loads(datafile.read_bytes())

View file

@ -397,7 +397,7 @@ class Status(TestCase):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def test_create_broadcast(self, one, two, broadcast_mock, *_): def test_create_broadcast(self, one, two, broadcast_mock, *_):
"""should send out two verions of a status on create""" """should send out two versions of a status on create"""
models.Comment.objects.create( models.Comment.objects.create(
content="hi", user=self.local_user, book=self.book content="hi", user=self.local_user, book=self.book
) )

View file

@ -7,7 +7,7 @@ class MarkdownTags(TestCase):
"""lotta different things here""" """lotta different things here"""
def test_get_markdown(self): def test_get_markdown(self):
"""mardown format data""" """markdown format data"""
result = markdown.get_markdown("_hi_") result = markdown.get_markdown("_hi_")
self.assertEqual(result, "<p><em>hi</em></p>") self.assertEqual(result, "<p><em>hi</em></p>")

View file

@ -41,7 +41,7 @@ class RatingTags(TestCase):
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_get_rating(self, *_): def test_get_rating(self, *_):
"""privacy filtered rating. Commented versions are how it ought to work with """privacy filtered rating. Commented versions are how it ought to work with
subjective ratings, which are currenly not used for performance reasons.""" subjective ratings, which are currently not used for performance reasons."""
# follows-only: not included # follows-only: not included
models.ReviewRating.objects.create( models.ReviewRating.objects.create(
user=self.remote_user, user=self.remote_user,

View file

@ -30,7 +30,7 @@ class PostgresTriggers(TestCase):
title="The Long Goodbye", title="The Long Goodbye",
subtitle="wow cool", subtitle="wow cool",
series="series name", series="series name",
languages=["irrelevent"], languages=["irrelevant"],
) )
book.authors.add(author) book.authors.add(author)
book.refresh_from_db() book.refresh_from_db()
@ -40,7 +40,7 @@ class PostgresTriggers(TestCase):
"'cool':5B 'goodby':3A 'long':2A 'name':9 'rays':7C 'seri':8 'the':6C 'wow':4B", "'cool':5B 'goodby':3A 'long':2A 'name':9 'rays':7C 'seri':8 'the':6C 'wow':4B",
) )
def test_seach_vector_on_author_update(self, _): def test_search_vector_on_author_update(self, _):
"""update search when an author name changes""" """update search when an author name changes"""
author = models.Author.objects.create(name="The Rays") author = models.Author.objects.create(name="The Rays")
book = models.Edition.objects.create( book = models.Edition.objects.create(
@ -53,7 +53,7 @@ class PostgresTriggers(TestCase):
self.assertEqual(book.search_vector, "'goodby':3A 'jeremy':4C 'long':2A") self.assertEqual(book.search_vector, "'goodby':3A 'jeremy':4C 'long':2A")
def test_seach_vector_on_author_delete(self, _): def test_search_vector_on_author_delete(self, _):
"""update search when an author name changes""" """update search when an author name changes"""
author = models.Author.objects.create(name="Jeremy") author = models.Author.objects.create(name="Jeremy")
book = models.Edition.objects.create( book = models.Edition.objects.create(

View file

@ -107,7 +107,7 @@ class Signature(TestCase):
@responses.activate @responses.activate
def test_remote_signer(self): def test_remote_signer(self):
"""signtures for remote users""" """signatures for remote users"""
datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
data = json.loads(datafile.read_bytes()) data = json.loads(datafile.read_bytes())
data["id"] = self.fake_remote.remote_id data["id"] = self.fake_remote.remote_id

View file

@ -58,7 +58,7 @@ class InboxActivities(TestCase):
with patch("bookwyrm.activitystreams.remove_status_task.delay") as redis_mock: with patch("bookwyrm.activitystreams.remove_status_task.delay") as redis_mock:
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called) self.assertTrue(redis_mock.called)
# deletion doens't remove the status, it turns it into a tombstone # deletion doesn't remove the status, it turns it into a tombstone
status = models.Status.objects.get() status = models.Status.objects.get()
self.assertTrue(status.deleted) self.assertTrue(status.deleted)
self.assertIsInstance(status.deleted_date, datetime) self.assertIsInstance(status.deleted_date, datetime)
@ -87,7 +87,7 @@ class InboxActivities(TestCase):
with patch("bookwyrm.activitystreams.remove_status_task.delay") as redis_mock: with patch("bookwyrm.activitystreams.remove_status_task.delay") as redis_mock:
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called) self.assertTrue(redis_mock.called)
# deletion doens't remove the status, it turns it into a tombstone # deletion doesn't remove the status, it turns it into a tombstone
status = models.Status.objects.get() status = models.Status.objects.get()
self.assertTrue(status.deleted) self.assertTrue(status.deleted)
self.assertIsInstance(status.deleted_date, datetime) self.assertIsInstance(status.deleted_date, datetime)

View file

@ -114,7 +114,7 @@ class LoginViews(TestCase):
view = views.Login.as_view() view = views.Login.as_view()
form = forms.LoginForm() form = forms.LoginForm()
form.data["localname"] = "mouse" form.data["localname"] = "mouse"
form.data["password"] = "passsword1" form.data["password"] = "password1"
request = self.factory.post("", form.data) request = self.factory.post("", form.data)
request.user = self.anonymous_user request.user = self.anonymous_user

View file

@ -72,7 +72,7 @@ class PasswordViews(TestCase):
validate_html(result.render()) validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_password_reset_nonexistant_code(self): def test_password_reset_nonexistent_code(self):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
view = views.PasswordReset.as_view() view = views.PasswordReset.as_view()
request = self.factory.get("") request = self.factory.get("")

View file

@ -234,7 +234,7 @@ class StatusViews(TestCase):
) )
def test_create_status_reply_with_mentions(self, *_): def test_create_status_reply_with_mentions(self, *_):
"""reply to a post with an @mention'ed user""" """reply to a post with an @mention'd user"""
view = views.CreateStatus.as_view() view = views.CreateStatus.as_view()
user = models.User.objects.create_user( user = models.User.objects.create_user(
"rat", "rat@rat.com", "password", local=True, localname="rat" "rat", "rat@rat.com", "password", local=True, localname="rat"
@ -356,12 +356,12 @@ class StatusViews(TestCase):
self.assertEqual(len(hashtags), 2) self.assertEqual(len(hashtags), 2)
self.assertEqual(list(status.mention_hashtags.all()), list(hashtags)) self.assertEqual(list(status.mention_hashtags.all()), list(hashtags))
hashtag_exising = models.Hashtag.objects.filter(name="#existing").first() hashtag_existing = models.Hashtag.objects.filter(name="#existing").first()
hashtag_new = models.Hashtag.objects.filter(name="#NewTag").first() hashtag_new = models.Hashtag.objects.filter(name="#NewTag").first()
self.assertEqual( self.assertEqual(
status.content, status.content,
"<p>this is an " "<p>this is an "
+ f'<a href="{hashtag_exising.remote_id}" data-mention="hashtag">' + f'<a href="{hashtag_existing.remote_id}" data-mention="hashtag">'
+ "#EXISTING</a> hashtag but all uppercase, this one is " + "#EXISTING</a> hashtag but all uppercase, this one is "
+ f'<a href="{hashtag_new.remote_id}" data-mention="hashtag">' + f'<a href="{hashtag_new.remote_id}" data-mention="hashtag">'
+ "#NewTag</a>.</p>", + "#NewTag</a>.</p>",

View file

@ -53,7 +53,7 @@ class WellknownViews(TestCase):
data = json.loads(result.getvalue()) data = json.loads(result.getvalue())
self.assertEqual(data["subject"], "acct:mouse@local.com") self.assertEqual(data["subject"], "acct:mouse@local.com")
def test_webfinger_case_sensitivty(self): def test_webfinger_case_sensitivity(self):
"""ensure that webfinger queries are not case sensitive""" """ensure that webfinger queries are not case sensitive"""
request = self.factory.get("", {"resource": "acct:MoUsE@local.com"}) request = self.factory.get("", {"resource": "acct:MoUsE@local.com"})
request.user = self.anonymous_user request.user = self.anonymous_user

View file

@ -1,10 +1,13 @@
""" celery status """ """ celery status """
import json
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.http import HttpResponse from django.http import HttpResponse
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
from django import forms
import redis import redis
from celerywyrm import settings from celerywyrm import settings
@ -46,21 +49,68 @@ class CeleryStatus(View):
queues = None queues = None
errors.append(err) errors.append(err)
form = ClearCeleryForm()
data = { data = {
"stats": stats, "stats": stats,
"active_tasks": active_tasks, "active_tasks": active_tasks,
"queues": queues, "queues": queues,
"form": form,
"errors": errors, "errors": errors,
} }
return TemplateResponse(request, "settings/celery.html", data) return TemplateResponse(request, "settings/celery.html", data)
def post(self, request):
"""Submit form to clear queues"""
form = ClearCeleryForm(request.POST)
if form.is_valid():
if len(celery.control.ping()) != 0:
return HttpResponse(
"Refusing to delete tasks while Celery worker is active"
)
pipeline = r.pipeline()
for queue in form.cleaned_data["queues"]:
for task in r.lrange(queue, 0, -1):
task_json = json.loads(task)
if task_json["headers"]["task"] in form.cleaned_data["tasks"]:
pipeline.lrem(queue, 0, task)
results = pipeline.execute()
return HttpResponse(f"Deleted {sum(results)} tasks")
class ClearCeleryForm(forms.Form):
"""Form to clear queues"""
queues = forms.MultipleChoiceField(
label="Queues",
choices=[
(LOW, "Low prioirty"),
(MEDIUM, "Medium priority"),
(HIGH, "High priority"),
(IMPORTS, "Imports"),
(BROADCAST, "Broadcasts"),
],
widget=forms.CheckboxSelectMultiple,
)
tasks = forms.MultipleChoiceField(
label="Tasks", choices=[], widget=forms.CheckboxSelectMultiple
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
celery.loader.import_default_modules()
self.fields["tasks"].choices = sorted(
[(k, k) for k in celery.tasks.keys() if not k.startswith("celery.")]
)
@require_GET @require_GET
# pylint: disable=unused-argument # pylint: disable=unused-argument
def celery_ping(request): def celery_ping(request):
"""Just tells you if Celery is on or not""" """Just tells you if Celery is on or not"""
try: try:
ping = celery.control.inspect().ping() ping = celery.control.inspect().ping(timeout=5)
if ping: if ping:
return HttpResponse() return HttpResponse()
# pylint: disable=broad-except # pylint: disable=broad-except

View file

@ -76,7 +76,7 @@ class Dashboard(View):
def get_charts_and_stats(request): def get_charts_and_stats(request):
"""Defines the dashbaord charts""" """Defines the dashboard charts"""
interval = int(request.GET.get("days", 1)) interval = int(request.GET.get("days", 1))
now = timezone.now() now = timezone.now()
start = request.GET.get("start") start = request.GET.get("start")

View file

@ -154,7 +154,7 @@ def add_authors(request, data):
data["author_matches"] = [] data["author_matches"] = []
data["isni_matches"] = [] data["isni_matches"] = []
# creting a book or adding an author to a book needs another step # creating a book or adding an author to a book needs another step
data["confirm_mode"] = True data["confirm_mode"] = True
# this isn't preserved because it isn't part of the form obj # this isn't preserved because it isn't part of the form obj
data["remove_authors"] = request.POST.getlist("remove_authors") data["remove_authors"] = request.POST.getlist("remove_authors")

View file

@ -104,7 +104,7 @@ def raise_is_blocked_activity(activity_json):
def sometimes_async_activity_task(activity_json, queue=MEDIUM): def sometimes_async_activity_task(activity_json, queue=MEDIUM):
"""Sometimes we can effectively respond to a request without queuing a new task, """Sometimes we can effectively respond to a request without queuing a new task,
and whever that is possible, we should do it.""" and whenever that is possible, we should do it."""
activity = activitypub.parse(activity_json) activity = activitypub.parse(activity_json)
# try resolving this activity without making any http requests # try resolving this activity without making any http requests
@ -115,7 +115,7 @@ def sometimes_async_activity_task(activity_json, queue=MEDIUM):
activity_task.apply_async(args=(activity_json,), queue=queue) activity_task.apply_async(args=(activity_json,), queue=queue)
@app.task(queue=MEDIUM, ignore_result=True) @app.task(queue=MEDIUM)
def activity_task(activity_json): def activity_task(activity_json):
"""do something with this json we think is legit""" """do something with this json we think is legit"""
# lets see if the activitypub module can make sense of this json # lets see if the activitypub module can make sense of this json

View file

@ -14,7 +14,7 @@ from bookwyrm.views.list.list import normalize_book_list_ordering
# pylint: disable=no-self-use # pylint: disable=no-self-use
@method_decorator(login_required, name="dispatch") @method_decorator(login_required, name="dispatch")
class Curate(View): class Curate(View):
"""approve or discard list suggestsions""" """approve or discard list suggestions"""
def get(self, request, list_id): def get(self, request, list_id):
"""display a pending list""" """display a pending list"""

View file

@ -14,7 +14,7 @@ from bookwyrm.settings import PAGE_LENGTH
# pylint: disable=no-self-use # pylint: disable=no-self-use
class EmbedList(View): class EmbedList(View):
"""embeded book list page""" """embedded book list page"""
def get(self, request, list_id, list_key): def get(self, request, list_id, list_key):
"""display a book list""" """display a book list"""

View file

@ -8,7 +8,7 @@ from django.db import transaction
from django.db.models import Avg, DecimalField, Q, Max from django.db.models import Avg, DecimalField, Q, Max
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.http import HttpResponseBadRequest, HttpResponse from django.http import HttpResponseBadRequest, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -183,7 +183,7 @@ def delete_list(request, list_id):
book_list.raise_not_deletable(request.user) book_list.raise_not_deletable(request.user)
book_list.delete() book_list.delete()
return redirect_to_referer(request, "lists") return redirect("/list")
@require_POST @require_POST

View file

@ -186,7 +186,7 @@ def update_readthrough_on_shelve(
active_readthrough = models.ReadThrough.objects.create( active_readthrough = models.ReadThrough.objects.create(
user=user, book=annotated_book user=user, book=annotated_book
) )
# santiize and set dates # sanitize and set dates
active_readthrough.start_date = load_date_in_user_tz_as_utc(start_date, user) active_readthrough.start_date = load_date_in_user_tz_as_utc(start_date, user)
# if the stop or finish date is set, the readthrough will be set as inactive # if the stop or finish date is set, the readthrough will be set as inactive
active_readthrough.finish_date = load_date_in_user_tz_as_utc(finish_date, user) active_readthrough.finish_date = load_date_in_user_tz_as_utc(finish_date, user)

View file

@ -232,7 +232,7 @@ def find_mentions(user, content):
if not content: if not content:
return {} return {}
# The regex has nested match groups, so the 0th entry has the full (outer) match # The regex has nested match groups, so the 0th entry has the full (outer) match
# And beacuse the strict username starts with @, the username is 1st char onward # And because the strict username starts with @, the username is 1st char onward
usernames = [m[0][1:] for m in re.findall(regex.STRICT_USERNAME, content)] usernames = [m[0][1:] for m in re.findall(regex.STRICT_USERNAME, content)]
known_users = ( known_users = (

View file

@ -9,7 +9,7 @@ from django.utils import timezone
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
from bookwyrm import models from bookwyrm import models
from bookwyrm.settings import DOMAIN, VERSION from bookwyrm.settings import DOMAIN, VERSION, LANGUAGE_CODE
@require_GET @require_GET
@ -110,7 +110,7 @@ def instance_info(_):
"status_count": status_count, "status_count": status_count,
}, },
"thumbnail": logo, "thumbnail": logo,
"languages": ["en"], "languages": [LANGUAGE_CODE[:2]],
"registrations": site.allow_registration, "registrations": site.allow_registration,
"approval_required": not site.allow_registration "approval_required": not site.allow_registration
and site.allow_invite_requests, and site.allow_invite_requests,

56
bw-dev
View file

@ -23,21 +23,27 @@ trap showerr EXIT
source .env source .env
trap - EXIT trap - EXIT
if docker compose &> /dev/null ; then
DOCKER_COMPOSE="docker compose"
else
DOCKER_COMPOSE="docker-compose"
fi
function clean { function clean {
docker-compose stop $DOCKER_COMPOSE stop
docker-compose rm -f $DOCKER_COMPOSE rm -f
} }
function runweb { function runweb {
docker-compose run --rm web "$@" $DOCKER_COMPOSE run --rm web "$@"
} }
function execdb { function execdb {
docker-compose exec db $@ $DOCKER_COMPOSE exec db $@
} }
function execweb { function execweb {
docker-compose exec web "$@" $DOCKER_COMPOSE exec web "$@"
} }
function initdb { function initdb {
@ -75,20 +81,23 @@ set -x
case "$CMD" in case "$CMD" in
up) up)
docker-compose up --build "$@" $DOCKER_COMPOSE up --build "$@"
;;
down)
$DOCKER_COMPOSE down
;; ;;
service_ports_web) service_ports_web)
prod_error prod_error
docker-compose run --rm --service-ports web $DOCKER_COMPOSE run --rm --service-ports web
;; ;;
initdb) initdb)
initdb "@" initdb "@"
;; ;;
resetdb) resetdb)
prod_error prod_error
docker-compose rm -svf $DOCKER_COMPOSE rm -svf
docker volume rm -f bookwyrm_media_volume bookwyrm_pgdata bookwyrm_redis_activity_data bookwyrm_redis_broker_data bookwyrm_static_volume docker volume rm -f bookwyrm_media_volume bookwyrm_pgdata bookwyrm_redis_activity_data bookwyrm_redis_broker_data bookwyrm_static_volume
docker-compose build $DOCKER_COMPOSE build
migrate migrate
migrate django_celery_beat migrate django_celery_beat
initdb initdb
@ -113,7 +122,7 @@ case "$CMD" in
execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB} execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB}
;; ;;
restart_celery) restart_celery)
docker-compose restart celery_worker $DOCKER_COMPOSE restart celery_worker
;; ;;
pytest) pytest)
prod_error prod_error
@ -161,7 +170,7 @@ case "$CMD" in
runweb django-admin compilemessages --ignore venv runweb django-admin compilemessages --ignore venv
;; ;;
build) build)
docker-compose build $DOCKER_COMPOSE build
;; ;;
clean) clean)
prod_error prod_error
@ -169,7 +178,7 @@ case "$CMD" in
;; ;;
black) black)
prod_error prod_error
docker-compose run --rm dev-tools black celerywyrm bookwyrm $DOCKER_COMPOSE run --rm dev-tools black celerywyrm bookwyrm
;; ;;
pylint) pylint)
prod_error prod_error
@ -178,25 +187,25 @@ case "$CMD" in
;; ;;
prettier) prettier)
prod_error prod_error
docker-compose run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js $DOCKER_COMPOSE run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js
;; ;;
eslint) eslint)
prod_error prod_error
docker-compose run --rm dev-tools npx eslint bookwyrm/static --ext .js $DOCKER_COMPOSE run --rm dev-tools npx eslint bookwyrm/static --ext .js
;; ;;
stylelint) stylelint)
prod_error prod_error
docker-compose run --rm dev-tools npx stylelint \ $DOCKER_COMPOSE run --rm dev-tools npx stylelint \
bookwyrm/static/css/bookwyrm.scss bookwyrm/static/css/bookwyrm/**/*.scss --fix \ bookwyrm/static/css/bookwyrm.scss bookwyrm/static/css/bookwyrm/**/*.scss --fix \
--config dev-tools/.stylelintrc.js --config dev-tools/.stylelintrc.js
;; ;;
formatters) formatters)
prod_error prod_error
runweb pylint bookwyrm/ runweb pylint bookwyrm/
docker-compose run --rm dev-tools black celerywyrm bookwyrm $DOCKER_COMPOSE run --rm dev-tools black celerywyrm bookwyrm
docker-compose run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js $DOCKER_COMPOSE run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js
docker-compose run --rm dev-tools npx eslint bookwyrm/static --ext .js $DOCKER_COMPOSE run --rm dev-tools npx eslint bookwyrm/static --ext .js
docker-compose run --rm dev-tools npx stylelint \ $DOCKER_COMPOSE run --rm dev-tools npx stylelint \
bookwyrm/static/css/bookwyrm.scss bookwyrm/static/css/bookwyrm/**/*.scss --fix \ bookwyrm/static/css/bookwyrm.scss bookwyrm/static/css/bookwyrm/**/*.scss --fix \
--config dev-tools/.stylelintrc.js --config dev-tools/.stylelintrc.js
;; ;;
@ -206,14 +215,14 @@ case "$CMD" in
;; ;;
update) update)
git pull git pull
docker-compose build $DOCKER_COMPOSE build
# ./update.sh # ./update.sh
runweb python manage.py migrate runweb python manage.py migrate
runweb python manage.py compile_themes runweb python manage.py compile_themes
runweb python manage.py collectstatic --no-input runweb python manage.py collectstatic --no-input
docker-compose up -d $DOCKER_COMPOSE up -d
docker-compose restart web $DOCKER_COMPOSE restart web
docker-compose restart celery_worker $DOCKER_COMPOSE restart celery_worker
;; ;;
populate_streams) populate_streams)
runweb python manage.py populate_streams "$@" runweb python manage.py populate_streams "$@"
@ -284,6 +293,7 @@ case "$CMD" in
echo "Unrecognised command. Try:" echo "Unrecognised command. Try:"
echo " setup" echo " setup"
echo " up [container]" echo " up [container]"
echo " down"
echo " service_ports_web" echo " service_ports_web"
echo " initdb" echo " initdb"
echo " resetdb" echo " resetdb"

View file

@ -11,3 +11,4 @@ class CelerywyrmConfig(AppConfig):
from bookwyrm.telemetry import open_telemetry from bookwyrm.telemetry import open_telemetry
open_telemetry.instrumentCelery() open_telemetry.instrumentCelery()
open_telemetry.instrumentPostgres()

View file

@ -3,6 +3,8 @@
# pylint: disable=unused-wildcard-import # pylint: disable=unused-wildcard-import
from bookwyrm.settings import * from bookwyrm.settings import *
QUERY_TIMEOUT = env.int("CELERY_QUERY_TIMEOUT", env.int("QUERY_TIMEOUT", 30))
# pylint: disable=line-too-long # pylint: disable=line-too-long
REDIS_BROKER_PASSWORD = requests.utils.quote(env("REDIS_BROKER_PASSWORD", "")) REDIS_BROKER_PASSWORD = requests.utils.quote(env("REDIS_BROKER_PASSWORD", ""))
REDIS_BROKER_HOST = env("REDIS_BROKER_HOST", "redis_broker") REDIS_BROKER_HOST = env("REDIS_BROKER_HOST", "redis_broker")

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: bookwyrm\n" "Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-13 14:54+0000\n" "POT-Creation-Date: 2023-03-29 14:55+0000\n"
"PO-Revision-Date: 2023-03-13 16:38\n" "PO-Revision-Date: 2023-04-07 14:21\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n" "Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Catalan\n" "Language-Team: Catalan\n"
"Language: ca\n" "Language: ca\n"
@ -316,19 +316,19 @@ msgstr "Citacions"
msgid "Everything else" msgid "Everything else"
msgstr "Tota la resta" msgstr "Tota la resta"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home Timeline" msgid "Home Timeline"
msgstr "Línia de temps Inici" msgstr "Línia de temps Inici"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home" msgid "Home"
msgstr "Inici" msgstr "Inici"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
msgid "Books Timeline" msgid "Books Timeline"
msgstr "Cronologia dels llibres" msgstr "Cronologia dels llibres"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
#: bookwyrm/templates/guided_tour/user_profile.html:101 #: bookwyrm/templates/guided_tour/user_profile.html:101
#: bookwyrm/templates/search/layout.html:22 #: bookwyrm/templates/search/layout.html:22
#: bookwyrm/templates/search/layout.html:43 #: bookwyrm/templates/search/layout.html:43
@ -336,75 +336,79 @@ msgstr "Cronologia dels llibres"
msgid "Books" msgid "Books"
msgstr "Llibres" msgstr "Llibres"
#: bookwyrm/settings.py:291 #: bookwyrm/settings.py:294
msgid "English" msgid "English"
msgstr "English (Anglès)" msgstr "English (Anglès)"
#: bookwyrm/settings.py:292 #: bookwyrm/settings.py:295
msgid "Català (Catalan)" msgid "Català (Catalan)"
msgstr "Català" msgstr "Català"
#: bookwyrm/settings.py:293 #: bookwyrm/settings.py:296
msgid "Deutsch (German)" msgid "Deutsch (German)"
msgstr "Deutsch (Alemany)" msgstr "Deutsch (Alemany)"
#: bookwyrm/settings.py:294 #: bookwyrm/settings.py:297
msgid "Esperanto (Esperanto)"
msgstr ""
#: bookwyrm/settings.py:298
msgid "Español (Spanish)" msgid "Español (Spanish)"
msgstr "Español (espanyol)" msgstr "Español (espanyol)"
#: bookwyrm/settings.py:295 #: bookwyrm/settings.py:299
msgid "Euskara (Basque)" msgid "Euskara (Basque)"
msgstr "Euskera (Basc)" msgstr "Euskera (Basc)"
#: bookwyrm/settings.py:296 #: bookwyrm/settings.py:300
msgid "Galego (Galician)" msgid "Galego (Galician)"
msgstr "Galego (gallec)" msgstr "Galego (gallec)"
#: bookwyrm/settings.py:297 #: bookwyrm/settings.py:301
msgid "Italiano (Italian)" msgid "Italiano (Italian)"
msgstr "Italiano (italià)" msgstr "Italiano (italià)"
#: bookwyrm/settings.py:298 #: bookwyrm/settings.py:302
msgid "Suomi (Finnish)" msgid "Suomi (Finnish)"
msgstr "Suomi (finès)" msgstr "Suomi (finès)"
#: bookwyrm/settings.py:299 #: bookwyrm/settings.py:303
msgid "Français (French)" msgid "Français (French)"
msgstr "Français (francès)" msgstr "Français (francès)"
#: bookwyrm/settings.py:300 #: bookwyrm/settings.py:304
msgid "Lietuvių (Lithuanian)" msgid "Lietuvių (Lithuanian)"
msgstr "Lietuvių (Lituà)" msgstr "Lietuvių (Lituà)"
#: bookwyrm/settings.py:301 #: bookwyrm/settings.py:305
msgid "Norsk (Norwegian)" msgid "Norsk (Norwegian)"
msgstr "Norsk (noruec)" msgstr "Norsk (noruec)"
#: bookwyrm/settings.py:302 #: bookwyrm/settings.py:306
msgid "Polski (Polish)" msgid "Polski (Polish)"
msgstr "Polski (polonès)" msgstr "Polski (polonès)"
#: bookwyrm/settings.py:303 #: bookwyrm/settings.py:307
msgid "Português do Brasil (Brazilian Portuguese)" msgid "Português do Brasil (Brazilian Portuguese)"
msgstr "Português do Brasil (portuguès del Brasil)" msgstr "Português do Brasil (portuguès del Brasil)"
#: bookwyrm/settings.py:304 #: bookwyrm/settings.py:308
msgid "Português Europeu (European Portuguese)" msgid "Português Europeu (European Portuguese)"
msgstr "Português Europeu (Portuguès europeu)" msgstr "Português Europeu (Portuguès europeu)"
#: bookwyrm/settings.py:305 #: bookwyrm/settings.py:309
msgid "Română (Romanian)" msgid "Română (Romanian)"
msgstr "Română (romanès)" msgstr "Română (romanès)"
#: bookwyrm/settings.py:306 #: bookwyrm/settings.py:310
msgid "Svenska (Swedish)" msgid "Svenska (Swedish)"
msgstr "Svenska (suec)" msgstr "Svenska (suec)"
#: bookwyrm/settings.py:307 #: bookwyrm/settings.py:311
msgid "简体中文 (Simplified Chinese)" msgid "简体中文 (Simplified Chinese)"
msgstr "简体中文 (xinès simplificat)" msgstr "简体中文 (xinès simplificat)"
#: bookwyrm/settings.py:308 #: bookwyrm/settings.py:312
msgid "繁體中文 (Traditional Chinese)" msgid "繁體中文 (Traditional Chinese)"
msgstr "繁體中文 (xinès tradicional)" msgstr "繁體中文 (xinès tradicional)"
@ -842,7 +846,7 @@ msgstr "ISNI:"
#: bookwyrm/templates/lists/bookmark_button.html:15 #: bookwyrm/templates/lists/bookmark_button.html:15
#: bookwyrm/templates/lists/edit_item_form.html:15 #: bookwyrm/templates/lists/edit_item_form.html:15
#: bookwyrm/templates/lists/form.html:130 #: bookwyrm/templates/lists/form.html:130
#: bookwyrm/templates/preferences/edit_user.html:136 #: bookwyrm/templates/preferences/edit_user.html:140
#: bookwyrm/templates/readthrough/readthrough_modal.html:81 #: bookwyrm/templates/readthrough/readthrough_modal.html:81
#: bookwyrm/templates/settings/announcements/edit_announcement.html:120 #: bookwyrm/templates/settings/announcements/edit_announcement.html:120
#: bookwyrm/templates/settings/federation/edit_instance.html:98 #: bookwyrm/templates/settings/federation/edit_instance.html:98
@ -4038,6 +4042,11 @@ msgstr "Oculta els seguidors i els que segueixo al perfil"
msgid "Default post privacy:" msgid "Default post privacy:"
msgstr "Privacitat de publicació per defecte:" msgstr "Privacitat de publicació per defecte:"
#: bookwyrm/templates/preferences/edit_user.html:136
#, python-format
msgid "Looking for shelf privacy? You can set a separate visibility level for each of your shelves. Go to <a href=\"%(path)s\">Your Books</a>, pick a shelf from the tab bar, and click \"Edit shelf.\""
msgstr ""
#: bookwyrm/templates/preferences/export.html:4 #: bookwyrm/templates/preferences/export.html:4
#: bookwyrm/templates/preferences/export.html:7 #: bookwyrm/templates/preferences/export.html:7
msgid "CSV Export" msgid "CSV Export"

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: bookwyrm\n" "Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-13 14:54+0000\n" "POT-Creation-Date: 2023-03-29 14:55+0000\n"
"PO-Revision-Date: 2023-03-26 15:31\n" "PO-Revision-Date: 2023-04-07 14:21\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n" "Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: German\n" "Language-Team: German\n"
"Language: de\n" "Language: de\n"
@ -316,19 +316,19 @@ msgstr "Zitate"
msgid "Everything else" msgid "Everything else"
msgstr "Alles andere" msgstr "Alles andere"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home Timeline" msgid "Home Timeline"
msgstr "Start-Zeitleiste" msgstr "Start-Zeitleiste"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home" msgid "Home"
msgstr "Startseite" msgstr "Startseite"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
msgid "Books Timeline" msgid "Books Timeline"
msgstr "Bücher-Timeline" msgstr "Bücher-Timeline"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
#: bookwyrm/templates/guided_tour/user_profile.html:101 #: bookwyrm/templates/guided_tour/user_profile.html:101
#: bookwyrm/templates/search/layout.html:22 #: bookwyrm/templates/search/layout.html:22
#: bookwyrm/templates/search/layout.html:43 #: bookwyrm/templates/search/layout.html:43
@ -336,75 +336,79 @@ msgstr "Bücher-Timeline"
msgid "Books" msgid "Books"
msgstr "Bücher" msgstr "Bücher"
#: bookwyrm/settings.py:291 #: bookwyrm/settings.py:294
msgid "English" msgid "English"
msgstr "English (Englisch)" msgstr "English (Englisch)"
#: bookwyrm/settings.py:292 #: bookwyrm/settings.py:295
msgid "Català (Catalan)" msgid "Català (Catalan)"
msgstr "Català (Katalanisch)" msgstr "Català (Katalanisch)"
#: bookwyrm/settings.py:293 #: bookwyrm/settings.py:296
msgid "Deutsch (German)" msgid "Deutsch (German)"
msgstr "Deutsch" msgstr "Deutsch"
#: bookwyrm/settings.py:294 #: bookwyrm/settings.py:297
msgid "Esperanto (Esperanto)"
msgstr "Esperanto (Esperanto)"
#: bookwyrm/settings.py:298
msgid "Español (Spanish)" msgid "Español (Spanish)"
msgstr "Español (Spanisch)" msgstr "Español (Spanisch)"
#: bookwyrm/settings.py:295 #: bookwyrm/settings.py:299
msgid "Euskara (Basque)" msgid "Euskara (Basque)"
msgstr "Euskara (Baskisch)" msgstr "Euskara (Baskisch)"
#: bookwyrm/settings.py:296 #: bookwyrm/settings.py:300
msgid "Galego (Galician)" msgid "Galego (Galician)"
msgstr "Galego (Galizisch)" msgstr "Galego (Galizisch)"
#: bookwyrm/settings.py:297 #: bookwyrm/settings.py:301
msgid "Italiano (Italian)" msgid "Italiano (Italian)"
msgstr "Italiano (Italienisch)" msgstr "Italiano (Italienisch)"
#: bookwyrm/settings.py:298 #: bookwyrm/settings.py:302
msgid "Suomi (Finnish)" msgid "Suomi (Finnish)"
msgstr "Suomi (Finnisch)" msgstr "Suomi (Finnisch)"
#: bookwyrm/settings.py:299 #: bookwyrm/settings.py:303
msgid "Français (French)" msgid "Français (French)"
msgstr "Français (Französisch)" msgstr "Français (Französisch)"
#: bookwyrm/settings.py:300 #: bookwyrm/settings.py:304
msgid "Lietuvių (Lithuanian)" msgid "Lietuvių (Lithuanian)"
msgstr "Lietuvių (Litauisch)" msgstr "Lietuvių (Litauisch)"
#: bookwyrm/settings.py:301 #: bookwyrm/settings.py:305
msgid "Norsk (Norwegian)" msgid "Norsk (Norwegian)"
msgstr "Norsk (Norwegisch)" msgstr "Norsk (Norwegisch)"
#: bookwyrm/settings.py:302 #: bookwyrm/settings.py:306
msgid "Polski (Polish)" msgid "Polski (Polish)"
msgstr "Polski (Polnisch)" msgstr "Polski (Polnisch)"
#: bookwyrm/settings.py:303 #: bookwyrm/settings.py:307
msgid "Português do Brasil (Brazilian Portuguese)" msgid "Português do Brasil (Brazilian Portuguese)"
msgstr "Português do Brasil (brasilianisches Portugiesisch)" msgstr "Português do Brasil (brasilianisches Portugiesisch)"
#: bookwyrm/settings.py:304 #: bookwyrm/settings.py:308
msgid "Português Europeu (European Portuguese)" msgid "Português Europeu (European Portuguese)"
msgstr "Português Europeu (Portugiesisch)" msgstr "Português Europeu (Portugiesisch)"
#: bookwyrm/settings.py:305 #: bookwyrm/settings.py:309
msgid "Română (Romanian)" msgid "Română (Romanian)"
msgstr "Română (Rumänisch)" msgstr "Română (Rumänisch)"
#: bookwyrm/settings.py:306 #: bookwyrm/settings.py:310
msgid "Svenska (Swedish)" msgid "Svenska (Swedish)"
msgstr "Svenska (Schwedisch)" msgstr "Svenska (Schwedisch)"
#: bookwyrm/settings.py:307 #: bookwyrm/settings.py:311
msgid "简体中文 (Simplified Chinese)" msgid "简体中文 (Simplified Chinese)"
msgstr "简体中文 (vereinfachtes Chinesisch)" msgstr "简体中文 (vereinfachtes Chinesisch)"
#: bookwyrm/settings.py:308 #: bookwyrm/settings.py:312
msgid "繁體中文 (Traditional Chinese)" msgid "繁體中文 (Traditional Chinese)"
msgstr "繁體中文 (Chinesisch, traditionell)" msgstr "繁體中文 (Chinesisch, traditionell)"
@ -842,7 +846,7 @@ msgstr "ISNI:"
#: bookwyrm/templates/lists/bookmark_button.html:15 #: bookwyrm/templates/lists/bookmark_button.html:15
#: bookwyrm/templates/lists/edit_item_form.html:15 #: bookwyrm/templates/lists/edit_item_form.html:15
#: bookwyrm/templates/lists/form.html:130 #: bookwyrm/templates/lists/form.html:130
#: bookwyrm/templates/preferences/edit_user.html:136 #: bookwyrm/templates/preferences/edit_user.html:140
#: bookwyrm/templates/readthrough/readthrough_modal.html:81 #: bookwyrm/templates/readthrough/readthrough_modal.html:81
#: bookwyrm/templates/settings/announcements/edit_announcement.html:120 #: bookwyrm/templates/settings/announcements/edit_announcement.html:120
#: bookwyrm/templates/settings/federation/edit_instance.html:98 #: bookwyrm/templates/settings/federation/edit_instance.html:98
@ -3033,7 +3037,7 @@ msgstr "Einladung beantragen"
#: bookwyrm/templates/landing/layout.html:50 #: bookwyrm/templates/landing/layout.html:50
#, python-format #, python-format
msgid "%(name)s registration is closed" msgid "%(name)s registration is closed"
msgstr "%(name)s erlaubt keine Selbtregistrierung" msgstr "%(name)s erlaubt keine Selbstregistrierung"
#: bookwyrm/templates/landing/layout.html:61 #: bookwyrm/templates/landing/layout.html:61
msgid "Thank you! Your request has been received." msgid "Thank you! Your request has been received."
@ -4038,6 +4042,11 @@ msgstr "Folgende und Gefolgte im Profil ausblenden"
msgid "Default post privacy:" msgid "Default post privacy:"
msgstr "Voreinstellung für Beitragssichtbarkeit:" msgstr "Voreinstellung für Beitragssichtbarkeit:"
#: bookwyrm/templates/preferences/edit_user.html:136
#, python-format
msgid "Looking for shelf privacy? You can set a separate visibility level for each of your shelves. Go to <a href=\"%(path)s\">Your Books</a>, pick a shelf from the tab bar, and click \"Edit shelf.\""
msgstr ""
#: bookwyrm/templates/preferences/export.html:4 #: bookwyrm/templates/preferences/export.html:4
#: bookwyrm/templates/preferences/export.html:7 #: bookwyrm/templates/preferences/export.html:7
msgid "CSV Export" msgid "CSV Export"

View file

@ -1,4 +1,4 @@
# Stub English-language trnaslation file # Stub English-language translation file
# Copyright (C) 2021 Mouse Reeve # Copyright (C) 2021 Mouse Reeve
# This file is distributed under the same license as the BookWyrm package. # This file is distributed under the same license as the BookWyrm package.
# Mouse Reeve <mousereeve@riseup.net>, 2021 # Mouse Reeve <mousereeve@riseup.net>, 2021
@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 0.0.1\n" "Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-29 14:55+0000\n" "POT-Creation-Date: 2023-04-26 00:20+0000\n"
"PO-Revision-Date: 2021-02-28 17:19-0800\n" "PO-Revision-Date: 2021-02-28 17:19-0800\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n" "Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: English <LL@li.org>\n" "Language-Team: English <LL@li.org>\n"
@ -276,11 +276,11 @@ msgstr ""
msgid "Import stopped" msgid "Import stopped"
msgstr "" msgstr ""
#: bookwyrm/models/import_job.py:360 bookwyrm/models/import_job.py:385 #: bookwyrm/models/import_job.py:363 bookwyrm/models/import_job.py:388
msgid "Error loading book" msgid "Error loading book"
msgstr "" msgstr ""
#: bookwyrm/models/import_job.py:369 #: bookwyrm/models/import_job.py:372
msgid "Could not find a match for book" msgid "Could not find a match for book"
msgstr "" msgstr ""
@ -301,7 +301,7 @@ msgstr ""
msgid "Approved" msgid "Approved"
msgstr "" msgstr ""
#: bookwyrm/models/user.py:32 bookwyrm/templates/book/book.html:298 #: bookwyrm/models/user.py:32 bookwyrm/templates/book/book.html:305
msgid "Reviews" msgid "Reviews"
msgstr "" msgstr ""
@ -625,7 +625,7 @@ msgstr ""
#: bookwyrm/templates/annual_summary/layout.html:157 #: bookwyrm/templates/annual_summary/layout.html:157
#: bookwyrm/templates/annual_summary/layout.html:178 #: bookwyrm/templates/annual_summary/layout.html:178
#: bookwyrm/templates/annual_summary/layout.html:247 #: bookwyrm/templates/annual_summary/layout.html:247
#: bookwyrm/templates/book/book.html:56 #: bookwyrm/templates/book/book.html:63
#: bookwyrm/templates/discover/large-book.html:22 #: bookwyrm/templates/discover/large-book.html:22
#: bookwyrm/templates/landing/large-book.html:26 #: bookwyrm/templates/landing/large-book.html:26
#: bookwyrm/templates/landing/small-book.html:18 #: bookwyrm/templates/landing/small-book.html:18
@ -713,24 +713,24 @@ msgid "View ISNI record"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:95 #: bookwyrm/templates/author/author.html:95
#: bookwyrm/templates/book/book.html:166 #: bookwyrm/templates/book/book.html:173
msgid "View on ISFDB" msgid "View on ISFDB"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:100 #: bookwyrm/templates/author/author.html:100
#: bookwyrm/templates/author/sync_modal.html:5 #: bookwyrm/templates/author/sync_modal.html:5
#: bookwyrm/templates/book/book.html:133 #: bookwyrm/templates/book/book.html:140
#: bookwyrm/templates/book/sync_modal.html:5 #: bookwyrm/templates/book/sync_modal.html:5
msgid "Load data" msgid "Load data"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:104 #: bookwyrm/templates/author/author.html:104
#: bookwyrm/templates/book/book.html:137 #: bookwyrm/templates/book/book.html:144
msgid "View on OpenLibrary" msgid "View on OpenLibrary"
msgstr "" msgstr ""
#: bookwyrm/templates/author/author.html:119 #: bookwyrm/templates/author/author.html:119
#: bookwyrm/templates/book/book.html:151 #: bookwyrm/templates/book/book.html:158
msgid "View on Inventaire" msgid "View on Inventaire"
msgstr "" msgstr ""
@ -839,7 +839,7 @@ msgid "ISNI:"
msgstr "" msgstr ""
#: bookwyrm/templates/author/edit_author.html:126 #: bookwyrm/templates/author/edit_author.html:126
#: bookwyrm/templates/book/book.html:211 #: bookwyrm/templates/book/book.html:218
#: bookwyrm/templates/book/edit/edit_book.html:150 #: bookwyrm/templates/book/edit/edit_book.html:150
#: bookwyrm/templates/book/file_links/add_link_modal.html:60 #: bookwyrm/templates/book/file_links/add_link_modal.html:60
#: bookwyrm/templates/book/file_links/edit_links.html:86 #: bookwyrm/templates/book/file_links/edit_links.html:86
@ -863,7 +863,7 @@ msgstr ""
#: bookwyrm/templates/author/edit_author.html:127 #: bookwyrm/templates/author/edit_author.html:127
#: bookwyrm/templates/author/sync_modal.html:23 #: bookwyrm/templates/author/sync_modal.html:23
#: bookwyrm/templates/book/book.html:212 #: bookwyrm/templates/book/book.html:219
#: bookwyrm/templates/book/cover_add_modal.html:33 #: bookwyrm/templates/book/cover_add_modal.html:33
#: bookwyrm/templates/book/edit/edit_book.html:152 #: bookwyrm/templates/book/edit/edit_book.html:152
#: bookwyrm/templates/book/edit/edit_book.html:155 #: bookwyrm/templates/book/edit/edit_book.html:155
@ -900,93 +900,93 @@ msgstr ""
msgid "Confirm" msgid "Confirm"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:19 #: bookwyrm/templates/book/book.html:20
msgid "Unable to connect to remote source." msgid "Unable to connect to remote source."
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:64 bookwyrm/templates/book/book.html:65 #: bookwyrm/templates/book/book.html:71 bookwyrm/templates/book/book.html:72
msgid "Edit Book" msgid "Edit Book"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:90 bookwyrm/templates/book/book.html:93 #: bookwyrm/templates/book/book.html:97 bookwyrm/templates/book/book.html:100
msgid "Click to add cover" msgid "Click to add cover"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:99 #: bookwyrm/templates/book/book.html:106
msgid "Failed to load cover" msgid "Failed to load cover"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:110 #: bookwyrm/templates/book/book.html:117
msgid "Click to enlarge" msgid "Click to enlarge"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:188 #: bookwyrm/templates/book/book.html:195
#, python-format #, python-format
msgid "(%(review_count)s review)" msgid "(%(review_count)s review)"
msgid_plural "(%(review_count)s reviews)" msgid_plural "(%(review_count)s reviews)"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: bookwyrm/templates/book/book.html:200 #: bookwyrm/templates/book/book.html:207
msgid "Add Description" msgid "Add Description"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:207 #: bookwyrm/templates/book/book.html:214
#: bookwyrm/templates/book/edit/edit_book_form.html:42 #: bookwyrm/templates/book/edit/edit_book_form.html:42
#: bookwyrm/templates/lists/form.html:13 bookwyrm/templates/shelf/form.html:17 #: bookwyrm/templates/lists/form.html:13 bookwyrm/templates/shelf/form.html:17
msgid "Description:" msgid "Description:"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:223 #: bookwyrm/templates/book/book.html:230
#, python-format #, python-format
msgid "%(count)s edition" msgid "%(count)s edition"
msgid_plural "%(count)s editions" msgid_plural "%(count)s editions"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: bookwyrm/templates/book/book.html:237 #: bookwyrm/templates/book/book.html:244
msgid "You have shelved this edition in:" msgid "You have shelved this edition in:"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:252 #: bookwyrm/templates/book/book.html:259
#, python-format #, python-format
msgid "A <a href=\"%(book_path)s\">different edition</a> of this book is on your <a href=\"%(shelf_path)s\">%(shelf_name)s</a> shelf." msgid "A <a href=\"%(book_path)s\">different edition</a> of this book is on your <a href=\"%(shelf_path)s\">%(shelf_name)s</a> shelf."
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:263 #: bookwyrm/templates/book/book.html:270
msgid "Your reading activity" msgid "Your reading activity"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:269 #: bookwyrm/templates/book/book.html:276
#: bookwyrm/templates/guided_tour/book.html:56 #: bookwyrm/templates/guided_tour/book.html:56
msgid "Add read dates" msgid "Add read dates"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:277 #: bookwyrm/templates/book/book.html:284
msgid "You don't have any reading activity for this book." msgid "You don't have any reading activity for this book."
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:303 #: bookwyrm/templates/book/book.html:310
msgid "Your reviews" msgid "Your reviews"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:309 #: bookwyrm/templates/book/book.html:316
msgid "Your comments" msgid "Your comments"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:315 #: bookwyrm/templates/book/book.html:322
msgid "Your quotes" msgid "Your quotes"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:351 #: bookwyrm/templates/book/book.html:358
msgid "Subjects" msgid "Subjects"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:363 #: bookwyrm/templates/book/book.html:370
msgid "Places" msgid "Places"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:374 #: bookwyrm/templates/book/book.html:381
#: bookwyrm/templates/groups/group.html:19 #: bookwyrm/templates/groups/group.html:19
#: bookwyrm/templates/guided_tour/lists.html:14 #: bookwyrm/templates/guided_tour/lists.html:14
#: bookwyrm/templates/guided_tour/user_books.html:102 #: bookwyrm/templates/guided_tour/user_books.html:102
@ -1000,11 +1000,11 @@ msgstr ""
msgid "Lists" msgid "Lists"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:386 #: bookwyrm/templates/book/book.html:393
msgid "Add to list" msgid "Add to list"
msgstr "" msgstr ""
#: bookwyrm/templates/book/book.html:396 #: bookwyrm/templates/book/book.html:403
#: bookwyrm/templates/book/cover_add_modal.html:32 #: bookwyrm/templates/book/cover_add_modal.html:32
#: bookwyrm/templates/lists/add_item_modal.html:39 #: bookwyrm/templates/lists/add_item_modal.html:39
#: bookwyrm/templates/lists/list.html:255 #: bookwyrm/templates/lists/list.html:255
@ -1928,13 +1928,13 @@ msgstr ""
#: bookwyrm/templates/get_started/book_preview.html:10 #: bookwyrm/templates/get_started/book_preview.html:10
#: bookwyrm/templates/shelf/shelf.html:86 bookwyrm/templates/user/user.html:37 #: bookwyrm/templates/shelf/shelf.html:86 bookwyrm/templates/user/user.html:37
#: bookwyrm/templatetags/shelf_tags.py:48 #: bookwyrm/templatetags/shelf_tags.py:14
msgid "To Read" msgid "To Read"
msgstr "" msgstr ""
#: bookwyrm/templates/get_started/book_preview.html:11 #: bookwyrm/templates/get_started/book_preview.html:11
#: bookwyrm/templates/shelf/shelf.html:87 bookwyrm/templates/user/user.html:38 #: bookwyrm/templates/shelf/shelf.html:87 bookwyrm/templates/user/user.html:38
#: bookwyrm/templatetags/shelf_tags.py:50 #: bookwyrm/templatetags/shelf_tags.py:15
msgid "Currently Reading" msgid "Currently Reading"
msgstr "" msgstr ""
@ -1943,12 +1943,13 @@ msgstr ""
#: bookwyrm/templates/snippets/shelf_selector.html:46 #: bookwyrm/templates/snippets/shelf_selector.html:46
#: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html:24 #: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html:24
#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:12 #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:12
#: bookwyrm/templates/user/user.html:39 bookwyrm/templatetags/shelf_tags.py:52 #: bookwyrm/templates/user/user.html:39 bookwyrm/templatetags/shelf_tags.py:16
msgid "Read" msgid "Read"
msgstr "" msgstr ""
#: bookwyrm/templates/get_started/book_preview.html:13 #: bookwyrm/templates/get_started/book_preview.html:13
#: bookwyrm/templates/shelf/shelf.html:89 bookwyrm/templates/user/user.html:40 #: bookwyrm/templates/shelf/shelf.html:89 bookwyrm/templates/user/user.html:40
#: bookwyrm/templatetags/shelf_tags.py:17
msgid "Stopped Reading" msgid "Stopped Reading"
msgstr "" msgstr ""
@ -1980,6 +1981,7 @@ msgstr ""
#: bookwyrm/templates/layout.html:46 bookwyrm/templates/lists/list.html:217 #: bookwyrm/templates/layout.html:46 bookwyrm/templates/lists/list.html:217
#: bookwyrm/templates/search/layout.html:5 #: bookwyrm/templates/search/layout.html:5
#: bookwyrm/templates/search/layout.html:10 #: bookwyrm/templates/search/layout.html:10
#: bookwyrm/templates/search/layout.html:32
msgid "Search" msgid "Search"
msgstr "" msgstr ""
@ -1987,6 +1989,10 @@ msgstr ""
msgid "Suggested Books" msgid "Suggested Books"
msgstr "" msgstr ""
#: bookwyrm/templates/get_started/books.html:33
msgid "Search results"
msgstr ""
#: bookwyrm/templates/get_started/books.html:46 #: bookwyrm/templates/get_started/books.html:46
#, python-format #, python-format
msgid "Popular on %(site_name)s" msgid "Popular on %(site_name)s"
@ -2065,6 +2071,10 @@ msgstr ""
msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users."
msgstr "" msgstr ""
#: bookwyrm/templates/get_started/users.html:8
msgid "You can follow users on other BookWyrm instances and federated services like Mastodon."
msgstr ""
#: bookwyrm/templates/get_started/users.html:11 #: bookwyrm/templates/get_started/users.html:11
msgid "Search for a user" msgid "Search for a user"
msgstr "" msgstr ""
@ -4045,7 +4055,7 @@ msgstr ""
#: bookwyrm/templates/preferences/edit_user.html:136 #: bookwyrm/templates/preferences/edit_user.html:136
#, python-format #, python-format
msgid "Looking for shelf privacy? You can set a sepearate visibility level for each of your shelves. Go to <a href=\"%(path)s\">Your Books</a>, pick a shelf from the tab bar, and click \"Edit shelf.\"" msgid "Looking for shelf privacy? You can set a separate visibility level for each of your shelves. Go to <a href=\"%(path)s\">Your Books</a>, pick a shelf from the tab bar, and click \"Edit shelf.\""
msgstr "" msgstr ""
#: bookwyrm/templates/preferences/export.html:4 #: bookwyrm/templates/preferences/export.html:4
@ -4536,7 +4546,16 @@ msgstr ""
msgid "Could not connect to Celery" msgid "Could not connect to Celery"
msgstr "" msgstr ""
#: bookwyrm/templates/settings/celery.html:121 #: bookwyrm/templates/settings/celery.html:120
#: bookwyrm/templates/settings/celery.html:143
msgid "Clear Queues"
msgstr ""
#: bookwyrm/templates/settings/celery.html:124
msgid "Clearing queues can cause serious problems including data loss! Only play with this if you really know what you're doing. You must shut down the Celery worker before you do this."
msgstr ""
#: bookwyrm/templates/settings/celery.html:150
msgid "Errors" msgid "Errors"
msgstr "" msgstr ""
@ -4901,7 +4920,7 @@ msgid "This is only intended to be used when things have gone very wrong with im
msgstr "" msgstr ""
#: bookwyrm/templates/settings/imports/imports.html:31 #: bookwyrm/templates/settings/imports/imports.html:31
msgid "While imports are disabled, users will not be allowed to start new imports, but existing imports will not be effected." msgid "While imports are disabled, users will not be allowed to start new imports, but existing imports will not be affected."
msgstr "" msgstr ""
#: bookwyrm/templates/settings/imports/imports.html:36 #: bookwyrm/templates/settings/imports/imports.html:36
@ -5757,7 +5776,7 @@ msgid "User profile"
msgstr "" msgstr ""
#: bookwyrm/templates/shelf/shelf.html:39 #: bookwyrm/templates/shelf/shelf.html:39
#: bookwyrm/templatetags/shelf_tags.py:46 bookwyrm/views/shelf/shelf.py:53 #: bookwyrm/templatetags/shelf_tags.py:13 bookwyrm/views/shelf/shelf.py:53
msgid "All books" msgid "All books"
msgstr "" msgstr ""

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: bookwyrm\n" "Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-13 14:54+0000\n" "POT-Creation-Date: 2023-03-29 14:55+0000\n"
"PO-Revision-Date: 2023-03-27 09:50\n" "PO-Revision-Date: 2023-04-10 08:55\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n" "Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Esperanto\n" "Language-Team: Esperanto\n"
"Language: eo\n" "Language: eo\n"
@ -316,19 +316,19 @@ msgstr "Citaĵoj"
msgid "Everything else" msgid "Everything else"
msgstr "Ĉio alia" msgstr "Ĉio alia"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home Timeline" msgid "Home Timeline"
msgstr "Hejma novaĵfluo" msgstr "Hejma novaĵfluo"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home" msgid "Home"
msgstr "Hejmo" msgstr "Hejmo"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
msgid "Books Timeline" msgid "Books Timeline"
msgstr "Libra novaĵfluo" msgstr "Libra novaĵfluo"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
#: bookwyrm/templates/guided_tour/user_profile.html:101 #: bookwyrm/templates/guided_tour/user_profile.html:101
#: bookwyrm/templates/search/layout.html:22 #: bookwyrm/templates/search/layout.html:22
#: bookwyrm/templates/search/layout.html:43 #: bookwyrm/templates/search/layout.html:43
@ -336,75 +336,79 @@ msgstr "Libra novaĵfluo"
msgid "Books" msgid "Books"
msgstr "Libroj" msgstr "Libroj"
#: bookwyrm/settings.py:291 #: bookwyrm/settings.py:294
msgid "English" msgid "English"
msgstr "English (Angla)" msgstr "English (Angla)"
#: bookwyrm/settings.py:292 #: bookwyrm/settings.py:295
msgid "Català (Catalan)" msgid "Català (Catalan)"
msgstr "Català (Kataluna)" msgstr "Català (Kataluna)"
#: bookwyrm/settings.py:293 #: bookwyrm/settings.py:296
msgid "Deutsch (German)" msgid "Deutsch (German)"
msgstr "Deutsch (Germana)" msgstr "Deutsch (Germana)"
#: bookwyrm/settings.py:294 #: bookwyrm/settings.py:297
msgid "Esperanto (Esperanto)"
msgstr "Esperanto (Esperanto)"
#: bookwyrm/settings.py:298
msgid "Español (Spanish)" msgid "Español (Spanish)"
msgstr "Español (Hispana)" msgstr "Español (Hispana)"
#: bookwyrm/settings.py:295 #: bookwyrm/settings.py:299
msgid "Euskara (Basque)" msgid "Euskara (Basque)"
msgstr "Euskara (Eŭska)" msgstr "Euskara (Eŭska)"
#: bookwyrm/settings.py:296 #: bookwyrm/settings.py:300
msgid "Galego (Galician)" msgid "Galego (Galician)"
msgstr "Galego (Galega)" msgstr "Galego (Galega)"
#: bookwyrm/settings.py:297 #: bookwyrm/settings.py:301
msgid "Italiano (Italian)" msgid "Italiano (Italian)"
msgstr "Italiano (Itala)" msgstr "Italiano (Itala)"
#: bookwyrm/settings.py:298 #: bookwyrm/settings.py:302
msgid "Suomi (Finnish)" msgid "Suomi (Finnish)"
msgstr "Suomi (Finna)" msgstr "Suomi (Finna)"
#: bookwyrm/settings.py:299 #: bookwyrm/settings.py:303
msgid "Français (French)" msgid "Français (French)"
msgstr "Français (Franca)" msgstr "Français (Franca)"
#: bookwyrm/settings.py:300 #: bookwyrm/settings.py:304
msgid "Lietuvių (Lithuanian)" msgid "Lietuvių (Lithuanian)"
msgstr "Lietuvių (Litova)" msgstr "Lietuvių (Litova)"
#: bookwyrm/settings.py:301 #: bookwyrm/settings.py:305
msgid "Norsk (Norwegian)" msgid "Norsk (Norwegian)"
msgstr "Norsk (Norvega)" msgstr "Norsk (Norvega)"
#: bookwyrm/settings.py:302 #: bookwyrm/settings.py:306
msgid "Polski (Polish)" msgid "Polski (Polish)"
msgstr "Polski (Pola)" msgstr "Polski (Pola)"
#: bookwyrm/settings.py:303 #: bookwyrm/settings.py:307
msgid "Português do Brasil (Brazilian Portuguese)" msgid "Português do Brasil (Brazilian Portuguese)"
msgstr "Português do Brasil (Brazila portugala)" msgstr "Português do Brasil (Brazila portugala)"
#: bookwyrm/settings.py:304 #: bookwyrm/settings.py:308
msgid "Português Europeu (European Portuguese)" msgid "Português Europeu (European Portuguese)"
msgstr "Português Europeu (Eŭropa portugala)" msgstr "Português Europeu (Eŭropa portugala)"
#: bookwyrm/settings.py:305 #: bookwyrm/settings.py:309
msgid "Română (Romanian)" msgid "Română (Romanian)"
msgstr "Română (Rumana)" msgstr "Română (Rumana)"
#: bookwyrm/settings.py:306 #: bookwyrm/settings.py:310
msgid "Svenska (Swedish)" msgid "Svenska (Swedish)"
msgstr "Svenska (Sveda)" msgstr "Svenska (Sveda)"
#: bookwyrm/settings.py:307 #: bookwyrm/settings.py:311
msgid "简体中文 (Simplified Chinese)" msgid "简体中文 (Simplified Chinese)"
msgstr "简体中文 (Simpligita ĉina)" msgstr "简体中文 (Simpligita ĉina)"
#: bookwyrm/settings.py:308 #: bookwyrm/settings.py:312
msgid "繁體中文 (Traditional Chinese)" msgid "繁體中文 (Traditional Chinese)"
msgstr "繁體中文 (Tradicia ĉina)" msgstr "繁體中文 (Tradicia ĉina)"
@ -461,7 +465,7 @@ msgstr "<a href=\"%(book_path)s\"><em>%(title)s</em></a> havas la plej diversajn
#: bookwyrm/templates/about/about.html:94 #: 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." 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 "Notu kion vi legas, parolu pri libroj, verku recenzojn kaj malkovru vian sekvan legaĵon. BookWyrm estas programo konstruita por homoj. Ĝi estas ĉiam sen reklamoj, kontraŭkapitalisma kaj celas resti malgranda kaj persona. Se vi havas petojn pri novaj trajtoj, raportojn pri cimoj, aŭ grandajn revojn <a href=\"https://joinbookwyrm.com/get-involved\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">kontaktu nin</a> kaj estu aŭskultata." msgstr "Notu kion vi legas, parolu pri libroj, verku recenzojn kaj malkovru vian sekvan legaĵon. BookWyrm estas programo konstruita por homoj. Ĝi estas ĉiam sen reklamoj, kontraŭkomerca kaj celas resti malgranda kaj persona. Se vi havas petojn pri novaj trajtoj, raportojn pri cimoj, aŭ grandajn revojn <a href=\"https://joinbookwyrm.com/get-involved\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">kontaktu nin</a> kaj estu aŭskultata."
#: bookwyrm/templates/about/about.html:105 #: bookwyrm/templates/about/about.html:105
msgid "Meet your admins" msgid "Meet your admins"
@ -842,7 +846,7 @@ msgstr "ISNI:"
#: bookwyrm/templates/lists/bookmark_button.html:15 #: bookwyrm/templates/lists/bookmark_button.html:15
#: bookwyrm/templates/lists/edit_item_form.html:15 #: bookwyrm/templates/lists/edit_item_form.html:15
#: bookwyrm/templates/lists/form.html:130 #: bookwyrm/templates/lists/form.html:130
#: bookwyrm/templates/preferences/edit_user.html:136 #: bookwyrm/templates/preferences/edit_user.html:140
#: bookwyrm/templates/readthrough/readthrough_modal.html:81 #: bookwyrm/templates/readthrough/readthrough_modal.html:81
#: bookwyrm/templates/settings/announcements/edit_announcement.html:120 #: bookwyrm/templates/settings/announcements/edit_announcement.html:120
#: bookwyrm/templates/settings/federation/edit_instance.html:98 #: bookwyrm/templates/settings/federation/edit_instance.html:98
@ -1307,7 +1311,7 @@ msgstr "Ĉu vi ne trovas la ĝustan eldonon?"
#: bookwyrm/templates/book/editions/editions.html:75 #: bookwyrm/templates/book/editions/editions.html:75
msgid "Add another edition" msgid "Add another edition"
msgstr "Aldoni alian aldonon" msgstr "Aldoni plian eldonon"
#: bookwyrm/templates/book/editions/format_filter.html:9 #: bookwyrm/templates/book/editions/format_filter.html:9
#: bookwyrm/templates/book/editions/language_filter.html:9 #: bookwyrm/templates/book/editions/language_filter.html:9
@ -4038,6 +4042,11 @@ msgstr "Kaŝi la sekvantojn kaj la sekvatojn ĉe via profilo"
msgid "Default post privacy:" msgid "Default post privacy:"
msgstr "Defaŭlta privateco de afiŝoj:" msgstr "Defaŭlta privateco de afiŝoj:"
#: bookwyrm/templates/preferences/edit_user.html:136
#, python-format
msgid "Looking for shelf privacy? You can set a separate visibility level for each of your shelves. Go to <a href=\"%(path)s\">Your Books</a>, pick a shelf from the tab bar, and click \"Edit shelf.\""
msgstr "Ĉu vi volas privatajn bretojn? Vi povas agordi apartan nivelon de videbleco por ĉiu breto. Iru al <a href=\"%(path)s\">Viaj libroj</a>, elektu breton per la langetoj, kaj alklaku «Modifi breton»."
#: bookwyrm/templates/preferences/export.html:4 #: bookwyrm/templates/preferences/export.html:4
#: bookwyrm/templates/preferences/export.html:7 #: bookwyrm/templates/preferences/export.html:7
msgid "CSV Export" msgid "CSV Export"

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: bookwyrm\n" "Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-13 14:54+0000\n" "POT-Creation-Date: 2023-03-29 14:55+0000\n"
"PO-Revision-Date: 2023-03-13 16:38\n" "PO-Revision-Date: 2023-04-07 14:21\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n" "Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"Language: es\n" "Language: es\n"
@ -316,19 +316,19 @@ msgstr "Citas"
msgid "Everything else" msgid "Everything else"
msgstr "Todo lo demás" msgstr "Todo lo demás"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home Timeline" msgid "Home Timeline"
msgstr "Línea de tiempo principal" msgstr "Línea de tiempo principal"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home" msgid "Home"
msgstr "Inicio" msgstr "Inicio"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
msgid "Books Timeline" msgid "Books Timeline"
msgstr "Línea temporal de libros" msgstr "Línea temporal de libros"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
#: bookwyrm/templates/guided_tour/user_profile.html:101 #: bookwyrm/templates/guided_tour/user_profile.html:101
#: bookwyrm/templates/search/layout.html:22 #: bookwyrm/templates/search/layout.html:22
#: bookwyrm/templates/search/layout.html:43 #: bookwyrm/templates/search/layout.html:43
@ -336,75 +336,79 @@ msgstr "Línea temporal de libros"
msgid "Books" msgid "Books"
msgstr "Libros" msgstr "Libros"
#: bookwyrm/settings.py:291 #: bookwyrm/settings.py:294
msgid "English" msgid "English"
msgstr "English (Inglés)" msgstr "English (Inglés)"
#: bookwyrm/settings.py:292 #: bookwyrm/settings.py:295
msgid "Català (Catalan)" msgid "Català (Catalan)"
msgstr "Català (Catalán)" msgstr "Català (Catalán)"
#: bookwyrm/settings.py:293 #: bookwyrm/settings.py:296
msgid "Deutsch (German)" msgid "Deutsch (German)"
msgstr "Deutsch (Alemán)" msgstr "Deutsch (Alemán)"
#: bookwyrm/settings.py:294 #: bookwyrm/settings.py:297
msgid "Esperanto (Esperanto)"
msgstr ""
#: bookwyrm/settings.py:298
msgid "Español (Spanish)" msgid "Español (Spanish)"
msgstr "Español" msgstr "Español"
#: bookwyrm/settings.py:295 #: bookwyrm/settings.py:299
msgid "Euskara (Basque)" msgid "Euskara (Basque)"
msgstr "Euskera" msgstr "Euskera"
#: bookwyrm/settings.py:296 #: bookwyrm/settings.py:300
msgid "Galego (Galician)" msgid "Galego (Galician)"
msgstr "Galego (gallego)" msgstr "Galego (gallego)"
#: bookwyrm/settings.py:297 #: bookwyrm/settings.py:301
msgid "Italiano (Italian)" msgid "Italiano (Italian)"
msgstr "Italiano" msgstr "Italiano"
#: bookwyrm/settings.py:298 #: bookwyrm/settings.py:302
msgid "Suomi (Finnish)" msgid "Suomi (Finnish)"
msgstr "Suomi (finés)" msgstr "Suomi (finés)"
#: bookwyrm/settings.py:299 #: bookwyrm/settings.py:303
msgid "Français (French)" msgid "Français (French)"
msgstr "Français (Francés)" msgstr "Français (Francés)"
#: bookwyrm/settings.py:300 #: bookwyrm/settings.py:304
msgid "Lietuvių (Lithuanian)" msgid "Lietuvių (Lithuanian)"
msgstr "Lietuvių (Lituano)" msgstr "Lietuvių (Lituano)"
#: bookwyrm/settings.py:301 #: bookwyrm/settings.py:305
msgid "Norsk (Norwegian)" msgid "Norsk (Norwegian)"
msgstr "Norsk (noruego)" msgstr "Norsk (noruego)"
#: bookwyrm/settings.py:302 #: bookwyrm/settings.py:306
msgid "Polski (Polish)" msgid "Polski (Polish)"
msgstr "Polski (Polaco)" msgstr "Polski (Polaco)"
#: bookwyrm/settings.py:303 #: bookwyrm/settings.py:307
msgid "Português do Brasil (Brazilian Portuguese)" msgid "Português do Brasil (Brazilian Portuguese)"
msgstr "Português do Brasil (portugués brasileño)" msgstr "Português do Brasil (portugués brasileño)"
#: bookwyrm/settings.py:304 #: bookwyrm/settings.py:308
msgid "Português Europeu (European Portuguese)" msgid "Português Europeu (European Portuguese)"
msgstr "Português Europeu (Portugués europeo)" msgstr "Português Europeu (Portugués europeo)"
#: bookwyrm/settings.py:305 #: bookwyrm/settings.py:309
msgid "Română (Romanian)" msgid "Română (Romanian)"
msgstr "Română (rumano)" msgstr "Română (rumano)"
#: bookwyrm/settings.py:306 #: bookwyrm/settings.py:310
msgid "Svenska (Swedish)" msgid "Svenska (Swedish)"
msgstr "Svenska (Sueco)" msgstr "Svenska (Sueco)"
#: bookwyrm/settings.py:307 #: bookwyrm/settings.py:311
msgid "简体中文 (Simplified Chinese)" msgid "简体中文 (Simplified Chinese)"
msgstr "简体中文 (Chino simplificado)" msgstr "简体中文 (Chino simplificado)"
#: bookwyrm/settings.py:308 #: bookwyrm/settings.py:312
msgid "繁體中文 (Traditional Chinese)" msgid "繁體中文 (Traditional Chinese)"
msgstr "繁體中文 (Chino tradicional)" msgstr "繁體中文 (Chino tradicional)"
@ -842,7 +846,7 @@ msgstr "ISNI:"
#: bookwyrm/templates/lists/bookmark_button.html:15 #: bookwyrm/templates/lists/bookmark_button.html:15
#: bookwyrm/templates/lists/edit_item_form.html:15 #: bookwyrm/templates/lists/edit_item_form.html:15
#: bookwyrm/templates/lists/form.html:130 #: bookwyrm/templates/lists/form.html:130
#: bookwyrm/templates/preferences/edit_user.html:136 #: bookwyrm/templates/preferences/edit_user.html:140
#: bookwyrm/templates/readthrough/readthrough_modal.html:81 #: bookwyrm/templates/readthrough/readthrough_modal.html:81
#: bookwyrm/templates/settings/announcements/edit_announcement.html:120 #: bookwyrm/templates/settings/announcements/edit_announcement.html:120
#: bookwyrm/templates/settings/federation/edit_instance.html:98 #: bookwyrm/templates/settings/federation/edit_instance.html:98
@ -4038,6 +4042,11 @@ msgstr "Ocultar a quién sigo y quién me sigue en el perfil."
msgid "Default post privacy:" msgid "Default post privacy:"
msgstr "Privacidad de publicación por defecto:" msgstr "Privacidad de publicación por defecto:"
#: bookwyrm/templates/preferences/edit_user.html:136
#, python-format
msgid "Looking for shelf privacy? You can set a separate visibility level for each of your shelves. Go to <a href=\"%(path)s\">Your Books</a>, pick a shelf from the tab bar, and click \"Edit shelf.\""
msgstr ""
#: bookwyrm/templates/preferences/export.html:4 #: bookwyrm/templates/preferences/export.html:4
#: bookwyrm/templates/preferences/export.html:7 #: bookwyrm/templates/preferences/export.html:7
msgid "CSV Export" msgid "CSV Export"

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: bookwyrm\n" "Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-13 14:54+0000\n" "POT-Creation-Date: 2023-03-29 14:55+0000\n"
"PO-Revision-Date: 2023-03-23 09:56\n" "PO-Revision-Date: 2023-04-25 12:51\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n" "Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Basque\n" "Language-Team: Basque\n"
"Language: eu\n" "Language: eu\n"
@ -316,19 +316,19 @@ msgstr "Aipuak"
msgid "Everything else" msgid "Everything else"
msgstr "Gainerako guztia" msgstr "Gainerako guztia"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home Timeline" msgid "Home Timeline"
msgstr "Hasierako denbora-lerroa" msgstr "Hasierako denbora-lerroa"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home" msgid "Home"
msgstr "Hasiera" msgstr "Hasiera"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
msgid "Books Timeline" msgid "Books Timeline"
msgstr "Liburuen denbora-lerroa" msgstr "Liburuen denbora-lerroa"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
#: bookwyrm/templates/guided_tour/user_profile.html:101 #: bookwyrm/templates/guided_tour/user_profile.html:101
#: bookwyrm/templates/search/layout.html:22 #: bookwyrm/templates/search/layout.html:22
#: bookwyrm/templates/search/layout.html:43 #: bookwyrm/templates/search/layout.html:43
@ -336,75 +336,79 @@ msgstr "Liburuen denbora-lerroa"
msgid "Books" msgid "Books"
msgstr "Liburuak" msgstr "Liburuak"
#: bookwyrm/settings.py:291 #: bookwyrm/settings.py:294
msgid "English" msgid "English"
msgstr "English (Ingelesa)" msgstr "English (Ingelesa)"
#: bookwyrm/settings.py:292 #: bookwyrm/settings.py:295
msgid "Català (Catalan)" msgid "Català (Catalan)"
msgstr "Català (katalana)" msgstr "Català (katalana)"
#: bookwyrm/settings.py:293 #: bookwyrm/settings.py:296
msgid "Deutsch (German)" msgid "Deutsch (German)"
msgstr "Deutsch (alemana)" msgstr "Deutsch (alemana)"
#: bookwyrm/settings.py:294 #: bookwyrm/settings.py:297
msgid "Esperanto (Esperanto)"
msgstr "Esperantoa"
#: bookwyrm/settings.py:298
msgid "Español (Spanish)" msgid "Español (Spanish)"
msgstr "Español (espainiera)" msgstr "Español (espainiera)"
#: bookwyrm/settings.py:295 #: bookwyrm/settings.py:299
msgid "Euskara (Basque)" msgid "Euskara (Basque)"
msgstr "Euskara" msgstr "Euskara"
#: bookwyrm/settings.py:296 #: bookwyrm/settings.py:300
msgid "Galego (Galician)" msgid "Galego (Galician)"
msgstr "Galego (Galiziera)" msgstr "Galego (Galiziera)"
#: bookwyrm/settings.py:297 #: bookwyrm/settings.py:301
msgid "Italiano (Italian)" msgid "Italiano (Italian)"
msgstr "Italiano (Italiera)" msgstr "Italiano (Italiera)"
#: bookwyrm/settings.py:298 #: bookwyrm/settings.py:302
msgid "Suomi (Finnish)" msgid "Suomi (Finnish)"
msgstr "Suomi (finlandiera)" msgstr "Suomi (finlandiera)"
#: bookwyrm/settings.py:299 #: bookwyrm/settings.py:303
msgid "Français (French)" msgid "Français (French)"
msgstr "Français (frantses)" msgstr "Français (frantses)"
#: bookwyrm/settings.py:300 #: bookwyrm/settings.py:304
msgid "Lietuvių (Lithuanian)" msgid "Lietuvių (Lithuanian)"
msgstr "Lituano (lituaniera)" msgstr "Lituano (lituaniera)"
#: bookwyrm/settings.py:301 #: bookwyrm/settings.py:305
msgid "Norsk (Norwegian)" msgid "Norsk (Norwegian)"
msgstr "Norsk (Norvegiera)" msgstr "Norsk (Norvegiera)"
#: bookwyrm/settings.py:302 #: bookwyrm/settings.py:306
msgid "Polski (Polish)" msgid "Polski (Polish)"
msgstr "Polski (poloniera)" msgstr "Polski (poloniera)"
#: bookwyrm/settings.py:303 #: bookwyrm/settings.py:307
msgid "Português do Brasil (Brazilian Portuguese)" msgid "Português do Brasil (Brazilian Portuguese)"
msgstr "Português do Brasil (Brasilgo Portugesa)" msgstr "Português do Brasil (Brasilgo Portugesa)"
#: bookwyrm/settings.py:304 #: bookwyrm/settings.py:308
msgid "Português Europeu (European Portuguese)" msgid "Português Europeu (European Portuguese)"
msgstr "Português Europeu (Europako Portugesa)" msgstr "Português Europeu (Europako Portugesa)"
#: bookwyrm/settings.py:305 #: bookwyrm/settings.py:309
msgid "Română (Romanian)" msgid "Română (Romanian)"
msgstr "Română (errumaniera)" msgstr "Română (errumaniera)"
#: bookwyrm/settings.py:306 #: bookwyrm/settings.py:310
msgid "Svenska (Swedish)" msgid "Svenska (Swedish)"
msgstr "Svenska (suediera)" msgstr "Svenska (suediera)"
#: bookwyrm/settings.py:307 #: bookwyrm/settings.py:311
msgid "简体中文 (Simplified Chinese)" msgid "简体中文 (Simplified Chinese)"
msgstr "简体中文 (Txinera soildua)" msgstr "简体中文 (Txinera soildua)"
#: bookwyrm/settings.py:308 #: bookwyrm/settings.py:312
msgid "繁體中文 (Traditional Chinese)" msgid "繁體中文 (Traditional Chinese)"
msgstr "繁體中文 (Txinera tradizionala)" msgstr "繁體中文 (Txinera tradizionala)"
@ -842,7 +846,7 @@ msgstr "ISNI:"
#: bookwyrm/templates/lists/bookmark_button.html:15 #: bookwyrm/templates/lists/bookmark_button.html:15
#: bookwyrm/templates/lists/edit_item_form.html:15 #: bookwyrm/templates/lists/edit_item_form.html:15
#: bookwyrm/templates/lists/form.html:130 #: bookwyrm/templates/lists/form.html:130
#: bookwyrm/templates/preferences/edit_user.html:136 #: bookwyrm/templates/preferences/edit_user.html:140
#: bookwyrm/templates/readthrough/readthrough_modal.html:81 #: bookwyrm/templates/readthrough/readthrough_modal.html:81
#: bookwyrm/templates/settings/announcements/edit_announcement.html:120 #: bookwyrm/templates/settings/announcements/edit_announcement.html:120
#: bookwyrm/templates/settings/federation/edit_instance.html:98 #: bookwyrm/templates/settings/federation/edit_instance.html:98
@ -1077,7 +1081,7 @@ msgstr "Gehitu liburua"
#: bookwyrm/templates/book/edit/edit_book.html:43 #: bookwyrm/templates/book/edit/edit_book.html:43
msgid "Failed to save book, see errors below for more information." msgid "Failed to save book, see errors below for more information."
msgstr "" msgstr "Liburua gordetzeak huts egin du, ikus behean agertzen diren erroreak informazio gehiagorako."
#: bookwyrm/templates/book/edit/edit_book.html:70 #: bookwyrm/templates/book/edit/edit_book.html:70
msgid "Confirm Book Info" msgid "Confirm Book Info"
@ -1476,16 +1480,16 @@ msgstr "baloratu du"
#: bookwyrm/templates/book/series.html:11 #: bookwyrm/templates/book/series.html:11
msgid "Series by" msgid "Series by"
msgstr "" msgstr "Seriearen sortzailea: "
#: bookwyrm/templates/book/series.html:27 #: bookwyrm/templates/book/series.html:27
#, python-format #, python-format
msgid "Book %(series_number)s" msgid "Book %(series_number)s"
msgstr "" msgstr "%(series_number)s. liburua"
#: bookwyrm/templates/book/series.html:27 #: bookwyrm/templates/book/series.html:27
msgid "Unsorted Book" msgid "Unsorted Book"
msgstr "" msgstr "Sailkatu gabeko liburua"
#: bookwyrm/templates/book/sync_modal.html:15 #: bookwyrm/templates/book/sync_modal.html:15
#, python-format #, python-format
@ -2176,7 +2180,7 @@ msgstr[1] "%(shared_books)s liburu zure apaletan"
#: bookwyrm/templates/groups/suggested_users.html:43 #: bookwyrm/templates/groups/suggested_users.html:43
#, python-format #, python-format
msgid "No potential members found for \"%(user_query)s\"" msgid "No potential members found for \"%(user_query)s\""
msgstr "Ez da kide potentzialik aurkitu \"%(user_query)s\"-(r)ekin" msgstr "Ez da kide potentzialik aurkitu \"%(user_query)s\"(r)ekin"
#: bookwyrm/templates/groups/user_groups.html:15 #: bookwyrm/templates/groups/user_groups.html:15
msgid "Manager" msgid "Manager"
@ -2699,11 +2703,11 @@ msgstr "Aurkitu liburu bat"
#: bookwyrm/templates/hashtag.html:12 #: bookwyrm/templates/hashtag.html:12
#, python-format #, python-format
msgid "See tagged statuses in the local %(site_name)s community" msgid "See tagged statuses in the local %(site_name)s community"
msgstr "" msgstr "Ikusi etiketatutako egoeran %(site_name)s komunitate lokalean"
#: bookwyrm/templates/hashtag.html:25 #: bookwyrm/templates/hashtag.html:25
msgid "No activities for this hashtag yet!" msgid "No activities for this hashtag yet!"
msgstr "" msgstr "Ez dago aktibitaterik oraindik traola honentzat!"
#: bookwyrm/templates/import/import.html:5 #: bookwyrm/templates/import/import.html:5
#: bookwyrm/templates/import/import.html:9 #: bookwyrm/templates/import/import.html:9
@ -3033,7 +3037,7 @@ msgstr "Eskatu gonbidapen bat"
#: bookwyrm/templates/landing/layout.html:50 #: bookwyrm/templates/landing/layout.html:50
#, python-format #, python-format
msgid "%(name)s registration is closed" msgid "%(name)s registration is closed"
msgstr "%(name)s -(r)en izen-ematea itxita dago" msgstr "%(name)s(e)n izena ematea itxita dago"
#: bookwyrm/templates/landing/layout.html:61 #: bookwyrm/templates/landing/layout.html:61
msgid "Thank you! Your request has been received." msgid "Thank you! Your request has been received."
@ -3456,62 +3460,62 @@ msgstr[1] "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k iradoki du
#: bookwyrm/templates/notifications/items/boost.html:21 #: bookwyrm/templates/notifications/items/boost.html:21
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> boosted your <a href=\"%(related_path)s\">review of <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> boosted your <a href=\"%(related_path)s\">review of <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k zure <a href=\"%(related_path)s\"><em>%(book_title)s</em>(r)en kritika</a> zabaldu du" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k zure <a href=\"%(related_path)s\"><em>%(book_title)s</em>(r)en kritika</a> bultzatu du"
#: bookwyrm/templates/notifications/items/boost.html:27 #: bookwyrm/templates/notifications/items/boost.html:27
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> boosted your <a href=\"%(related_path)s\">review of <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> boosted your <a href=\"%(related_path)s\">review of <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">kritika</a> sustatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">kritika</a> bultzatu dute"
#: bookwyrm/templates/notifications/items/boost.html:36 #: bookwyrm/templates/notifications/items/boost.html:36
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and %(other_user_display_count)s others boosted your <a href=\"%(related_path)s\">review of <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and %(other_user_display_count)s others boosted your <a href=\"%(related_path)s\">review of <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta beste %(other_user_display_count)s erabiltzailek <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">kritika </a> sustatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta beste %(other_user_display_count)s erabiltzailek <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">kritika </a> bultzatu dute"
#: bookwyrm/templates/notifications/items/boost.html:44 #: bookwyrm/templates/notifications/items/boost.html:44
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> boosted your <a href=\"%(related_path)s\">comment on <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> boosted your <a href=\"%(related_path)s\">comment on <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k zure <a href=\"%(related_path)s\"><em>%(book_title)s</em>(r)i buruzko iruzkina</a> zabaldu du" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k zure <a href=\"%(related_path)s\"><em>%(book_title)s</em>(r)i buruzko iruzkina</a> bultzatu du"
#: bookwyrm/templates/notifications/items/boost.html:50 #: bookwyrm/templates/notifications/items/boost.html:50
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> boosted your <a href=\"%(related_path)s\">comment on <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> boosted your <a href=\"%(related_path)s\">comment on <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a> (e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">iruzkina </a> sustatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a> (e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">iruzkina </a> bultzatu dute"
#: bookwyrm/templates/notifications/items/boost.html:59 #: bookwyrm/templates/notifications/items/boost.html:59
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and %(other_user_display_count)s others boosted your <a href=\"%(related_path)s\">comment on <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and %(other_user_display_count)s others boosted your <a href=\"%(related_path)s\">comment on <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta %(other_user_display_count)s erabiltzailek <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">iruzkina </a> sustatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta %(other_user_display_count)s erabiltzailek <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">iruzkina </a> bultzatu dute"
#: bookwyrm/templates/notifications/items/boost.html:67 #: bookwyrm/templates/notifications/items/boost.html:67
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> boosted your <a href=\"%(related_path)s\">quote from <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> boosted your <a href=\"%(related_path)s\">quote from <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k zure <a href=\"%(related_path)s\"><em>%(book_title)s</em>(e)ko aipua</a> zabaldu du" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k zure <a href=\"%(related_path)s\"><em>%(book_title)s</em>(e)ko aipua</a> bultzatu du"
#: bookwyrm/templates/notifications/items/boost.html:73 #: bookwyrm/templates/notifications/items/boost.html:73
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> boosted your <a href=\"%(related_path)s\">quote from <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> boosted your <a href=\"%(related_path)s\">quote from <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">aipamena</a> sustatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">aipamena</a> bultzatu dute"
#: bookwyrm/templates/notifications/items/boost.html:82 #: bookwyrm/templates/notifications/items/boost.html:82
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and %(other_user_display_count)s others boosted your <a href=\"%(related_path)s\">quote from <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and %(other_user_display_count)s others boosted your <a href=\"%(related_path)s\">quote from <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta beste %(other_user_display_count)s erabiltzailek <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">aipamena </a> sustatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta beste %(other_user_display_count)s erabiltzailek <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">aipamena </a> bultzatu dute"
#: bookwyrm/templates/notifications/items/boost.html:90 #: bookwyrm/templates/notifications/items/boost.html:90
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> boosted your <a href=\"%(related_path)s\">status</a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> boosted your <a href=\"%(related_path)s\">status</a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k zure <a href=\"%(related_path)s\">egoera</a> zabaldu du" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k zure <a href=\"%(related_path)s\">egoera</a> bultzatu du"
#: bookwyrm/templates/notifications/items/boost.html:96 #: bookwyrm/templates/notifications/items/boost.html:96
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> boosted your <a href=\"%(related_path)s\">status</a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> boosted your <a href=\"%(related_path)s\">status</a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k zure <a href=\"%(related_path)s\">egoera </a> sustatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k zure <a href=\"%(related_path)s\">egoera </a> bultzatu dute"
#: bookwyrm/templates/notifications/items/boost.html:105 #: bookwyrm/templates/notifications/items/boost.html:105
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and %(other_user_display_count)s others boosted your <a href=\"%(related_path)s\">status</a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and %(other_user_display_count)s others boosted your <a href=\"%(related_path)s\">status</a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta %(other_user_display_count)s erabiltzailek zure <a href=\"%(related_path)s\">egoera</a> sustatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta %(other_user_display_count)s erabiltzailek zure <a href=\"%(related_path)s\">egoera</a> bultzatu dute"
#: bookwyrm/templates/notifications/items/fav.html:21 #: bookwyrm/templates/notifications/items/fav.html:21
#, python-format #, python-format
@ -3521,7 +3525,7 @@ msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k atsegin du zu
#: bookwyrm/templates/notifications/items/fav.html:27 #: bookwyrm/templates/notifications/items/fav.html:27
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> liked your <a href=\"%(related_path)s\">review of <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> liked your <a href=\"%(related_path)s\">review of <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">kritika</a> maitatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">kritika</a> atsegin dute"
#: bookwyrm/templates/notifications/items/fav.html:36 #: bookwyrm/templates/notifications/items/fav.html:36
#, python-format #, python-format
@ -3536,7 +3540,7 @@ msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k atsegin du zu
#: bookwyrm/templates/notifications/items/fav.html:50 #: bookwyrm/templates/notifications/items/fav.html:50
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> liked your <a href=\"%(related_path)s\">comment on <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> liked your <a href=\"%(related_path)s\">comment on <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">iruzkina </a>maitatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">iruzkina </a> atsegin dute"
#: bookwyrm/templates/notifications/items/fav.html:59 #: bookwyrm/templates/notifications/items/fav.html:59
#, python-format #, python-format
@ -3551,7 +3555,7 @@ msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k atsegin du zu
#: bookwyrm/templates/notifications/items/fav.html:73 #: bookwyrm/templates/notifications/items/fav.html:73
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> liked your <a href=\"%(related_path)s\">quote from <em>%(book_title)s</em></a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> liked your <a href=\"%(related_path)s\">quote from <em>%(book_title)s</em></a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">aipamena</a> maitatu zuten" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a>(e)k <em>%(book_title)s</em> liburuari buruzko zure <a href=\"%(related_path)s\">aipamena</a> atsegin dute"
#: bookwyrm/templates/notifications/items/fav.html:82 #: bookwyrm/templates/notifications/items/fav.html:82
#, python-format #, python-format
@ -3566,7 +3570,7 @@ msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k zure <a href=
#: bookwyrm/templates/notifications/items/fav.html:96 #: bookwyrm/templates/notifications/items/fav.html:96
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> liked your <a href=\"%(related_path)s\">status</a>" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> liked your <a href=\"%(related_path)s\">status</a>"
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a> (e)k zure <a href=\"%(related_path)s\">egoera</a> maitatu zuen" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a> (e)k zure <a href=\"%(related_path)s\">egoera</a> atsegin dute"
#: bookwyrm/templates/notifications/items/fav.html:105 #: bookwyrm/templates/notifications/items/fav.html:105
#, python-format #, python-format
@ -3616,7 +3620,7 @@ msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a>(e)k zure \"<a hre
#: bookwyrm/templates/notifications/items/leave.html:26 #: bookwyrm/templates/notifications/items/leave.html:26
#, python-format #, python-format
msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> have left your group \"<a href=\"%(group_path)s\">%(group_name)s</a>\"" msgid "<a href=\"%(related_user_link)s\">%(related_user)s</a> and <a href=\"%(second_user_link)s\">%(second_user)s</a> have left your group \"<a href=\"%(group_path)s\">%(group_name)s</a>\""
msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a> zure \"<a href=\"%(group_path)s\">%(group_name)s</a>\" taldetik atera ziren" msgstr "<a href=\"%(related_user_link)s\">%(related_user)s</a> eta <a href=\"%(second_user_link)s\">%(second_user)s</a> zure \"<a href=\"%(group_path)s\">%(group_name)s</a>\" taldetik atera dira"
#: bookwyrm/templates/notifications/items/leave.html:36 #: bookwyrm/templates/notifications/items/leave.html:36
#, python-format #, python-format
@ -4038,6 +4042,11 @@ msgstr "Ezkutatu jarraitzaile eta jarraituak profilean"
msgid "Default post privacy:" msgid "Default post privacy:"
msgstr "Lehenetsitako pribatutasuna bidalketentzat:" msgstr "Lehenetsitako pribatutasuna bidalketentzat:"
#: bookwyrm/templates/preferences/edit_user.html:136
#, python-format
msgid "Looking for shelf privacy? You can set a separate visibility level for each of your shelves. Go to <a href=\"%(path)s\">Your Books</a>, pick a shelf from the tab bar, and click \"Edit shelf.\""
msgstr "Apalen pribatutasunaren bila zabiltza? Zure apal bakoitzarentzat berariazko ikusgarritasun maila ezarri dezakezu. Zoaz <a href=\"%(path)s\">Zure liburuak</a> atalera, hautatu apal bat fitxa-barran eta klikatu \"Editatu apala\"."
#: bookwyrm/templates/preferences/export.html:4 #: bookwyrm/templates/preferences/export.html:4
#: bookwyrm/templates/preferences/export.html:7 #: bookwyrm/templates/preferences/export.html:7
msgid "CSV Export" msgid "CSV Export"
@ -4483,7 +4492,7 @@ msgstr "Lehentasun handia"
#: bookwyrm/templates/settings/celery.html:50 #: bookwyrm/templates/settings/celery.html:50
msgid "Broadcasts" msgid "Broadcasts"
msgstr "" msgstr "Emanaldiak"
#: bookwyrm/templates/settings/celery.html:60 #: bookwyrm/templates/settings/celery.html:60
msgid "Could not connect to Redis broker" msgid "Could not connect to Redis broker"
@ -5838,7 +5847,7 @@ msgstr "Bultzatu"
#: bookwyrm/templates/snippets/boost_button.html:33 #: bookwyrm/templates/snippets/boost_button.html:33
#: bookwyrm/templates/snippets/boost_button.html:34 #: bookwyrm/templates/snippets/boost_button.html:34
msgid "Un-boost" msgid "Un-boost"
msgstr "Desegin zabaltzea" msgstr "Desegin bultzatzea"
#: bookwyrm/templates/snippets/create_status.html:36 #: bookwyrm/templates/snippets/create_status.html:36
msgid "Quote" msgid "Quote"
@ -6173,7 +6182,7 @@ msgstr "Izena eman"
#: bookwyrm/templates/snippets/report_modal.html:8 #: bookwyrm/templates/snippets/report_modal.html:8
#, python-format #, python-format
msgid "Report @%(username)s's status" msgid "Report @%(username)s's status"
msgstr "Salatu @%(username)s-(r)en egoera" msgstr "Salatu @%(username)s(r)en egoera"
#: bookwyrm/templates/snippets/report_modal.html:10 #: bookwyrm/templates/snippets/report_modal.html:10
#, python-format #, python-format
@ -6249,17 +6258,17 @@ msgstr ""
#: bookwyrm/templates/snippets/status/content_status.html:102 #: bookwyrm/templates/snippets/status/content_status.html:102
#, python-format #, python-format
msgid "%(endpage)s" msgid "%(endpage)s"
msgstr "" msgstr "%(endpage)s"
#: bookwyrm/templates/snippets/status/content_status.html:104 #: bookwyrm/templates/snippets/status/content_status.html:104
#, python-format #, python-format
msgid "(%(percent)s%%" msgid "(%(percent)s%%"
msgstr "" msgstr "(%%%(percent)s"
#: bookwyrm/templates/snippets/status/content_status.html:104 #: bookwyrm/templates/snippets/status/content_status.html:104
#, python-format #, python-format
msgid " - %(endpercent)s%%" msgid " - %(endpercent)s%%"
msgstr "" msgstr " - %%%(endpercent)s"
#: bookwyrm/templates/snippets/status/content_status.html:127 #: bookwyrm/templates/snippets/status/content_status.html:127
msgid "Open image in new window" msgid "Open image in new window"
@ -6362,7 +6371,7 @@ msgstr "Ezabatu egoera"
#: bookwyrm/templates/snippets/status/layout.html:57 #: bookwyrm/templates/snippets/status/layout.html:57
#: bookwyrm/templates/snippets/status/layout.html:58 #: bookwyrm/templates/snippets/status/layout.html:58
msgid "Boost status" msgid "Boost status"
msgstr "Zabaldu egoera" msgstr "Bultzatu egoera"
#: bookwyrm/templates/snippets/status/layout.html:61 #: bookwyrm/templates/snippets/status/layout.html:61
#: bookwyrm/templates/snippets/status/layout.html:62 #: bookwyrm/templates/snippets/status/layout.html:62
@ -6421,7 +6430,7 @@ msgstr "Zure kontua babestu dezakezu zure erabiltzaile-hobespenetan bi faktoreta
#: bookwyrm/templates/user/books_header.html:9 #: bookwyrm/templates/user/books_header.html:9
#, python-format #, python-format
msgid "%(username)s's books" msgid "%(username)s's books"
msgstr "%(username)s-(r)en liburuak" msgstr "%(username)s(r)en liburuak"
#: bookwyrm/templates/user/goal.html:8 #: bookwyrm/templates/user/goal.html:8
#, python-format #, python-format
@ -6445,7 +6454,7 @@ msgstr "Zure %(year)s urteko liburuak"
#: bookwyrm/templates/user/goal.html:42 #: bookwyrm/templates/user/goal.html:42
#, python-format #, python-format
msgid "%(username)s's %(year)s Books" msgid "%(username)s's %(year)s Books"
msgstr "%(username)s-(r)en %(year)s-(e)ko Liburuak" msgstr "%(username)s(r)en %(year)s(e)ko liburuak"
#: bookwyrm/templates/user/groups.html:9 #: bookwyrm/templates/user/groups.html:9
msgid "Your Groups" msgid "Your Groups"
@ -6592,7 +6601,7 @@ msgstr "Liburu zerrenda: %(name)s"
#, python-format #, python-format
msgid "%(num)d book - by %(user)s" msgid "%(num)d book - by %(user)s"
msgid_plural "%(num)d books - by %(user)s" msgid_plural "%(num)d books - by %(user)s"
msgstr[0] "" msgstr[0] "liburu %(num)d - %(user)s"
msgstr[1] "%(num)d liburu - %(user)s" msgstr[1] "%(num)d liburu - %(user)s"
#: bookwyrm/templatetags/utilities.py:39 #: bookwyrm/templatetags/utilities.py:39

Binary file not shown.

View file

@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: bookwyrm\n" "Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-13 14:54+0000\n" "POT-Creation-Date: 2023-03-29 14:55+0000\n"
"PO-Revision-Date: 2023-03-23 14:16\n" "PO-Revision-Date: 2023-04-07 14:21\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n" "Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Finnish\n" "Language-Team: Finnish\n"
"Language: fi\n" "Language: fi\n"
@ -316,19 +316,19 @@ msgstr "Lainaukset"
msgid "Everything else" msgid "Everything else"
msgstr "Muut" msgstr "Muut"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home Timeline" msgid "Home Timeline"
msgstr "Oma aikajana" msgstr "Oma aikajana"
#: bookwyrm/settings.py:218 #: bookwyrm/settings.py:221
msgid "Home" msgid "Home"
msgstr "Etusivu" msgstr "Etusivu"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
msgid "Books Timeline" msgid "Books Timeline"
msgstr "Kirjavirta" msgstr "Kirjavirta"
#: bookwyrm/settings.py:219 #: bookwyrm/settings.py:222
#: bookwyrm/templates/guided_tour/user_profile.html:101 #: bookwyrm/templates/guided_tour/user_profile.html:101
#: bookwyrm/templates/search/layout.html:22 #: bookwyrm/templates/search/layout.html:22
#: bookwyrm/templates/search/layout.html:43 #: bookwyrm/templates/search/layout.html:43
@ -336,75 +336,79 @@ msgstr "Kirjavirta"
msgid "Books" msgid "Books"
msgstr "Kirjat" msgstr "Kirjat"
#: bookwyrm/settings.py:291 #: bookwyrm/settings.py:294
msgid "English" msgid "English"
msgstr "English (englanti)" msgstr "English (englanti)"
#: bookwyrm/settings.py:292 #: bookwyrm/settings.py:295
msgid "Català (Catalan)" msgid "Català (Catalan)"
msgstr "Català (katalaani)" msgstr "Català (katalaani)"
#: bookwyrm/settings.py:293 #: bookwyrm/settings.py:296
msgid "Deutsch (German)" msgid "Deutsch (German)"
msgstr "Deutsch (saksa)" msgstr "Deutsch (saksa)"
#: bookwyrm/settings.py:294 #: bookwyrm/settings.py:297
msgid "Esperanto (Esperanto)"
msgstr ""
#: bookwyrm/settings.py:298
msgid "Español (Spanish)" msgid "Español (Spanish)"
msgstr "Español (espanja)" msgstr "Español (espanja)"
#: bookwyrm/settings.py:295 #: bookwyrm/settings.py:299
msgid "Euskara (Basque)" msgid "Euskara (Basque)"
msgstr "Euskara (baski)" msgstr "Euskara (baski)"
#: bookwyrm/settings.py:296 #: bookwyrm/settings.py:300
msgid "Galego (Galician)" msgid "Galego (Galician)"
msgstr "Galego (galego)" msgstr "Galego (galego)"
#: bookwyrm/settings.py:297 #: bookwyrm/settings.py:301
msgid "Italiano (Italian)" msgid "Italiano (Italian)"
msgstr "Italiano (italia)" msgstr "Italiano (italia)"
#: bookwyrm/settings.py:298 #: bookwyrm/settings.py:302
msgid "Suomi (Finnish)" msgid "Suomi (Finnish)"
msgstr "suomi" msgstr "suomi"
#: bookwyrm/settings.py:299 #: bookwyrm/settings.py:303
msgid "Français (French)" msgid "Français (French)"
msgstr "Français (ranska)" msgstr "Français (ranska)"
#: bookwyrm/settings.py:300 #: bookwyrm/settings.py:304
msgid "Lietuvių (Lithuanian)" msgid "Lietuvių (Lithuanian)"
msgstr "Lietuvių (liettua)" msgstr "Lietuvių (liettua)"
#: bookwyrm/settings.py:301 #: bookwyrm/settings.py:305
msgid "Norsk (Norwegian)" msgid "Norsk (Norwegian)"
msgstr "Norsk (norja)" msgstr "Norsk (norja)"
#: bookwyrm/settings.py:302 #: bookwyrm/settings.py:306
msgid "Polski (Polish)" msgid "Polski (Polish)"
msgstr "Polski (puola)" msgstr "Polski (puola)"
#: bookwyrm/settings.py:303 #: bookwyrm/settings.py:307
msgid "Português do Brasil (Brazilian Portuguese)" msgid "Português do Brasil (Brazilian Portuguese)"
msgstr "Português do Brasil (brasilianportugali)" msgstr "Português do Brasil (brasilianportugali)"
#: bookwyrm/settings.py:304 #: bookwyrm/settings.py:308
msgid "Português Europeu (European Portuguese)" msgid "Português Europeu (European Portuguese)"
msgstr "Português Europeu (portugali)" msgstr "Português Europeu (portugali)"
#: bookwyrm/settings.py:305 #: bookwyrm/settings.py:309
msgid "Română (Romanian)" msgid "Română (Romanian)"
msgstr "Română (romania)" msgstr "Română (romania)"
#: bookwyrm/settings.py:306 #: bookwyrm/settings.py:310
msgid "Svenska (Swedish)" msgid "Svenska (Swedish)"
msgstr "Svenska (ruotsi)" msgstr "Svenska (ruotsi)"
#: bookwyrm/settings.py:307 #: bookwyrm/settings.py:311
msgid "简体中文 (Simplified Chinese)" msgid "简体中文 (Simplified Chinese)"
msgstr "简体中文 (yksinkertaistettu kiina)" msgstr "简体中文 (yksinkertaistettu kiina)"
#: bookwyrm/settings.py:308 #: bookwyrm/settings.py:312
msgid "繁體中文 (Traditional Chinese)" msgid "繁體中文 (Traditional Chinese)"
msgstr "繁體中文 (perinteinen kiina)" msgstr "繁體中文 (perinteinen kiina)"
@ -842,7 +846,7 @@ msgstr "ISNI:"
#: bookwyrm/templates/lists/bookmark_button.html:15 #: bookwyrm/templates/lists/bookmark_button.html:15
#: bookwyrm/templates/lists/edit_item_form.html:15 #: bookwyrm/templates/lists/edit_item_form.html:15
#: bookwyrm/templates/lists/form.html:130 #: bookwyrm/templates/lists/form.html:130
#: bookwyrm/templates/preferences/edit_user.html:136 #: bookwyrm/templates/preferences/edit_user.html:140
#: bookwyrm/templates/readthrough/readthrough_modal.html:81 #: bookwyrm/templates/readthrough/readthrough_modal.html:81
#: bookwyrm/templates/settings/announcements/edit_announcement.html:120 #: bookwyrm/templates/settings/announcements/edit_announcement.html:120
#: bookwyrm/templates/settings/federation/edit_instance.html:98 #: bookwyrm/templates/settings/federation/edit_instance.html:98
@ -4038,6 +4042,11 @@ msgstr "Älä näytä seuraajia ja seurattavia profiilisivulla"
msgid "Default post privacy:" msgid "Default post privacy:"
msgstr "Julkaisujen julkisuuden oletusvalinta:" msgstr "Julkaisujen julkisuuden oletusvalinta:"
#: bookwyrm/templates/preferences/edit_user.html:136
#, python-format
msgid "Looking for shelf privacy? You can set a separate visibility level for each of your shelves. Go to <a href=\"%(path)s\">Your Books</a>, pick a shelf from the tab bar, and click \"Edit shelf.\""
msgstr ""
#: bookwyrm/templates/preferences/export.html:4 #: bookwyrm/templates/preferences/export.html:4
#: bookwyrm/templates/preferences/export.html:7 #: bookwyrm/templates/preferences/export.html:7
msgid "CSV Export" msgid "CSV Export"

Some files were not shown because too many files have changed in this diff Show more