mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-25 19:11:09 +00:00
Merge branch 'main' into gts
This commit is contained in:
commit
a0b7112c9c
74 changed files with 298 additions and 169 deletions
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
@ -514,7 +531,9 @@ 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, ignore_result=True)
|
||||||
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -52,7 +52,7 @@ 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]
|
||||||
|
@ -321,7 +321,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
|
||||||
|
|
|
@ -73,7 +73,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 = []
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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
|
||||||
|
@ -233,7 +233,7 @@ 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)
|
||||||
|
|
|
@ -68,12 +68,12 @@ def dedupe_model(model):
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -581,7 +581,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)
|
||||||
|
|
|
@ -24,7 +24,7 @@ class AnnualGoal(BookWyrmModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""unqiueness constraint"""
|
"""uniqueness constraint"""
|
||||||
|
|
||||||
unique_together = ("user", "year")
|
unique_together = ("user", "year")
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
@ -436,7 +436,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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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 bulma’s named breapoints:
|
* standard bulma’s 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -254,7 +261,9 @@ def rerank_user_task(user_id, update_only=False):
|
||||||
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, ignore_result=True)
|
||||||
|
@ -268,7 +277,9 @@ def remove_suggestion_task(user_id, suggested_user_id):
|
||||||
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, ignore_result=True)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -46,7 +46,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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ 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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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({})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>")
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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("")
|
||||||
|
|
|
@ -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>",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -103,7 +103,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
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
@ -4045,7 +4045,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
|
||||||
|
|
|
@ -29,6 +29,7 @@ opentelemetry-api==1.16.0
|
||||||
opentelemetry-exporter-otlp-proto-grpc==1.16.0
|
opentelemetry-exporter-otlp-proto-grpc==1.16.0
|
||||||
opentelemetry-instrumentation-celery==0.37b0
|
opentelemetry-instrumentation-celery==0.37b0
|
||||||
opentelemetry-instrumentation-django==0.37b0
|
opentelemetry-instrumentation-django==0.37b0
|
||||||
|
opentelemetry-instrumentation-psycopg2==0.37b0
|
||||||
opentelemetry-sdk==1.16.0
|
opentelemetry-sdk==1.16.0
|
||||||
protobuf==3.20.*
|
protobuf==3.20.*
|
||||||
pyotp==2.8.0
|
pyotp==2.8.0
|
||||||
|
|
Loading…
Reference in a new issue