Merge branch 'main' into list-privacy

This commit is contained in:
Mouse Reeve 2022-05-16 08:10:43 -07:00
commit 74368ab159
116 changed files with 15287 additions and 3453 deletions

View file

@ -24,5 +24,5 @@ jobs:
pip install pylint
- name: Analysing the code with pylint
run: |
pylint bookwyrm/ --ignore=migrations,tests --disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801
pylint bookwyrm/ --ignore=migrations --disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801

View file

@ -9,21 +9,18 @@ Social reading and reviewing, decentralized with ActivityPub
- [What it is and isn't](#what-it-is-and-isnt)
- [The role of federation](#the-role-of-federation)
- [Features](#features)
- [Book data](#book-data)
- [Set up Bookwyrm](#set-up-bookwyrm)
- [Set up BookWyrm](#set-up-bookwyrm)
## Joining BookWyrm
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
You can request an invite by entering your email address at https://bookwyrm.social.
If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
## Contributing
See [contributing](https://docs.joinbookwyrm.com/how-to-contribute.html) for code, translation or monetary contributions.
See [contributing](https://docs.joinbookwyrm.com/contributing.html) for code, translation or monetary contributions.
## About BookWyrm
### What it is and isn't
BookWyrm is a platform for social reading! You can use it to track what you're reading, review books, and follow your friends. It isn't primarily meant for cataloguing or as a data-source for books, but it does do both of those things to some degree.
BookWyrm is a platform for social reading. You can use it to track what you're reading, review books, and follow your friends. It isn't primarily meant for cataloguing or as a data-source for books, but it does do both of those things to some degree.
### The role of federation
BookWyrm is built on [ActivityPub](http://activitypub.rocks/). With ActivityPub, it inter-operates with different instances of BookWyrm, and other ActivityPub compliant services, like Mastodon. This means you can run an instance for your book club, and still follow your friend who posts on a server devoted to 20th century Russian speculative fiction. It also means that your friend on mastodon can read and comment on a book review that you post on your BookWyrm instance.
@ -78,8 +75,5 @@ Deployment
- [Nginx](https://nginx.org/en/) HTTP server
## Book data
The application is set up to share book and author data between instances, and get book data from arbitrary outside sources. Right now, the only connector is to OpenLibrary, but other connectors could be written.
## Set up Bookwyrm
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up Bookwyrm in a [developer environment](https://docs.joinbookwyrm.com/developer-environment.html) or [production](https://docs.joinbookwyrm.com/installing-in-production.html).
## Set up BookWyrm
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up BookWyrm in a [developer environment](https://docs.joinbookwyrm.com/install-dev.html) or [production](https://docs.joinbookwyrm.com/install-prod.html).

View file

@ -105,16 +105,6 @@ def init_connectors():
)
def init_federated_servers():
"""big no to nazis"""
built_in_blocks = ["gab.ai", "gab.com"]
for server in built_in_blocks:
models.FederatedServer.objects.create(
server_name=server,
status="blocked",
)
def init_settings():
"""info about the instance"""
models.SiteSettings.objects.create(
@ -163,7 +153,6 @@ class Command(BaseCommand):
"group",
"permission",
"connector",
"federatedserver",
"settings",
"linkdomain",
]
@ -176,8 +165,6 @@ class Command(BaseCommand):
init_permissions()
if not limit or limit == "connector":
init_connectors()
if not limit or limit == "federatedserver":
init_federated_servers()
if not limit or limit == "settings":
init_settings()
if not limit or limit == "linkdomain":

View file

@ -0,0 +1,38 @@
# Generated by Django 3.2.12 on 2022-03-26 16:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0146_auto_20220316_2352"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("de-de", "Deutsch (German)"),
("es-es", "Español (Spanish)"),
("gl-es", "Galego (Galician)"),
("it-it", "Italiano (Italian)"),
("fr-fr", "Français (French)"),
("lt-lt", "Lietuvių (Lithuanian)"),
("no-no", "Norsk (Norwegian)"),
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
("pt-pt", "Português Europeu (European Portuguese)"),
("ro-ro", "Română (Romanian)"),
("sv-se", "Svenska (Swedish)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,39 @@
# Generated by Django 3.2.12 on 2022-03-31 14:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0147_alter_user_preferred_language"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("de-de", "Deutsch (German)"),
("es-es", "Español (Spanish)"),
("gl-es", "Galego (Galician)"),
("it-it", "Italiano (Italian)"),
("fi-fi", "Suomi (Finnish)"),
("fr-fr", "Français (French)"),
("lt-lt", "Lietuvių (Lithuanian)"),
("no-no", "Norsk (Norwegian)"),
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
("pt-pt", "Português Europeu (European Portuguese)"),
("ro-ro", "Română (Romanian)"),
("sv-se", "Svenska (Swedish)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -8,6 +8,7 @@ from django.db.models import Q
from django.dispatch import receiver
from django.http import Http404
from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify
from bookwyrm.settings import DOMAIN
from .fields import RemoteIdField
@ -35,10 +36,11 @@ class BookWyrmModel(models.Model):
remote_id = RemoteIdField(null=True, activitypub_field="id")
def get_remote_id(self):
"""generate a url that resolves to the local object"""
"""generate the url that resolves to the local object, without a slug"""
base_path = f"https://{DOMAIN}"
if hasattr(self, "user"):
base_path = f"{base_path}{self.user.local_path}"
model_name = type(self).__name__.lower()
return f"{base_path}/{model_name}/{self.id}"
@ -49,8 +51,20 @@ class BookWyrmModel(models.Model):
@property
def local_path(self):
"""how to link to this object in the local app"""
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
"""how to link to this object in the local app, with a slug"""
local = self.get_remote_id().replace(f"https://{DOMAIN}", "")
name = None
if hasattr(self, "name_field"):
name = getattr(self, self.name_field)
elif hasattr(self, "name"):
name = self.name
if name:
slug = slugify(name)
local = f"{local}/s/{slug}"
return local
def raise_visible_to_user(self, viewer):
"""is a user authorized to view an object?"""

View file

@ -125,7 +125,7 @@ class ActivitypubFieldMixin:
"""model_field_name to activitypubFieldName"""
if self.activitypub_field:
return self.activitypub_field
name = self.name.split(".")[-1]
name = self.name.rsplit(".", maxsplit=1)[-1]
components = name.split("_")
return components[0] + "".join(x.title() for x in components[1:])
@ -389,7 +389,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
self.alt_field = alt_field
super().__init__(*args, **kwargs)
# pylint: disable=arguments-differ
# pylint: disable=arguments-differ,arguments-renamed
def set_field_from_activity(self, instance, data, save=True, overwrite=True):
"""helper function for assinging a value to the field"""
value = getattr(data, self.get_activitypub_field())

View file

@ -39,15 +39,14 @@ class UserRelationship(BookWyrmModel):
def save(self, *args, **kwargs):
"""clear the template cache"""
# invalidate the template cache
cache.delete_many(
[
f"relationship-{self.user_subject.id}-{self.user_object.id}",
f"relationship-{self.user_object.id}-{self.user_subject.id}",
]
)
clear_cache(self.user_subject, self.user_object)
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
"""clear the template cache"""
clear_cache(self.user_subject, self.user_object)
super().delete(*args, **kwargs)
class Meta:
"""relationships should be unique"""
@ -90,7 +89,9 @@ class UserFollows(ActivityMixin, UserRelationship):
user_object=self.user_subject,
)
).exists():
raise IntegrityError()
raise IntegrityError(
"Attempting to follow blocked user", self.user_subject, self.user_object
)
# don't broadcast this type of relationship -- accepts and requests
# are handled by the UserFollowRequest model
super().save(*args, broadcast=False, **kwargs)
@ -98,11 +99,12 @@ class UserFollows(ActivityMixin, UserRelationship):
@classmethod
def from_request(cls, follow_request):
"""converts a follow request into a follow relationship"""
return cls.objects.create(
obj, _ = cls.objects.get_or_create(
user_subject=follow_request.user_subject,
user_object=follow_request.user_object,
remote_id=follow_request.remote_id,
)
return obj
class UserFollowRequest(ActivitypubMixin, UserRelationship):
@ -133,7 +135,9 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
user_object=self.user_subject,
)
).exists():
raise IntegrityError()
raise IntegrityError(
"Attempting to follow blocked user", self.user_subject, self.user_object
)
super().save(*args, **kwargs)
if broadcast and self.user_subject.local and not self.user_object.local:
@ -174,7 +178,8 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
with transaction.atomic():
UserFollows.from_request(self)
self.delete()
if self.id:
self.delete()
def reject(self):
"""generate a Reject for this follow request"""
@ -207,3 +212,13 @@ class UserBlocks(ActivityMixin, UserRelationship):
Q(user_subject=self.user_subject, user_object=self.user_object)
| Q(user_subject=self.user_object, user_object=self.user_subject)
).delete()
def clear_cache(user_subject, user_object):
"""clear relationship cache"""
cache.delete_many(
[
f"relationship-{user_subject.id}-{user_object.id}",
f"relationship-{user_object.id}-{user_subject.id}",
]
)

View file

@ -6,6 +6,7 @@ from django.db import models
from django.utils import timezone
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel
from . import fields
@ -65,6 +66,11 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
identifier = self.identifier or self.get_identifier()
return f"{base_path}/books/{identifier}"
@property
def local_path(self):
"""No slugs"""
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
def raise_not_deletable(self, viewer):
"""don't let anyone delete a default shelf"""
super().raise_not_deletable(viewer)

View file

@ -284,11 +284,13 @@ LANGUAGES = [
("es-es", _("Español (Spanish)")),
("gl-es", _("Galego (Galician)")),
("it-it", _("Italiano (Italian)")),
("fi-fi", _("Suomi (Finnish)")),
("fr-fr", _("Français (French)")),
("lt-lt", _("Lietuvių (Lithuanian)")),
("no-no", _("Norsk (Norwegian)")),
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
("pt-pt", _("Português Europeu (European Portuguese)")),
("ro-ro", _("Română (Romanian)")),
("sv-se", _("Svenska (Swedish)")),
("zh-hans", _("简体中文 (Simplified Chinese)")),
("zh-hant", _("繁體中文 (Traditional Chinese)")),

View file

@ -36,6 +36,18 @@ body {
flex-direction: column;
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-thumb {
background: $scrollbar-thumb;
border-radius: 0.5em;
}
::-webkit-scrollbar-track {
background: $scrollbar-track;
}
button {
border: none;
margin: 0;

View file

@ -114,3 +114,17 @@ details[open] summary .details-close {
padding-bottom: 0.25rem;
}
}
/** Navbar details
******************************************************************************/
#navbar-dropdown .navbar-item {
color: $text;
font-size: 0.875rem;
padding: 0.375rem 3rem 0.375rem 1rem;
white-space: nowrap;
}
#navbar-dropdown .navbar-item:hover {
background-color: $background-secondary;
}

View file

@ -23,3 +23,8 @@
.has-background-tertiary {
background-color: $background-tertiary !important;
}
/* Workaround for dark theme as .has-text-black doesn't give desired effect. */
.has-text-default {
color: $text !important;
}

View file

@ -28,6 +28,8 @@ $background-body: rgb(24, 27, 28);
$background-secondary: rgb(28, 30, 32);
$background-tertiary: rgb(32, 34, 36);
$modal-background-background-color: rgba($black, 0.8);
$scrollbar-track: $background-secondary;
$scrollbar-thumb: $light;
/* highlight colors */
$primary-highlight: $primary;
@ -51,6 +53,7 @@ $link-hover: $white-bis;
$link-hover-border: #51595d;
$link-focus: $white-bis;
$link-active: $white-bis;
$link-light: #0d1c26;
/* bulma overrides */
$background: $background-secondary;
@ -81,6 +84,13 @@ $progress-value-background-color: $border-light;
$family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif;
.has-text-muted {
color: $grey-lighter !important;
}
.has-text-more-muted {
color: $grey-light !important;
}
@import "../bookwyrm.scss";
@import "../vendor/icons.css";

View file

@ -19,6 +19,8 @@ $scheme-main: $white-bis;
$background-body: $white;
$background-secondary: $white-ter;
$background-tertiary: $white-bis;
$scrollbar-track: $background-secondary;
$scrollbar-thumb: $grey-lighter;
/* highlight colors */
$primary-highlight: $primary-light;
@ -55,5 +57,13 @@ $invisible-overlay-background-color: rgba($scheme-invert, 0.66);
$family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif;
.has-text-muted {
color: $grey-dark !important;
}
.has-text-more-muted {
color: $grey !important;
}
@import "../bookwyrm.scss";
@import "../vendor/icons.css";

View file

@ -99,7 +99,7 @@
<p>
{% url "conduct" as coc_path %}
{% blocktrans trimmed with site_name=site.name %}
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="coc_path">code of conduct</a>, and respond when users report spam and bad behavior.
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="{{ coc_path }}">code of conduct</a>, and respond when users report spam and bad behavior.
{% endblocktrans %}
</p>
</header>

View file

@ -284,7 +284,7 @@
{% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %}
<nav class="tabs">
<ul>
{% url 'book' book.id as tab_url %}
{% url 'book' book.id book.name|slugify as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a>
</li>

View file

@ -21,7 +21,7 @@
<div class="column my-3-mobile ml-3-tablet mr-auto">
<h2 class="title is-5 mb-1">
<a href="{{ book.local_path }}" class="has-text-black">
<a href="{{ book.local_path }}" class="has-text-default">
{{ book|book_title }}
</a>
</h2>

View file

@ -15,7 +15,7 @@
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'book' book.id %}">{{ book|book_title }}</a></li>
<li><a href="{% url 'book' book.id book.name|slugify %}">{{ book|book_title }}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{% trans "Edit links" %}

View file

@ -43,7 +43,7 @@
{% endif %}
<p>
<a href="https://joinbookwyrm.com/">
{% trans "Join Bookwyrm" %}
{% trans "Join BookWyrm" %}
</a>
</p>
</footer>

View file

@ -38,7 +38,7 @@
{% for membership in group.memberships.all %}
{% with member=membership.user %}
<div class="box has-text-centered is-shadowless has-background-tertiary my-2 mx-2 member_{{ member.id }}">
<a href="{{ member.local_path }}" class="has-text-black">
<a href="{{ member.local_path }}" class="has-text-default">
{% include 'snippets/avatar.html' with user=member large=True %}
<span title="{{ member.display_name }}" class="is-block is-6 has-text-weight-bold">{{ member.display_name|truncatechars:10 }}</span>
<span title="@{{ member|username }}" class="is-block pb-3">@{{ member|username|truncatechars:8 }}</span>

View file

@ -9,7 +9,7 @@
<div class="column is-flex is-flex-grow-0">
{% for user in suggested_users %}
<div class="box has-text-centered is-shadowless has-background-tertiary m-2">
<a href="{{ user.local_path }}" class="has-text-black">
<a href="{{ user.local_path }}" class="has-text-default">
{% include 'snippets/avatar.html' with user=user large=True %}
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>

View file

@ -90,64 +90,8 @@
<div class="navbar-end">
{% if request.user.is_authenticated %}
<div class="navbar-item mt-3 py-0 has-dropdown is-hoverable">
<a
href="{{ request.user.local_path }}"
class="navbar-link pulldown-menu"
role="button"
aria-expanded="false"
tabindex="0"
aria-haspopup="true"
aria-controls="navbar-dropdown"
>
{% include 'snippets/avatar.html' with user=request.user %}
<span class="ml-2">{{ request.user.display_name }}</span>
</a>
<ul class="navbar-dropdown" id="navbar_dropdown">
<li>
<a href="{% url 'directory' %}" class="navbar-item">
{% trans "Directory" %}
</a>
</li>
<li>
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
{% trans 'Your Books' %}
</a>
</li>
<li>
<a href="{% url 'direct-messages' %}" class="navbar-item">
{% trans "Direct Messages" %}
</a>
</li>
<li>
<a href="{% url 'prefs-profile' %}" class="navbar-item">
{% trans 'Settings' %}
</a>
</li>
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
<li class="navbar-divider" role="presentation">&nbsp;</li>
{% endif %}
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
<li>
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
{% trans 'Invites' %}
</a>
</li>
{% endif %}
{% if perms.bookwyrm.moderate_user %}
<li>
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
{% trans 'Admin' %}
</a>
</li>
{% endif %}
<li class="navbar-divider" role="presentation">&nbsp;</li>
<li>
<a href="{% url 'logout' %}" class="navbar-item">
{% trans 'Log out' %}
</a>
</li>
</ul>
<div class="navbar-item mt-3 py-0">
{% include 'user_menu.html' %}
</div>
<div class="navbar-item mt-3 py-0">
<a href="{% url 'notifications' %}" class="tags has-addons">

View file

@ -6,7 +6,7 @@
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li>
<li><a href="{% url 'list' list.id %}">{{ list.name|truncatechars:30 }}</a></li>
<li><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{{ list.name|truncatechars:30 }}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{% trans "Curate" %}

View file

@ -180,7 +180,7 @@
<h2 class="title is-5">
{% trans "Sort List" %}
</h2>
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
<form name="sort" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block">
<div class="field">
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
<div class="select is-fullwidth">
@ -207,7 +207,7 @@
{% trans "Suggest Books" %}
{% endif %}
</h2>
<form name="search" action="{% url 'list' list.id %}" method="GET" class="block">
<form name="search" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block">
<div class="field has-addons">
<div class="control">
<input aria-label="{% trans 'Search for a book' %}" class="input" type="text" name="q" placeholder="{% trans 'Search for a book' %}" value="{{ query }}">
@ -221,7 +221,7 @@
</div>
</div>
{% if query %}
<p class="help"><a href="{% url 'list' list.id %}">{% trans "Clear search" %}</a></p>
<p class="help"><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{% trans "Clear search" %}</a></p>
{% endif %}
</form>
{% if not suggested_books %}

View file

@ -47,12 +47,12 @@
{% block preview %}
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-grey-dark{% endif %}">
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-muted{% endif %}">
<div class="columns">
<div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %}
</div>
<div class="column is-narrow has-grey-dark">
<div class="column is-narrow has-text-muted">
{{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %}
</div>

View file

@ -47,12 +47,12 @@
{% block preview %}
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-grey-dark{% endif %}">
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-muted{% endif %}">
<div class="columns">
<div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %}
</div>
<div class="column is-narrow has-grey-dark">
<div class="column is-narrow has-text-muted">
{{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %}
</div>

View file

@ -1,7 +1,7 @@
{% load notification_page_tags %}
{% related_status notification as related_status %}
<div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-grey{% endif %}">
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-more-muted{% endif %}">
<div class="column is-narrow is-size-3">
<a class="icon" href="{% block primary_link %}{% endblock %}">
{% block icon %}{% endblock %}

View file

@ -48,12 +48,12 @@
{% block preview %}
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-black{% endif %}">
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-default{% endif %}">
<div class="columns">
<div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %}
</div>
<div class="column is-narrow has-text-black">
<div class="column is-narrow has-text-default">
{{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %}
</div>

View file

@ -51,12 +51,12 @@
{% block preview %}
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-black{% endif %}">
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-default{% endif %}">
<div class="columns">
<div class="column is-clipped">
{% include 'snippets/status_preview.html' with status=related_status %}
</div>
<div class="column is-narrow has-text-black">
<div class="column is-narrow has-text-default">
{{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %}
</div>

View file

@ -0,0 +1,22 @@
{% extends 'preferences/layout.html' %}
{% load i18n %}
{% block title %}{% trans "CSV Export" %}{% endblock %}
{% block header %}
{% trans "CSV Export" %}
{% endblock %}
{% block panel %}
<div class="block content">
<p class="notification">
{% trans "Your export will include all the books on your shelves, books you have reviewed, and books with reading activity." %}
</p>
<p>
<a href="{% url 'prefs-export-file' %}" class="button">
<span class="icon icon-download" aria-hidden="true"></span>
<span>Download file</span>
</a>
</p>
</div>
{% endblock %}

View file

@ -24,6 +24,17 @@
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Delete Account" %}</a>
</li>
</ul>
<h2 class="menu-label">{% trans "Data" %}</h2>
<ul class="menu-list">
<li>
{% url 'import' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Import" %}</a>
</li>
<li>
{% url 'prefs-export' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "CSV export" %}</a>
</li>
</ul>
<h2 class="menu-label">{% trans "Relationships" %}</h2>
<ul class="menu-list">
<li>

View file

@ -0,0 +1,7 @@
{% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %}
{% include 'settings/federation/software_filter.html' %}
{% endblock %}

View file

@ -12,6 +12,9 @@
{% endblock %}
{% block panel %}
{% include 'settings/federation/instance_filters.html' %}
<div class="tabs">
<ul>
{% url 'settings-federation' status='federated' as url %}
@ -36,6 +39,10 @@
{% trans "Date added" as text %}
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
</th>
<th>
{% trans "Last updated" as text %}
{% include 'snippets/table-sort-header.html' with field="updated_date" sort=sort text=text %}
</th>
<th>
{% trans "Software" as text %}
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
@ -43,12 +50,12 @@
<th>
{% trans "Users" %}
</th>
<th>{% trans "Status" %}</th>
</tr>
{% for server in servers %}
<tr>
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
<td>{{ server.created_date }}</td>
<td>{{ server.created_date|date:'Y-m-d' }}</td>
<td>{{ server.updated_date|date:'Y-m-d' }}</td>
<td>
{% if server.application_type %}
{{ server.application_type }}
@ -56,7 +63,6 @@
{% endif %}
</td>
<td>{{ server.user_set.count }}</td>
<td>{{ server.get_status_display }}</td>
</tr>
{% endfor %}
{% if not servers %}

View file

@ -0,0 +1,19 @@
{% extends 'snippets/filters_panel/filter_field.html' %}
{% load i18n %}
{% block filter %}
<label class="label" for="id_server">{% trans "Software" %}</label>
<div class="control">
<div class="select">
<select name="application_type">
<option value="">-----</option>
{% for option in software_options %}
{% if option %}
<option value="{{ option }}">{{ option }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
{% endblock %}

View file

@ -14,6 +14,11 @@
{% blocktrans with username=goal.user.display_name read_count=progress.count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}{{ username }} has read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
{% endif %}
</p>
<progress class="progress is-large" value="{{ progress.count }}" max="{{ goal.goal }}" aria-hidden="true">{{ progress.percent }}%</progress>
<progress
class="progress is-large is-primary"
value="{{ progress.count }}"
max="{{ goal.goal }}"
aria-hidden="true"
>{{ progress.percent }}%</progress>
{% endwith %}

View file

@ -37,7 +37,7 @@
{% endwith %}
{% endif %}
<article class="column ml-3-tablet my-3-mobile">
<article class="column ml-3-tablet my-3-mobile is-clipped">
{% if status_type == 'Review' %}
<header class="mb-2">
<h3

View file

@ -32,7 +32,7 @@
<div class="card-footer-item">
{% trans "Reply" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light is-transparent toggle-button" focus="id_content_reply" %}
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light toggle-button" focus="id_content_reply" %}
</div>
<div class="card-footer-item">
{% include 'snippets/boost_button.html' with status=status %}
@ -42,7 +42,7 @@
</div>
{% if not moderation_mode %}
<div class="card-footer-item">
{% include 'snippets/status/status_options.html' with class="is-small is-light is-transparent" right=True %}
{% include 'snippets/status/status_options.html' with class="is-small is-light" right=True %}
</div>
{% endif %}

View file

@ -5,7 +5,7 @@
{% for user in suggested_users %}
<div class="column is-flex is-flex-grow-0">
<div class="box has-text-centered is-shadowless has-background-tertiary m-0">
<a href="{{ user.local_path }}" class="has-text-black">
<a href="{{ user.local_path }}" class="has-text-default">
{% include 'snippets/avatar.html' with user=user large=True %}
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>

View file

@ -0,0 +1,77 @@
{% load utilities %}
{% load i18n %}
<details class="dropdown" id="navbar-dropdown">
<summary
class="is-relative pulldown-menu dropdown-trigger"
aria-label="{% trans 'View profile and more' %}"
role="button"
aria-haspopup="menu"
>
<span class="">
{% include 'snippets/avatar.html' with user=request.user %}
<span class="ml-2">{{ request.user.display_name }}</span>
</span>
<span class="icon icon-arrow-down is-hidden-mobile" aria-hidden="true"></span>
</summary>
<div class="dropdown-menu">
<ul
class="dropdown-content"
role="menu"
>
<li role="menuitem">
<a href="{% url 'user-feed' request.user.localname %}" class="navbar-item">
{% trans "Profile" %}
</a>
</li>
<li role="menuitem">
<a href="{% url 'directory' %}" class="navbar-item">
{% trans "Directory" %}
</a>
</li>
<li role="menuitem">
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
{% trans 'Your Books' %}
</a>
</li>
<li role="menuitem">
<a href="{% url 'direct-messages' %}" class="navbar-item">
{% trans "Direct Messages" %}
</a>
</li>
<li role="menuitem">
<a href="{% url 'prefs-profile' %}" class="navbar-item">
{% trans 'Settings' %}
</a>
</li>
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
<li class="navbar-divider" role="presentation" aria-hidden="true">&nbsp;</li>
{% endif %}
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
<li role="menuitem">
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
{% trans 'Invites' %}
</a>
</li>
{% endif %}
{% if perms.bookwyrm.moderate_user %}
<li role="menuitem">
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
{% trans 'Admin' %}
</a>
</li>
{% endif %}
<li class="navbar-divider" role="presentation" aria-hidden="true">&nbsp;</li>
<li role="menuitem">
<a href="{% url 'logout' %}" class="navbar-item">
{% trans 'Log out' %}
</a>
</li>
</ul>
</div>
</details>

View file

@ -1 +1,2 @@
from . import *
""" import ALL the tests """
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -4,7 +4,10 @@ from bookwyrm import models
class Author(TestCase):
"""serialize author tests"""
def setUp(self):
"""initial data"""
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
@ -16,6 +19,7 @@ class Author(TestCase):
)
def test_serialize_model(self):
"""check presense of author fields"""
activity = self.author.to_activity()
self.assertEqual(activity["id"], self.author.remote_id)
self.assertIsInstance(activity["aliases"], list)

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -101,6 +101,7 @@ class AbstractConnector(TestCase):
result = self.connector.get_or_create_book(
f"https://{DOMAIN}/book/{self.book.id}"
)
self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual(result, self.book)

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -90,7 +90,6 @@ class InitDB(TestCase):
self.assertEqual(Group.objects.count(), 3)
self.assertTrue(Permission.objects.exists())
self.assertEqual(models.Connector.objects.count(), 3)
self.assertEqual(models.FederatedServer.objects.count(), 2)
self.assertEqual(models.SiteSettings.objects.count(), 1)
self.assertEqual(models.LinkDomain.objects.count(), 5)
@ -102,7 +101,6 @@ class InitDB(TestCase):
# everything should have been called
self.assertEqual(Group.objects.count(), 3)
self.assertEqual(models.Connector.objects.count(), 0)
self.assertEqual(models.FederatedServer.objects.count(), 0)
self.assertEqual(models.SiteSettings.objects.count(), 0)
self.assertEqual(models.LinkDomain.objects.count(), 0)

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -38,7 +38,7 @@ class BaseModel(TestCase):
def test_remote_id(self):
"""these should be generated"""
self.test_model.id = 1
self.test_model.id = 1 # pylint: disable=invalid-name
expected = self.test_model.get_remote_id()
self.assertEqual(expected, f"https://{DOMAIN}/bookwyrmtestmodel/1")

View file

@ -162,6 +162,7 @@ class ModelFields(TestCase):
class TestActivity(ActivityObject):
"""real simple mock"""
# pylint: disable=invalid-name
to: List[str]
cc: List[str]
id: str = "http://hi.com"

View file

@ -17,7 +17,7 @@ class User(TestCase):
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
self.user = models.User.objects.create_user(
"mouse@%s" % DOMAIN,
f"mouse@{DOMAIN}",
"mouse@mouse.mouse",
"mouseword",
local=True,
@ -107,7 +107,7 @@ class User(TestCase):
def test_get_or_create_remote_server(self):
responses.add(
responses.GET,
"https://%s/.well-known/nodeinfo" % DOMAIN,
f"https://{DOMAIN}/.well-known/nodeinfo",
json={"links": [{"href": "http://www.example.com"}, {}]},
)
responses.add(
@ -124,7 +124,7 @@ class User(TestCase):
@responses.activate
def test_get_or_create_remote_server_no_wellknown(self):
responses.add(
responses.GET, "https://%s/.well-known/nodeinfo" % DOMAIN, status=404
responses.GET, f"https://{DOMAIN}/.well-known/nodeinfo", status=404
)
server = models.user.get_or_create_remote_server(DOMAIN)
@ -136,7 +136,7 @@ class User(TestCase):
def test_get_or_create_remote_server_no_links(self):
responses.add(
responses.GET,
"https://%s/.well-known/nodeinfo" % DOMAIN,
f"https://{DOMAIN}/.well-known/nodeinfo",
json={"links": [{"href": "http://www.example.com"}, {}]},
)
responses.add(responses.GET, "http://www.example.com", status=404)
@ -150,7 +150,7 @@ class User(TestCase):
def test_get_or_create_remote_server_unknown_format(self):
responses.add(
responses.GET,
"https://%s/.well-known/nodeinfo" % DOMAIN,
f"https://{DOMAIN}/.well-known/nodeinfo",
json={"links": [{"href": "http://www.example.com"}, {}]},
)
responses.add(responses.GET, "http://www.example.com", json={"fish": "salmon"})

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -64,8 +64,8 @@ class Signature(TestCase):
def send(self, signature, now, data, digest):
"""test request"""
c = Client()
return c.post(
client = Client()
return client.post(
urlsplit(self.rat.inbox).path,
data=data,
content_type="application/json",

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -61,7 +61,7 @@ class InboxActivities(TestCase):
self.assertEqual(models.Notification.objects.count(), 0)
activity = {
"type": "Announce",
"id": "%s/boost" % self.status.remote_id,
"id": f"{self.status.remote_id}/boost",
"actor": self.remote_user.remote_id,
"object": self.status.remote_id,
"to": ["https://www.w3.org/ns/activitystreams#public"],
@ -94,7 +94,7 @@ class InboxActivities(TestCase):
self.assertEqual(models.Notification.objects.count(), 0)
activity = {
"type": "Announce",
"id": "%s/boost" % self.status.remote_id,
"id": f"{self.status.remote_id}/boost",
"actor": self.remote_user.remote_id,
"object": "https://remote.com/status/1",
"to": ["https://www.w3.org/ns/activitystreams#public"],

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -1 +1,2 @@
from . import *
# pylint: disable=missing-module-docstring
from . import * # pylint: disable=import-self

View file

@ -66,7 +66,7 @@ class AuthorViews(TestCase):
def test_author_page_edition_author(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Author.as_view()
another_book = models.Edition.objects.create(
models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=self.work,

View file

@ -0,0 +1,69 @@
""" test for app action functionality """
from unittest.mock import patch
from django.http import StreamingHttpResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
@patch("bookwyrm.activitystreams.add_status_task.delay")
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
class ExportViews(TestCase):
"""viewing and creating statuses"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
remote_id="https://example.com/users/mouse",
)
self.work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Test Book",
remote_id="https://example.com/book/1",
parent_work=self.work,
isbn_13="9781234567890",
bnf_id="beep",
)
def tst_export_get(self, *_):
"""request export"""
request = self.factory.get("")
request.user = self.local_user
result = views.Export.as_view()(request)
validate_html(result.render())
def test_export_file(self, *_):
"""simple export"""
models.ShelfBook.objects.create(
shelf=self.local_user.shelf_set.first(),
user=self.local_user,
book=self.book,
)
request = self.factory.get("")
request.user = self.local_user
export = views.export_user_book_data(request)
self.assertIsInstance(export, StreamingHttpResponse)
self.assertEqual(export.status_code, 200)
result = list(export.streaming_content)
# pylint: disable=line-too-long
self.assertEqual(
result[0],
b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,isbn_10,isbn_13,oclc_number,rating,review_name,review_cw,review_content\r\n",
)
expected = f"Test Book,,{self.book.remote_id},,,,,beep,,,,123456789X,9781234567890,,,,,\r\n"
self.assertEqual(result[1].decode("utf-8"), expected)

View file

@ -139,7 +139,7 @@ class ViewsHelpers(TestCase):
}
responses.add(
responses.GET,
"https://example.com/.well-known/webfinger?resource=acct:%s" % username,
f"https://example.com/.well-known/webfinger?resource=acct:{username}",
json=wellknown,
status=200,
)

View file

@ -83,7 +83,7 @@ class UserViews(TestCase):
def test_user_page_domain(self):
"""when the user domain has dashes in it"""
with patch("bookwyrm.models.user.set_remote_server"):
self.remote_user = models.User.objects.create_user(
models.User.objects.create_user(
"nutria",
"",
"nutriaword",

View file

@ -391,6 +391,9 @@ urlpatterns = [
re_path(
r"^group/(?P<group_id>\d+)(.json)?/?$", views.Group.as_view(), name="group"
),
re_path(
rf"^group/(?P<group_id>\d+){regex.SLUG}/?$", views.Group.as_view(), name="group"
),
re_path(
r"^group/delete/(?P<group_id>\d+)/?$", views.delete_group, name="delete-group"
),
@ -417,7 +420,10 @@ urlpatterns = [
re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"),
re_path(r"^list/?$", views.Lists.as_view(), name="lists"),
re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"),
re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"),
re_path(r"^list/(?P<list_id>\d+)(\.json)?/?$", views.List.as_view(), name="list"),
re_path(
rf"^list/(?P<list_id>\d+){regex.SLUG}/?$", views.List.as_view(), name="list"
),
re_path(
r"^list/(?P<list_id>\d+)/item/(?P<list_item>\d+)/?$",
views.ListItem.as_view(),
@ -475,12 +481,19 @@ urlpatterns = [
views.ChangePassword.as_view(),
name="prefs-password",
),
re_path(r"^preferences/export/?$", views.Export.as_view(), name="prefs-export"),
re_path(
r"^preferences/export/file/?$",
views.export_user_book_data,
name="prefs-export-file",
),
re_path(r"^preferences/delete/?$", views.DeleteUser.as_view(), name="prefs-delete"),
re_path(r"^preferences/block/?$", views.Block.as_view(), name="prefs-block"),
re_path(r"^block/(?P<user_id>\d+)/?$", views.Block.as_view()),
re_path(r"^unblock/(?P<user_id>\d+)/?$", views.unblock),
# statuses
re_path(rf"{STATUS_PATH}(.json)?/?$", views.Status.as_view(), name="status"),
re_path(rf"{STATUS_PATH}{regex.SLUG}/?$", views.Status.as_view(), name="status"),
re_path(rf"{STATUS_PATH}/activity/?$", views.Status.as_view(), name="status"),
re_path(
rf"{STATUS_PATH}/replies(.json)?/?$", views.Replies.as_view(), name="replies"
@ -517,6 +530,7 @@ urlpatterns = [
re_path(r"^unboost/(?P<status_id>\d+)/?$", views.Unboost.as_view()),
# books
re_path(rf"{BOOK_PATH}(.json)?/?$", views.Book.as_view(), name="book"),
re_path(rf"{BOOK_PATH}{regex.SLUG}/?$", views.Book.as_view(), name="book"),
re_path(
rf"{BOOK_PATH}/(?P<user_statuses>review|comment|quote)/?$",
views.Book.as_view(),
@ -574,6 +588,11 @@ urlpatterns = [
re_path(
r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view(), name="author"
),
re_path(
rf"^author/(?P<author_id>\d+){regex.SLUG}/?$",
views.Author.as_view(),
name="author",
),
re_path(
r"^author/(?P<author_id>\d+)/edit/?$",
views.EditAuthor.as_view(),

View file

@ -6,5 +6,6 @@ STRICT_LOCALNAME = r"@[a-zA-Z_\-\.0-9]+"
USERNAME = rf"{LOCALNAME}(@{DOMAIN})?"
STRICT_USERNAME = rf"\B{STRICT_LOCALNAME}(@{DOMAIN})?\b"
FULL_USERNAME = rf"{LOCALNAME}@{DOMAIN}\b"
SLUG = r"/s/(?P<slug>[-_a-z0-9]*)"
# should match (BookWyrm/1.0.0; or (BookWyrm/99.1.2;
BOOKWYRM_USER_AGENT = r"\(BookWyrm/[0-9]+\.[0-9]+\.[0-9]+;"

View file

@ -28,6 +28,7 @@ from .admin.user_admin import UserAdmin, UserAdminList
# user preferences
from .preferences.change_password import ChangePassword
from .preferences.edit_user import EditUser
from .preferences.export import Export, export_user_book_data
from .preferences.delete_user import DeleteUser
from .preferences.block import Block, unblock

View file

@ -103,7 +103,7 @@ class Dashboard(View):
status="pending"
).count(),
"invite_requests": models.InviteRequest.objects.filter(
ignored=False, invite_sent=False
ignored=False, invite__isnull=True
).count(),
"user_stats": user_chart.get_chart(start, end, interval),
"status_stats": status_chart.get_chart(start, end, interval),

View file

@ -25,14 +25,23 @@ class Federation(View):
def get(self, request, status="federated"):
"""list of servers"""
servers = models.FederatedServer.objects.filter(status=status)
filters = {}
if software := request.GET.get("application_type"):
filters["application_type"] = software
servers = models.FederatedServer.objects.filter(status=status, **filters)
sort = request.GET.get("sort")
sort_fields = ["created_date", "application_type", "server_name"]
# pylint: disable=consider-using-f-string
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
sort_fields = [
"created_date",
"updated_date",
"application_type",
"server_name",
]
if not sort in sort_fields + [f"-{f}" for f in sort_fields]:
sort = "-created_date"
servers = servers.order_by(sort)
servers = servers.order_by(sort, "-created_date")
paginated = Paginator(servers, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
@ -49,6 +58,9 @@ class Federation(View):
page.number, on_each_side=2, on_ends=1
),
"sort": sort,
"software_options": models.FederatedServer.objects.values_list(
"application_type", flat=True
).distinct(),
"form": forms.ServerForm(),
}
return TemplateResponse(request, "settings/federation/instance_list.html", data)

View file

@ -11,20 +11,24 @@ from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
# pylint: disable= no-self-use
class Author(View):
"""this person wrote a book"""
def get(self, request, author_id):
# pylint: disable=unused-argument
def get(self, request, author_id, slug=None):
"""landing page for an author"""
author = get_object_or_404(models.Author, id=author_id)
if is_api_request(request):
return ActivitypubResponse(author.to_activity())
if redirect_local_path := maybe_redirect_local_path(request, author):
return redirect_local_path
books = (
models.Work.objects.filter(editions__authors=author)
.order_by("created_date")

View file

@ -15,14 +15,14 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager, ConnectorException
from bookwyrm.connectors.abstract_connector import get_image
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
# pylint: disable=no-self-use
class Book(View):
"""a book! this is the stuff"""
def get(self, request, book_id, user_statuses=False, update_error=False):
def get(self, request, book_id, **kwargs):
"""info about a book"""
if is_api_request(request):
book = get_object_or_404(
@ -30,7 +30,11 @@ class Book(View):
)
return ActivitypubResponse(book.to_activity())
user_statuses = user_statuses if request.user.is_authenticated else False
user_statuses = (
kwargs.get("user_statuses", False)
if request.user.is_authenticated
else False
)
# it's safe to use this OR because edition and work and subclasses of the same
# table, so they never have clashing IDs
@ -46,6 +50,11 @@ class Book(View):
if not book or not book.parent_work:
raise Http404()
if redirect_local_path := not user_statuses and maybe_redirect_local_path(
request, book
):
return redirect_local_path
# all reviews for all editions of the book
reviews = models.Review.privacy_filter(request.user).filter(
book__parent_work__editions=book
@ -80,7 +89,7 @@ class Book(View):
else None,
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
"lists": lists,
"update_error": update_error,
"update_error": kwargs.get("update_error", False),
}
if request.user.is_authenticated:

View file

@ -15,7 +15,7 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH, STREAMS
from bookwyrm.suggested_users import suggested_users
from .helpers import filter_stream_by_status_type, get_user_from_username
from .helpers import is_api_request, is_bookwyrm_request
from .helpers import is_api_request, is_bookwyrm_request, maybe_redirect_local_path
from .annual_summary import get_annual_summary_year
@ -113,7 +113,8 @@ class DirectMessage(View):
class Status(View):
"""get posting"""
def get(self, request, username, status_id):
# pylint: disable=unused-argument
def get(self, request, username, status_id, slug=None):
"""display a particular status (and replies, etc)"""
user = get_user_from_username(request.user, username)
status = get_object_or_404(
@ -130,6 +131,9 @@ class Status(View):
status.to_activity(pure=not is_bookwyrm_request(request))
)
if redirect_local_path := maybe_redirect_local_path(request, status):
return redirect_local_path
visible_thread = (
models.Status.privacy_filter(request.user)
.filter(thread_id=status.thread_id)

View file

@ -2,12 +2,12 @@
import urllib.parse
import re
from django.contrib.auth.decorators import login_required
from django.db import IntegrityError
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.views.decorators.http import require_POST
from bookwyrm import models
from bookwyrm.models.relationship import clear_cache
from .helpers import (
get_user_from_username,
handle_remote_webfinger,
@ -22,17 +22,17 @@ def follow(request):
"""follow another user, here or abroad"""
username = request.POST["user"]
to_follow = get_user_from_username(request.user, username)
clear_cache(request.user, to_follow)
try:
models.UserFollowRequest.objects.create(
user_subject=request.user,
user_object=to_follow,
)
except IntegrityError:
pass
follow_request, created = models.UserFollowRequest.objects.get_or_create(
user_subject=request.user,
user_object=to_follow,
)
if request.GET.get("next"):
return redirect(request.GET.get("next", "/"))
if not created:
# this request probably failed to connect with the remote
# that means we should save to trigger a re-broadcast
follow_request.save()
return redirect(to_follow.local_path)
@ -49,14 +49,14 @@ def unfollow(request):
user_subject=request.user, user_object=to_unfollow
).delete()
except models.UserFollows.DoesNotExist:
pass
clear_cache(request.user, to_unfollow)
try:
models.UserFollowRequest.objects.get(
user_subject=request.user, user_object=to_unfollow
).delete()
except models.UserFollowRequest.DoesNotExist:
pass
clear_cache(request.user, to_unfollow)
# this is handled with ajax so it shouldn't really matter
return redirect(request.headers.get("Referer", "/"))

View file

@ -14,17 +14,22 @@ from django.db.models.functions import Greatest
from bookwyrm import forms, models
from bookwyrm.suggested_users import suggested_users
from .helpers import get_user_from_username
from .helpers import get_user_from_username, maybe_redirect_local_path
# pylint: disable=no-self-use
class Group(View):
"""group page"""
def get(self, request, group_id):
# pylint: disable=unused-argument
def get(self, request, group_id, slug=None):
"""display a group"""
group = get_object_or_404(models.Group, id=group_id)
group.raise_visible_to_user(request.user)
if redirect_local_path := maybe_redirect_local_path(request, group):
return redirect_local_path
lists = (
models.List.privacy_filter(request.user)
.filter(group=group)
@ -80,7 +85,8 @@ class Group(View):
class UserGroups(View):
"""a user's groups page"""
def get(self, request, username):
# pylint: disable=unused-argument
def get(self, request, username, slug=None):
"""display a group"""
user = get_user_from_username(request.user, username)
groups = (

View file

@ -8,6 +8,7 @@ from dateutil.parser import ParserError
from requests import HTTPError
from django.db.models import Q
from django.conf import settings as django_settings
from django.shortcuts import redirect
from django.http import Http404
from django.utils import translation
@ -201,3 +202,21 @@ def filter_stream_by_status_type(activities, allowed_types=None):
)
return activities
def maybe_redirect_local_path(request, model):
"""
if the request had an invalid path, return a permanent redirect response to the
correct one, including a slug if any.
if path is valid, returns False.
"""
# don't redirect empty path for unit tests which currently have this
if request.path in ("/", model.local_path):
return False
new_path = model.local_path
if len(request.GET) > 0:
new_path = f"{model.local_path}?{request.GET.urlencode()}"
return redirect(new_path, permanent=True)

View file

@ -91,9 +91,12 @@ class ConfirmEmailCode(View):
def get(self, request, code): # pylint: disable=unused-argument
"""you got the code! good work"""
settings = models.SiteSettings.get()
if request.user.is_authenticated or not settings.require_confirm_email:
if request.user.is_authenticated:
return redirect("/")
if not settings.require_confirm_email:
return redirect("login")
# look up the user associated with this code
try:
user = models.User.objects.get(confirmation_code=code)

View file

@ -18,21 +18,27 @@ from django.views.decorators.http import require_POST
from bookwyrm import book_search, forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.views.helpers import is_api_request
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
# pylint: disable=no-self-use
class List(View):
"""book list page"""
def get(self, request, list_id, add_failed=False, add_succeeded=False):
def get(self, request, list_id, **kwargs):
"""display a book list"""
add_failed = kwargs.get("add_failed", False)
add_succeeded = kwargs.get("add_succeeded", False)
book_list = get_object_or_404(models.List, id=list_id)
book_list.raise_visible_to_user(request.user)
if is_api_request(request):
return ActivitypubResponse(book_list.to_activity(**request.GET))
if r := maybe_redirect_local_path(request, book_list):
return r
query = request.GET.get("q")
suggestions = None

View file

@ -0,0 +1,97 @@
""" Let users export their book data """
import csv
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.http import StreamingHttpResponse
from django.template.response import TemplateResponse
from django.views import View
from django.utils.decorators import method_decorator
from django.views.decorators.http import require_GET
from bookwyrm import models
# pylint: disable=no-self-use
@method_decorator(login_required, name="dispatch")
class Export(View):
"""Let users export data"""
def get(self, request):
"""Request csv file"""
return TemplateResponse(request, "preferences/export.html")
@login_required
@require_GET
def export_user_book_data(request):
"""Streaming the csv file of a user's book data"""
data = (
models.Edition.viewer_aware_objects(request.user)
.filter(
Q(shelves__user=request.user)
| Q(readthrough__user=request.user)
| Q(review__user=request.user)
| Q(comment__user=request.user)
| Q(quotation__user=request.user)
)
.distinct()
)
generator = csv_row_generator(data, request.user)
pseudo_buffer = Echo()
writer = csv.writer(pseudo_buffer)
# for testing, if you want to see the results in the browser:
# from django.http import JsonResponse
# return JsonResponse(list(generator), safe=False)
return StreamingHttpResponse(
(writer.writerow(row) for row in generator),
content_type="text/csv",
headers={"Content-Disposition": 'attachment; filename="bookwyrm-export.csv"'},
)
def csv_row_generator(books, user):
"""generate a csv entry for the user's book"""
deduplication_fields = [
f.name
for f in models.Edition._meta.get_fields() # pylint: disable=protected-access
if getattr(f, "deduplication_field", False)
]
fields = (
["title", "author_text"]
+ deduplication_fields
+ ["rating", "review_name", "review_cw", "review_content"]
)
yield fields
for book in books:
# I think this is more efficient than doing a subquery in the view? but idk
review_rating = (
models.Review.objects.filter(user=user, book=book, rating__isnull=False)
.order_by("-published_date")
.first()
)
book.rating = review_rating.rating if review_rating else None
review = (
models.Review.objects.filter(user=user, book=book, content__isnull=False)
.order_by("-published_date")
.first()
)
if review:
book.review_name = review.name
book.review_cw = review.content_warning
book.review_content = review.raw_content
yield [getattr(book, field, "") or "" for field in fields]
class Echo:
"""An object that implements just the write method of the file-like
interface. (https://docs.djangoproject.com/en/3.2/howto/outputting-csv/)
"""
# pylint: disable=no-self-use
def write(self, value):
"""Write the value by returning it, instead of storing in a buffer."""
return value

View file

@ -24,6 +24,8 @@ class Search(View):
def get(self, request):
"""that search bar up top"""
query = request.GET.get("q")
# check if query is isbn
query = isbn_check(query)
min_confidence = request.GET.get("min_confidence", 0)
search_type = request.GET.get("type")
search_remote = (
@ -123,3 +125,35 @@ def list_search(query, viewer, *_):
)
.order_by("-similarity")
), None
def isbn_check(query):
"""isbn10 or isbn13 check, if so remove separators"""
if query:
su_num = re.sub(r"(?<=\d)\D(?=\d|[xX])", "", query)
if len(su_num) == 13 and su_num.isdecimal():
# Multiply every other digit by 3
# Add these numbers and the other digits
product = sum(int(ch) for ch in su_num[::2]) + sum(
int(ch) * 3 for ch in su_num[1::2]
)
if product % 10 == 0:
return su_num
elif (
len(su_num) == 10
and su_num[:-1].isdecimal()
and (su_num[-1].isdecimal() or su_num[-1].lower() == "x")
):
product = 0
# Iterate through code_string
for i in range(9):
# for each character, multiply by a different decreasing number: 10 - x
product = product + int(su_num[i]) * (10 - i)
# Handle last character
if su_num[9].lower() == "x":
product += 10
else:
product += int(su_num[9])
if product % 11 == 0:
return su_num
return query

2
bw-dev
View file

@ -116,6 +116,7 @@ case "$CMD" in
git fetch origin l10n_main:l10n_main
git checkout l10n_main locale/de_DE
git checkout l10n_main locale/es_ES
git checkout l10n_main locale/fi_FI
git checkout l10n_main locale/fr_FR
git checkout l10n_main locale/gl_ES
git checkout l10n_main locale/it_IT
@ -123,6 +124,7 @@ case "$CMD" in
git checkout l10n_main locale/no_NO
git checkout l10n_main locale/pt_PT
git checkout l10n_main locale/pt_BR
git checkout l10n_main locale/ro_RO
git checkout l10n_main locale/sv_SE
git checkout l10n_main locale/zh_Hans
git checkout l10n_main locale/zh_Hant

View file

@ -1,7 +1,4 @@
#/usr/bin/env bash
# for zsh, run:
# autoload bashcompinit
# bashcompinit
complete -W "up
service_ports_web
initdb

36
complete_bwdev.zsh Normal file
View file

@ -0,0 +1,36 @@
#/usr/bin/env bash
autoload bashcompinit
bashcompinit
complete -W "up
service_ports_web
initdb
resetdb
makemigrations
migrate
bash
shell
dbshell
restart_celery
pytest
collectstatic
makemessages
compilemessages
update_locales
build
clean
black
prettier
stylelint
formatters
compilescss
collectstatic_watch
populate_streams
populate_lists_streams
populate_suggestions
generate_thumbnails
generate_preview_images
copy_media_to_s3
set_cors_to_s3
setup
admin_code
runweb" -o bashdefault -o default bw-dev

View file

@ -3,6 +3,7 @@ version: '3'
services:
nginx:
image: nginx:latest
restart: unless-stopped
ports:
- 1333:80
depends_on:

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-17 16:15+0000\n"
"POT-Creation-Date: 2022-05-14 14:03+0000\n"
"PO-Revision-Date: 2021-02-28 17:19-0800\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: English <LL@li.org>\n"
@ -166,14 +166,14 @@ msgstr ""
#: bookwyrm/models/federated_server.py:11
#: bookwyrm/templates/settings/federation/edit_instance.html:55
#: bookwyrm/templates/settings/federation/instance_list.html:19
#: bookwyrm/templates/settings/federation/instance_list.html:22
msgid "Federated"
msgstr ""
#: bookwyrm/models/federated_server.py:12 bookwyrm/models/link.py:71
#: bookwyrm/templates/settings/federation/edit_instance.html:56
#: bookwyrm/templates/settings/federation/instance.html:10
#: bookwyrm/templates/settings/federation/instance_list.html:23
#: bookwyrm/templates/settings/federation/instance_list.html:26
#: bookwyrm/templates/settings/link_domains/link_domains.html:27
msgid "Blocked"
msgstr ""
@ -188,7 +188,7 @@ msgstr ""
msgid "%(value)s is not a valid username"
msgstr ""
#: bookwyrm/models/fields.py:181 bookwyrm/templates/layout.html:179
#: bookwyrm/models/fields.py:181 bookwyrm/templates/layout.html:123
#: bookwyrm/templates/ostatus/error.html:29
msgid "username"
msgstr ""
@ -301,34 +301,42 @@ msgid "Italiano (Italian)"
msgstr ""
#: bookwyrm/settings.py:287
msgid "Français (French)"
msgid "Suomi (Finnish)"
msgstr ""
#: bookwyrm/settings.py:288
msgid "Lietuvių (Lithuanian)"
msgid "Français (French)"
msgstr ""
#: bookwyrm/settings.py:289
msgid "Norsk (Norwegian)"
msgid "Lietuvių (Lithuanian)"
msgstr ""
#: bookwyrm/settings.py:290
msgid "Português do Brasil (Brazilian Portuguese)"
msgid "Norsk (Norwegian)"
msgstr ""
#: bookwyrm/settings.py:291
msgid "Português Europeu (European Portuguese)"
msgid "Português do Brasil (Brazilian Portuguese)"
msgstr ""
#: bookwyrm/settings.py:292
msgid "Svenska (Swedish)"
msgid "Português Europeu (European Portuguese)"
msgstr ""
#: bookwyrm/settings.py:293
msgid "简体中文 (Simplified Chinese)"
msgid "Română (Romanian)"
msgstr ""
#: bookwyrm/settings.py:294
msgid "Svenska (Swedish)"
msgstr ""
#: bookwyrm/settings.py:295
msgid "简体中文 (Simplified Chinese)"
msgstr ""
#: bookwyrm/settings.py:296
msgid "繁體中文 (Traditional Chinese)"
msgstr ""
@ -393,14 +401,14 @@ msgstr ""
#: bookwyrm/templates/about/about.html:101
#, python-format
msgid "%(site_name)s's moderators and administrators keep the site up and running, enforce the <a href=\"coc_path\">code of conduct</a>, and respond when users report spam and bad behavior."
msgid "%(site_name)s's moderators and administrators keep the site up and running, enforce the <a href=\"%(coc_path)s\">code of conduct</a>, and respond when users report spam and bad behavior."
msgstr ""
#: bookwyrm/templates/about/about.html:115
msgid "Moderator"
msgstr ""
#: bookwyrm/templates/about/about.html:117 bookwyrm/templates/layout.html:140
#: bookwyrm/templates/about/about.html:117 bookwyrm/templates/user_menu.html:63
msgid "Admin"
msgstr ""
@ -431,7 +439,7 @@ msgid "Software version:"
msgstr ""
#: bookwyrm/templates/about/layout.html:30
#: bookwyrm/templates/embed-layout.html:34 bookwyrm/templates/layout.html:238
#: bookwyrm/templates/embed-layout.html:34 bookwyrm/templates/layout.html:182
#, python-format
msgid "About %(site_name)s"
msgstr ""
@ -709,7 +717,7 @@ msgid "Openlibrary key:"
msgstr ""
#: bookwyrm/templates/author/edit_author.html:84
#: bookwyrm/templates/book/edit/edit_book_form.html:326
#: bookwyrm/templates/book/edit/edit_book_form.html:323
msgid "Inventaire ID:"
msgstr ""
@ -735,7 +743,7 @@ msgstr ""
#: bookwyrm/templates/lists/edit_item_form.html:15
#: bookwyrm/templates/lists/form.html:130
#: bookwyrm/templates/preferences/edit_user.html:136
#: bookwyrm/templates/readthrough/readthrough_modal.html:74
#: bookwyrm/templates/readthrough/readthrough_modal.html:81
#: bookwyrm/templates/settings/announcements/edit_announcement.html:120
#: bookwyrm/templates/settings/federation/edit_instance.html:98
#: bookwyrm/templates/settings/federation/instance.html:105
@ -759,7 +767,7 @@ msgstr ""
#: bookwyrm/templates/lists/add_item_modal.html:36
#: bookwyrm/templates/lists/delete_list_modal.html:16
#: bookwyrm/templates/readthrough/delete_readthrough_modal.html:27
#: bookwyrm/templates/readthrough/readthrough_modal.html:73
#: bookwyrm/templates/readthrough/readthrough_modal.html:80
#: bookwyrm/templates/search/barcode_modal.html:45
#: bookwyrm/templates/settings/federation/instance.html:106
#: bookwyrm/templates/settings/link_domains/edit_domain_modal.html:22
@ -885,7 +893,7 @@ msgstr ""
#: bookwyrm/templates/lists/add_item_modal.html:39
#: bookwyrm/templates/lists/list.html:255
#: bookwyrm/templates/settings/email_blocklist/domain_form.html:24
#: bookwyrm/templates/settings/ip_blocklist/ip_address_form.html:31
#: bookwyrm/templates/settings/ip_blocklist/ip_address_form.html:32
msgid "Add"
msgstr ""
@ -894,12 +902,12 @@ msgid "ISBN:"
msgstr ""
#: bookwyrm/templates/book/book_identifiers.html:15
#: bookwyrm/templates/book/edit/edit_book_form.html:335
#: bookwyrm/templates/book/edit/edit_book_form.html:332
msgid "OCLC Number:"
msgstr ""
#: bookwyrm/templates/book/book_identifiers.html:22
#: bookwyrm/templates/book/edit/edit_book_form.html:344
#: bookwyrm/templates/book/edit/edit_book_form.html:341
msgid "ASIN:"
msgstr ""
@ -908,12 +916,12 @@ msgid "Add cover"
msgstr ""
#: bookwyrm/templates/book/cover_add_modal.html:17
#: bookwyrm/templates/book/edit/edit_book_form.html:234
#: bookwyrm/templates/book/edit/edit_book_form.html:233
msgid "Upload cover:"
msgstr ""
#: bookwyrm/templates/book/cover_add_modal.html:23
#: bookwyrm/templates/book/edit/edit_book_form.html:240
#: bookwyrm/templates/book/edit/edit_book_form.html:239
msgid "Load cover from url:"
msgstr ""
@ -925,8 +933,7 @@ msgstr ""
#: bookwyrm/templates/components/inline_form.html:8
#: bookwyrm/templates/components/modal.html:13
#: bookwyrm/templates/components/modal.html:30
#: bookwyrm/templates/components/tooltip.html:7
#: bookwyrm/templates/feed/suggested_books.html:55
#: bookwyrm/templates/feed/suggested_books.html:67
#: bookwyrm/templates/get_started/layout.html:25
#: bookwyrm/templates/get_started/layout.html:58
msgid "Close"
@ -1032,77 +1039,77 @@ msgstr ""
msgid "First published date:"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:165
#: bookwyrm/templates/book/edit/edit_book_form.html:164
msgid "Published date:"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:176
#: bookwyrm/templates/book/edit/edit_book_form.html:175
msgid "Authors"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:187
#: bookwyrm/templates/book/edit/edit_book_form.html:186
#, python-format
msgid "Remove %(name)s"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:190
#: bookwyrm/templates/book/edit/edit_book_form.html:189
#, python-format
msgid "Author page for %(name)s"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:198
#: bookwyrm/templates/book/edit/edit_book_form.html:197
msgid "Add Authors:"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:200
#: bookwyrm/templates/book/edit/edit_book_form.html:203
msgid "Add Author"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:201
#: bookwyrm/templates/book/edit/edit_book_form.html:204
msgid "Add Author"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:202
#: bookwyrm/templates/book/edit/edit_book_form.html:205
msgid "Jane Doe"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:211
#: bookwyrm/templates/book/edit/edit_book_form.html:210
msgid "Add Another Author"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:221
#: bookwyrm/templates/book/edit/edit_book_form.html:220
#: bookwyrm/templates/shelf/shelf.html:146
msgid "Cover"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:253
#: bookwyrm/templates/book/edit/edit_book_form.html:252
msgid "Physical Properties"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:260
#: bookwyrm/templates/book/edit/edit_book_form.html:259
#: bookwyrm/templates/book/editions/format_filter.html:6
msgid "Format:"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:272
#: bookwyrm/templates/book/edit/edit_book_form.html:269
msgid "Format details:"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:283
#: bookwyrm/templates/book/edit/edit_book_form.html:280
msgid "Pages:"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:294
#: bookwyrm/templates/book/edit/edit_book_form.html:291
msgid "Book Identifiers"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:299
#: bookwyrm/templates/book/edit/edit_book_form.html:296
msgid "ISBN 13:"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:308
#: bookwyrm/templates/book/edit/edit_book_form.html:305
msgid "ISBN 10:"
msgstr ""
#: bookwyrm/templates/book/edit/edit_book_form.html:317
#: bookwyrm/templates/book/edit/edit_book_form.html:314
msgid "Openlibrary ID:"
msgstr ""
@ -1193,11 +1200,10 @@ msgstr ""
#: bookwyrm/templates/book/file_links/edit_links.html:36
#: bookwyrm/templates/import/import_status.html:127
#: bookwyrm/templates/settings/announcements/announcements.html:37
#: bookwyrm/templates/settings/federation/instance_list.html:46
#: bookwyrm/templates/settings/invites/manage_invite_requests.html:47
#: bookwyrm/templates/settings/invites/status_filter.html:5
#: bookwyrm/templates/settings/users/user_admin.html:52
#: bookwyrm/templates/settings/users/user_info.html:20
#: bookwyrm/templates/settings/users/user_info.html:24
msgid "Status"
msgstr ""
@ -1288,10 +1294,6 @@ msgstr ""
msgid "Loading data will connect to <strong>%(source_name)s</strong> and check for any metadata about this book which aren't present here. Existing metadata will not be overwritten."
msgstr ""
#: bookwyrm/templates/components/tooltip.html:3
msgid "Help"
msgstr ""
#: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8
msgid "Edit status"
msgstr ""
@ -1313,7 +1315,7 @@ msgid "Sorry! We couldn't find that code."
msgstr ""
#: bookwyrm/templates/confirm_email/confirm_email.html:19
#: bookwyrm/templates/settings/users/user_info.html:85
#: bookwyrm/templates/settings/users/user_info.html:92
msgid "Confirmation code:"
msgstr ""
@ -1324,15 +1326,16 @@ msgstr ""
msgid "Submit"
msgstr ""
#: bookwyrm/templates/confirm_email/confirm_email.html:32
#: bookwyrm/templates/confirm_email/confirm_email.html:38
msgid "Can't find your code?"
msgstr ""
#: bookwyrm/templates/confirm_email/resend_form.html:4
#: bookwyrm/templates/confirm_email/resend.html:5
#: bookwyrm/templates/confirm_email/resend_modal.html:5
msgid "Resend confirmation link"
msgstr ""
#: bookwyrm/templates/confirm_email/resend_form.html:11
#: bookwyrm/templates/confirm_email/resend_modal.html:15
#: bookwyrm/templates/landing/layout.html:68
#: bookwyrm/templates/landing/password_reset_request.html:18
#: bookwyrm/templates/preferences/edit_user.html:53
@ -1340,7 +1343,11 @@ msgstr ""
msgid "Email address:"
msgstr ""
#: bookwyrm/templates/confirm_email/resend_form.html:17
#: bookwyrm/templates/confirm_email/resend_modal.html:28
msgid "No user matching this email address found."
msgstr ""
#: bookwyrm/templates/confirm_email/resend_modal.html:38
msgid "Resend link"
msgstr ""
@ -1360,7 +1367,7 @@ msgstr ""
#: bookwyrm/templates/directory/directory.html:4
#: bookwyrm/templates/directory/directory.html:9
#: bookwyrm/templates/layout.html:109
#: bookwyrm/templates/user_menu.html:30
msgid "Directory"
msgstr ""
@ -1603,12 +1610,12 @@ msgstr ""
msgid "%(site_name)s home page"
msgstr ""
#: bookwyrm/templates/embed-layout.html:40 bookwyrm/templates/layout.html:242
#: bookwyrm/templates/embed-layout.html:40 bookwyrm/templates/layout.html:186
msgid "Contact site admin"
msgstr ""
#: bookwyrm/templates/embed-layout.html:46
msgid "Join Bookwyrm"
msgid "Join BookWyrm"
msgstr ""
#: bookwyrm/templates/feed/direct_messages.html:8
@ -1617,7 +1624,7 @@ msgid "Direct Messages with <a href=\"%(path)s\">%(username)s</a>"
msgstr ""
#: bookwyrm/templates/feed/direct_messages.html:10
#: bookwyrm/templates/layout.html:119
#: bookwyrm/templates/user_menu.html:40
msgid "Direct Messages"
msgstr ""
@ -1654,14 +1661,22 @@ msgid "Updates"
msgstr ""
#: bookwyrm/templates/feed/suggested_books.html:6
#: bookwyrm/templates/layout.html:114
#: bookwyrm/templates/user_menu.html:35
msgid "Your Books"
msgstr ""
#: bookwyrm/templates/feed/suggested_books.html:8
#: bookwyrm/templates/feed/suggested_books.html:10
msgid "There are no books here right now! Try searching for a book to get started"
msgstr ""
#: bookwyrm/templates/feed/suggested_books.html:13
msgid "Do you have book data from another service like GoodReads?"
msgstr ""
#: bookwyrm/templates/feed/suggested_books.html:16
msgid "Import your reading history"
msgstr ""
#: bookwyrm/templates/feed/suggested_users.html:5
#: bookwyrm/templates/get_started/users.html:6
msgid "Who to follow"
@ -1956,28 +1971,33 @@ msgstr ""
msgid "Data source:"
msgstr ""
#: bookwyrm/templates/import/import.html:40
#: bookwyrm/templates/import/import.html:39
msgid "You can download your Goodreads data from the <a href=\"https://www.goodreads.com/review/import\" target=\"_blank\" rel=\"noopener noreferrer\">Import/Export page</a> of your Goodreads account."
msgstr ""
#: bookwyrm/templates/import/import.html:44
msgid "Data file:"
msgstr ""
#: bookwyrm/templates/import/import.html:48
#: bookwyrm/templates/import/import.html:52
msgid "Include reviews"
msgstr ""
#: bookwyrm/templates/import/import.html:53
#: bookwyrm/templates/import/import.html:57
msgid "Privacy setting for imported reviews:"
msgstr ""
#: bookwyrm/templates/import/import.html:59
#: bookwyrm/templates/import/import.html:63
#: bookwyrm/templates/preferences/layout.html:31
#: bookwyrm/templates/settings/federation/instance_blocklist.html:76
msgid "Import"
msgstr ""
#: bookwyrm/templates/import/import.html:64
#: bookwyrm/templates/import/import.html:68
msgid "Recent Imports"
msgstr ""
#: bookwyrm/templates/import/import.html:66
#: bookwyrm/templates/import/import.html:70
msgid "No recent imports"
msgstr ""
@ -2117,10 +2137,6 @@ msgstr ""
msgid "Reject"
msgstr ""
#: bookwyrm/templates/import/tooltip.html:6
msgid "You can download your Goodreads data from the <a href=\"https://www.goodreads.com/review/import\" target=\"_blank\" rel=\"noopener noreferrer\">Import/Export page</a> of your Goodreads account."
msgstr ""
#: bookwyrm/templates/import/troubleshoot.html:7
msgid "Failed items"
msgstr ""
@ -2206,7 +2222,7 @@ msgid "Login"
msgstr ""
#: bookwyrm/templates/landing/login.html:7
#: bookwyrm/templates/landing/login.html:36 bookwyrm/templates/layout.html:187
#: bookwyrm/templates/landing/login.html:36 bookwyrm/templates/layout.html:131
#: bookwyrm/templates/ostatus/error.html:37
msgid "Log in"
msgstr ""
@ -2215,7 +2231,7 @@ msgstr ""
msgid "Success! Email address confirmed."
msgstr ""
#: bookwyrm/templates/landing/login.html:21 bookwyrm/templates/layout.html:178
#: bookwyrm/templates/landing/login.html:21 bookwyrm/templates/layout.html:122
#: bookwyrm/templates/ostatus/error.html:28
#: bookwyrm/templates/snippets/register_form.html:4
msgid "Username:"
@ -2223,12 +2239,12 @@ msgstr ""
#: bookwyrm/templates/landing/login.html:27
#: bookwyrm/templates/landing/password_reset.html:26
#: bookwyrm/templates/layout.html:182 bookwyrm/templates/ostatus/error.html:32
#: bookwyrm/templates/layout.html:126 bookwyrm/templates/ostatus/error.html:32
#: bookwyrm/templates/snippets/register_form.html:45
msgid "Password:"
msgstr ""
#: bookwyrm/templates/landing/login.html:39 bookwyrm/templates/layout.html:184
#: bookwyrm/templates/landing/login.html:39 bookwyrm/templates/layout.html:128
#: bookwyrm/templates/ostatus/error.html:34
msgid "Forgot your password?"
msgstr ""
@ -2272,54 +2288,38 @@ msgstr ""
msgid "Feed"
msgstr ""
#: bookwyrm/templates/layout.html:124 bookwyrm/templates/setup/config.html:52
msgid "Settings"
msgstr ""
#: bookwyrm/templates/layout.html:133
#: bookwyrm/templates/settings/invites/manage_invite_requests.html:15
#: bookwyrm/templates/settings/invites/manage_invites.html:3
#: bookwyrm/templates/settings/invites/manage_invites.html:15
#: bookwyrm/templates/settings/layout.html:42
msgid "Invites"
msgstr ""
#: bookwyrm/templates/layout.html:147
msgid "Log out"
msgstr ""
#: bookwyrm/templates/layout.html:155 bookwyrm/templates/layout.html:156
#: bookwyrm/templates/layout.html:99 bookwyrm/templates/layout.html:100
#: bookwyrm/templates/notifications/notifications_page.html:5
#: bookwyrm/templates/notifications/notifications_page.html:10
msgid "Notifications"
msgstr ""
#: bookwyrm/templates/layout.html:183 bookwyrm/templates/ostatus/error.html:33
#: bookwyrm/templates/layout.html:127 bookwyrm/templates/ostatus/error.html:33
msgid "password"
msgstr ""
#: bookwyrm/templates/layout.html:195
#: bookwyrm/templates/layout.html:139
msgid "Join"
msgstr ""
#: bookwyrm/templates/layout.html:229
#: bookwyrm/templates/layout.html:173
msgid "Successfully posted status"
msgstr ""
#: bookwyrm/templates/layout.html:230
#: bookwyrm/templates/layout.html:174
msgid "Error posting status"
msgstr ""
#: bookwyrm/templates/layout.html:246
#: bookwyrm/templates/layout.html:190
msgid "Documentation"
msgstr ""
#: bookwyrm/templates/layout.html:253
#: bookwyrm/templates/layout.html:197
#, python-format
msgid "Support %(site_name)s on <a href=\"%(support_link)s\" target=\"_blank\">%(support_title)s</a>"
msgstr ""
#: bookwyrm/templates/layout.html:257
#: bookwyrm/templates/layout.html:201
msgid "BookWyrm's source code is freely available. You can contribute or report issues on <a href=\"https://github.com/mouse-reeve/bookwyrm\">GitHub</a>."
msgstr ""
@ -2856,7 +2856,7 @@ msgstr ""
#: bookwyrm/templates/preferences/blocks.html:4
#: bookwyrm/templates/preferences/blocks.html:7
#: bookwyrm/templates/preferences/layout.html:31
#: bookwyrm/templates/preferences/layout.html:42
msgid "Blocked Users"
msgstr ""
@ -2900,6 +2900,7 @@ msgstr ""
#: bookwyrm/templates/preferences/edit_user.html:12
#: bookwyrm/templates/preferences/edit_user.html:25
#: bookwyrm/templates/settings/users/user_info.html:7
#: bookwyrm/templates/user_menu.html:25
msgid "Profile"
msgstr ""
@ -2953,11 +2954,28 @@ msgstr ""
msgid "Default post privacy:"
msgstr ""
#: bookwyrm/templates/preferences/export.html:4
#: bookwyrm/templates/preferences/export.html:7
msgid "CSV Export"
msgstr ""
#: bookwyrm/templates/preferences/export.html:13
msgid "Your export will include all the books on your shelves, books you have reviewed, and books with reading activity."
msgstr ""
#: bookwyrm/templates/preferences/layout.html:11
msgid "Account"
msgstr ""
#: bookwyrm/templates/preferences/layout.html:27
msgid "Data"
msgstr ""
#: bookwyrm/templates/preferences/layout.html:35
msgid "CSV export"
msgstr ""
#: bookwyrm/templates/preferences/layout.html:38
msgid "Relationships"
msgstr ""
@ -2992,19 +3010,19 @@ msgid "Update read dates for \"<em>%(title)s</em>\""
msgstr ""
#: bookwyrm/templates/readthrough/readthrough_form.html:10
#: bookwyrm/templates/readthrough/readthrough_modal.html:31
#: bookwyrm/templates/readthrough/readthrough_modal.html:38
#: bookwyrm/templates/snippets/reading_modals/finish_reading_modal.html:24
#: bookwyrm/templates/snippets/reading_modals/start_reading_modal.html:21
msgid "Started reading"
msgstr ""
#: bookwyrm/templates/readthrough/readthrough_form.html:18
#: bookwyrm/templates/readthrough/readthrough_modal.html:49
#: bookwyrm/templates/readthrough/readthrough_modal.html:56
msgid "Progress"
msgstr ""
#: bookwyrm/templates/readthrough/readthrough_form.html:24
#: bookwyrm/templates/readthrough/readthrough_modal.html:56
#: bookwyrm/templates/readthrough/readthrough_modal.html:63
#: bookwyrm/templates/snippets/reading_modals/finish_reading_modal.html:32
msgid "Finished reading"
msgstr ""
@ -3116,7 +3134,7 @@ msgstr ""
#: bookwyrm/templates/search/layout.html:23
#: bookwyrm/templates/search/layout.html:46
#: bookwyrm/templates/settings/email_blocklist/email_blocklist.html:27
#: bookwyrm/templates/settings/federation/instance_list.html:44
#: bookwyrm/templates/settings/federation/instance_list.html:51
#: bookwyrm/templates/settings/layout.html:36
#: bookwyrm/templates/settings/users/user.html:13
#: bookwyrm/templates/settings/users/user_admin.html:5
@ -3183,7 +3201,7 @@ msgid "Create Announcement"
msgstr ""
#: bookwyrm/templates/settings/announcements/announcements.html:21
#: bookwyrm/templates/settings/federation/instance_list.html:36
#: bookwyrm/templates/settings/federation/instance_list.html:39
msgid "Date added"
msgstr ""
@ -3478,19 +3496,19 @@ msgstr ""
#: bookwyrm/templates/settings/federation/edit_instance.html:52
#: bookwyrm/templates/settings/federation/instance.html:46
#: bookwyrm/templates/settings/users/user_info.html:106
#: bookwyrm/templates/settings/users/user_info.html:113
msgid "Status:"
msgstr ""
#: bookwyrm/templates/settings/federation/edit_instance.html:66
#: bookwyrm/templates/settings/federation/instance.html:40
#: bookwyrm/templates/settings/users/user_info.html:100
#: bookwyrm/templates/settings/users/user_info.html:107
msgid "Software:"
msgstr ""
#: bookwyrm/templates/settings/federation/edit_instance.html:76
#: bookwyrm/templates/settings/federation/instance.html:43
#: bookwyrm/templates/settings/users/user_info.html:103
#: bookwyrm/templates/settings/users/user_info.html:110
msgid "Version:"
msgstr ""
@ -3517,7 +3535,7 @@ msgid "View all"
msgstr ""
#: bookwyrm/templates/settings/federation/instance.html:62
#: bookwyrm/templates/settings/users/user_info.html:56
#: bookwyrm/templates/settings/users/user_info.html:60
msgid "Reports:"
msgstr ""
@ -3534,7 +3552,7 @@ msgid "Blocked by us:"
msgstr ""
#: bookwyrm/templates/settings/federation/instance.html:90
#: bookwyrm/templates/settings/users/user_info.html:110
#: bookwyrm/templates/settings/users/user_info.html:117
msgid "Notes"
msgstr ""
@ -3578,16 +3596,21 @@ msgstr ""
msgid "Failed:"
msgstr ""
#: bookwyrm/templates/settings/federation/instance_list.html:32
#: bookwyrm/templates/settings/federation/instance_list.html:35
#: bookwyrm/templates/settings/users/server_filter.html:5
msgid "Instance name"
msgstr ""
#: bookwyrm/templates/settings/federation/instance_list.html:40
#: bookwyrm/templates/settings/federation/instance_list.html:43
msgid "Last updated"
msgstr ""
#: bookwyrm/templates/settings/federation/instance_list.html:47
#: bookwyrm/templates/settings/federation/software_filter.html:5
msgid "Software"
msgstr ""
#: bookwyrm/templates/settings/federation/instance_list.html:63
#: bookwyrm/templates/settings/federation/instance_list.html:69
msgid "No instances found"
msgstr ""
@ -3598,6 +3621,14 @@ msgstr ""
msgid "Invite Requests"
msgstr ""
#: bookwyrm/templates/settings/invites/manage_invite_requests.html:15
#: bookwyrm/templates/settings/invites/manage_invites.html:3
#: bookwyrm/templates/settings/invites/manage_invites.html:15
#: bookwyrm/templates/settings/layout.html:42
#: bookwyrm/templates/user_menu.html:56
msgid "Invites"
msgstr ""
#: bookwyrm/templates/settings/invites/manage_invite_requests.html:23
msgid "Ignored Invite Requests"
msgstr ""
@ -3711,6 +3742,10 @@ msgstr ""
msgid "IP Address:"
msgstr ""
#: bookwyrm/templates/settings/ip_blocklist/ip_address_form.html:24
msgid "You can block IP ranges using CIDR syntax."
msgstr ""
#: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:5
#: bookwyrm/templates/settings/ip_blocklist/ip_blocklist.html:7
#: bookwyrm/templates/settings/layout.html:69
@ -3729,10 +3764,6 @@ msgstr ""
msgid "No IP addresses currently blocked"
msgstr ""
#: bookwyrm/templates/settings/ip_blocklist/ip_tooltip.html:6
msgid "You can block IP ranges using CIDR syntax."
msgstr ""
#: bookwyrm/templates/settings/layout.html:4
msgid "Administration"
msgstr ""
@ -3990,25 +4021,25 @@ msgid "Allow registration"
msgstr ""
#: bookwyrm/templates/settings/site.html:145
msgid "Allow invite requests"
msgstr ""
#: bookwyrm/templates/settings/site.html:151
msgid "Set a question for invite requests"
msgstr ""
#: bookwyrm/templates/settings/site.html:156
msgid "Question:"
msgstr ""
#: bookwyrm/templates/settings/site.html:163
msgid "Require users to confirm email address"
msgstr ""
#: bookwyrm/templates/settings/site.html:165
#: bookwyrm/templates/settings/site.html:147
msgid "(Recommended if registration is open)"
msgstr ""
#: bookwyrm/templates/settings/site.html:152
msgid "Allow invite requests"
msgstr ""
#: bookwyrm/templates/settings/site.html:158
msgid "Set a question for invite requests"
msgstr ""
#: bookwyrm/templates/settings/site.html:163
msgid "Question:"
msgstr ""
#: bookwyrm/templates/settings/site.html:168
msgid "Registration closed text:"
msgstr ""
@ -4107,18 +4138,18 @@ msgstr ""
msgid "Remote instance"
msgstr ""
#: bookwyrm/templates/settings/users/user_admin.html:67
#: bookwyrm/templates/settings/users/user_info.html:24
#: bookwyrm/templates/settings/users/user_admin.html:74
#: bookwyrm/templates/settings/users/user_info.html:28
msgid "Active"
msgstr ""
#: bookwyrm/templates/settings/users/user_admin.html:67
#: bookwyrm/templates/settings/users/user_info.html:28
#: bookwyrm/templates/settings/users/user_admin.html:79
#: bookwyrm/templates/settings/users/user_info.html:32
msgid "Inactive"
msgstr ""
#: bookwyrm/templates/settings/users/user_admin.html:73
#: bookwyrm/templates/settings/users/user_info.html:120
#: bookwyrm/templates/settings/users/user_admin.html:88
#: bookwyrm/templates/settings/users/user_info.html:127
msgid "Not set"
msgstr ""
@ -4126,51 +4157,59 @@ msgstr ""
msgid "View user profile"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:36
#: bookwyrm/templates/settings/users/user_info.html:19
msgid "Go to user admin"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:40
msgid "Local"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:38
#: bookwyrm/templates/settings/users/user_info.html:42
msgid "Remote"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:47
#: bookwyrm/templates/settings/users/user_info.html:51
msgid "User details"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:51
#: bookwyrm/templates/settings/users/user_info.html:55
msgid "Email:"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:61
#: bookwyrm/templates/settings/users/user_info.html:65
msgid "(View reports)"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:67
#: bookwyrm/templates/settings/users/user_info.html:71
msgid "Blocked by count:"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:70
#: bookwyrm/templates/settings/users/user_info.html:74
msgid "Date added:"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:77
msgid "Last active date:"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:73
#: bookwyrm/templates/settings/users/user_info.html:80
msgid "Manually approved followers:"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:76
#: bookwyrm/templates/settings/users/user_info.html:83
msgid "Discoverable:"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:80
#: bookwyrm/templates/settings/users/user_info.html:87
msgid "Deactivation reason:"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:95
#: bookwyrm/templates/settings/users/user_info.html:102
msgid "Instance details"
msgstr ""
#: bookwyrm/templates/settings/users/user_info.html:117
#: bookwyrm/templates/settings/users/user_info.html:124
msgid "View instance"
msgstr ""
@ -4246,6 +4285,10 @@ msgstr ""
msgid "You are running BookWyrm in production mode without https. <strong>USE_HTTPS</strong> should be enabled in production."
msgstr ""
#: bookwyrm/templates/setup/config.html:52 bookwyrm/templates/user_menu.html:45
msgid "Settings"
msgstr ""
#: bookwyrm/templates/setup/config.html:56
msgid "Instance domain:"
msgstr ""
@ -4413,7 +4456,7 @@ msgid "Some thoughts on the book"
msgstr ""
#: bookwyrm/templates/snippets/create_status/comment.html:27
#: bookwyrm/templates/snippets/reading_modals/progress_update_modal.html:17
#: bookwyrm/templates/snippets/reading_modals/progress_update_modal.html:18
msgid "Progress:"
msgstr ""
@ -5054,6 +5097,14 @@ msgstr[1] ""
msgid "No followers you follow"
msgstr ""
#: bookwyrm/templates/user_menu.html:7
msgid "View profile and more"
msgstr ""
#: bookwyrm/templates/user_menu.html:72
msgid "Log out"
msgstr ""
#: bookwyrm/templates/widgets/clearable_file_input_with_warning.html:28
msgid "File exceeds maximum size: 10MB"
msgstr ""

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

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