mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-29 04:51:11 +00:00
Merge branch 'main' into export-fixes
This commit is contained in:
commit
2c59908ddd
15 changed files with 7420 additions and 13 deletions
46
bookwyrm/migrations/0195_alter_user_preferred_language.py
Normal file
46
bookwyrm/migrations/0195_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Generated by Django 3.2.23 on 2024-02-21 00:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0194_merge_20240203_1619"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="preferred_language",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("en-us", "English"),
|
||||||
|
("ca-es", "Català (Catalan)"),
|
||||||
|
("de-de", "Deutsch (German)"),
|
||||||
|
("eo-uy", "Esperanto (Esperanto)"),
|
||||||
|
("es-es", "Español (Spanish)"),
|
||||||
|
("eu-es", "Euskara (Basque)"),
|
||||||
|
("gl-es", "Galego (Galician)"),
|
||||||
|
("it-it", "Italiano (Italian)"),
|
||||||
|
("ko-kr", "한국어 (Korean)"),
|
||||||
|
("fi-fi", "Suomi (Finnish)"),
|
||||||
|
("fr-fr", "Français (French)"),
|
||||||
|
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||||
|
("nl-nl", "Nederlands (Dutch)"),
|
||||||
|
("no-no", "Norsk (Norwegian)"),
|
||||||
|
("pl-pl", "Polski (Polish)"),
|
||||||
|
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||||
|
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||||
|
("ro-ro", "Română (Romanian)"),
|
||||||
|
("sv-se", "Svenska (Swedish)"),
|
||||||
|
("uk-ua", "Українська (Ukrainian)"),
|
||||||
|
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||||
|
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -321,6 +321,7 @@ LANGUAGES = [
|
||||||
("eu-es", _("Euskara (Basque)")),
|
("eu-es", _("Euskara (Basque)")),
|
||||||
("gl-es", _("Galego (Galician)")),
|
("gl-es", _("Galego (Galician)")),
|
||||||
("it-it", _("Italiano (Italian)")),
|
("it-it", _("Italiano (Italian)")),
|
||||||
|
("ko-kr", _("한국어 (Korean)")),
|
||||||
("fi-fi", _("Suomi (Finnish)")),
|
("fi-fi", _("Suomi (Finnish)")),
|
||||||
("fr-fr", _("Français (French)")),
|
("fr-fr", _("Français (French)")),
|
||||||
("lt-lt", _("Lietuvių (Lithuanian)")),
|
("lt-lt", _("Lietuvių (Lithuanian)")),
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
{% trans "Search for a book, user, or list" as search_placeholder %}
|
{% trans "Search for a book, author, user, or list" as search_placeholder %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "Search for a book" as search_placeholder %}
|
{% trans "Search for a book" as search_placeholder %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
17
bookwyrm/templates/search/author.html
Normal file
17
bookwyrm/templates/search/author.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends 'search/layout.html' %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
|
||||||
|
{% if results %}
|
||||||
|
<ul class="block">
|
||||||
|
{% for author in results %}
|
||||||
|
<li class="">
|
||||||
|
<a href="{{ author.local_path }}" class="author" itemprop="author" itemscope itemtype="https://schema.org/Thing">
|
||||||
|
<span itemprop="name">{{ author.name }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -20,6 +20,7 @@
|
||||||
<div class="select" aria-label="{% trans 'Search type' %}">
|
<div class="select" aria-label="{% trans 'Search type' %}">
|
||||||
<select name="type">
|
<select name="type">
|
||||||
<option value="book" {% if type == "book" %}selected{% endif %}>{% trans "Books" %}</option>
|
<option value="book" {% if type == "book" %}selected{% endif %}>{% trans "Books" %}</option>
|
||||||
|
<option value="author" {% if type == "author" %}selected{% endif %}>{% trans "Authors" %}</option>
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<option value="user" {% if type == "user" %}selected{% endif %}>{% trans "Users" %}</option>
|
<option value="user" {% if type == "user" %}selected{% endif %}>{% trans "Users" %}</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -42,6 +43,9 @@
|
||||||
<li{% if type == "book" %} class="is-active"{% endif %}>
|
<li{% if type == "book" %} class="is-active"{% endif %}>
|
||||||
<a href="{% url 'search' %}?q={{ query }}&type=book">{% trans "Books" %}</a>
|
<a href="{% url 'search' %}?q={{ query }}&type=book">{% trans "Books" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li{% if type == "author" %} class="is-active"{% endif %}>
|
||||||
|
<a href="{% url 'search' %}?q={{ query }}&type=author">{% trans "Authors" %}</a>
|
||||||
|
</li>
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<li{% if type == "user" %} class="is-active"{% endif %}>
|
<li{% if type == "user" %} class="is-active"{% endif %}>
|
||||||
<a href="{% url 'search' %}?q={{ query }}&type=user">{% trans "Users" %}</a>
|
<a href="{% url 'search' %}?q={{ query }}&type=user">{% trans "Users" %}</a>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
{% block filter %}
|
{% block filter %}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label class="label" for="filter_query">{% trans 'Filter by keyword' %}</label>
|
<label class="label" for="my-books-filter">{% trans 'Filter by keyword' %}</label>
|
||||||
<input aria-label="Filter by keyword" id="my-books-filter" class="input" type="text" name="filter" placeholder="{% trans 'Enter text here' %}" value="{{ shelves_filter_query|default:'' }}" spellcheck="false" />
|
<input id="my-books-filter" class="input" type="text" name="filter" placeholder="{% trans 'Enter text here' %}" value="{{ shelves_filter_query|default:'' }}" spellcheck="false" />
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -13,16 +13,26 @@ def validate_html(html):
|
||||||
"warn-proprietary-attributes": False,
|
"warn-proprietary-attributes": False,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
# idk how else to filter out these unescape amp errs
|
# Tidy's parser is strict when validating unescaped/encoded ampersands found within
|
||||||
|
# the html document that are notpart of a character or entity reference
|
||||||
|
# (eg: `&` or `&`). Despite the fact the HTML5 spec no longer recommends
|
||||||
|
# escaping ampersands in URLs, Tidy will still complain if they are used as query
|
||||||
|
# param keys. Unfortunately, there is no way currently to configure tidy to ignore
|
||||||
|
# this so we must explictly redlist related strings that will appear in Tidy's
|
||||||
|
# errors output.
|
||||||
|
#
|
||||||
|
# See further discussion: https://github.com/htacg/tidy-html5/issues/1017
|
||||||
|
excluded = [
|
||||||
|
"&book",
|
||||||
|
"&type",
|
||||||
|
"&resolved",
|
||||||
|
"id and name attribute",
|
||||||
|
"illegal characters found in URI",
|
||||||
|
"escaping malformed URI reference",
|
||||||
|
"&filter",
|
||||||
|
]
|
||||||
errors = "\n".join(
|
errors = "\n".join(
|
||||||
e
|
e for e in errors.split("\n") if not any(exclude in e for exclude in excluded)
|
||||||
for e in errors.split("\n")
|
|
||||||
if "&book" not in e
|
|
||||||
and "&type" not in e
|
|
||||||
and "&resolved" not in e
|
|
||||||
and "id and name attribute" not in e
|
|
||||||
and "illegal characters found in URI" not in e
|
|
||||||
and "escaping malformed URI reference" not in e
|
|
||||||
)
|
)
|
||||||
if errors:
|
if errors:
|
||||||
raise Exception(errors)
|
raise Exception(errors)
|
||||||
|
|
|
@ -133,3 +133,73 @@ class BookViews(TestCase):
|
||||||
|
|
||||||
self.assertEqual(models.ShelfBook.objects.get().book, edition2)
|
self.assertEqual(models.ShelfBook.objects.get().book, edition2)
|
||||||
self.assertEqual(models.ReadThrough.objects.get().book, edition2)
|
self.assertEqual(models.ReadThrough.objects.get().book, edition2)
|
||||||
|
|
||||||
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
|
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||||
|
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
|
||||||
|
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||||
|
def test_move_ratings_on_switch_edition(self, *_):
|
||||||
|
"""updates user's rating on a book to new edition"""
|
||||||
|
work = models.Work.objects.create(title="test work")
|
||||||
|
edition1 = models.Edition.objects.create(title="first ed", parent_work=work)
|
||||||
|
edition2 = models.Edition.objects.create(title="second ed", parent_work=work)
|
||||||
|
|
||||||
|
models.ReviewRating.objects.create(
|
||||||
|
book=edition1,
|
||||||
|
user=self.local_user,
|
||||||
|
rating=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsInstance(
|
||||||
|
models.ReviewRating.objects.get(user=self.local_user, book=edition1),
|
||||||
|
models.ReviewRating,
|
||||||
|
)
|
||||||
|
with self.assertRaises(models.ReviewRating.DoesNotExist):
|
||||||
|
models.ReviewRating.objects.get(user=self.local_user, book=edition2)
|
||||||
|
|
||||||
|
request = self.factory.post("", {"edition": edition2.id})
|
||||||
|
request.user = self.local_user
|
||||||
|
views.switch_edition(request)
|
||||||
|
|
||||||
|
self.assertIsInstance(
|
||||||
|
models.ReviewRating.objects.get(user=self.local_user, book=edition2),
|
||||||
|
models.ReviewRating,
|
||||||
|
)
|
||||||
|
with self.assertRaises(models.ReviewRating.DoesNotExist):
|
||||||
|
models.ReviewRating.objects.get(user=self.local_user, book=edition1)
|
||||||
|
|
||||||
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
|
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||||
|
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
|
||||||
|
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||||
|
def test_move_reviews_on_switch_edition(self, *_):
|
||||||
|
"""updates user's review on a book to new edition"""
|
||||||
|
work = models.Work.objects.create(title="test work")
|
||||||
|
edition1 = models.Edition.objects.create(title="first ed", parent_work=work)
|
||||||
|
edition2 = models.Edition.objects.create(title="second ed", parent_work=work)
|
||||||
|
|
||||||
|
models.Review.objects.create(
|
||||||
|
book=edition1,
|
||||||
|
user=self.local_user,
|
||||||
|
name="blah",
|
||||||
|
rating=3,
|
||||||
|
content="not bad",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsInstance(
|
||||||
|
models.Review.objects.get(user=self.local_user, book=edition1),
|
||||||
|
models.Review,
|
||||||
|
)
|
||||||
|
with self.assertRaises(models.Review.DoesNotExist):
|
||||||
|
models.Review.objects.get(user=self.local_user, book=edition2)
|
||||||
|
|
||||||
|
request = self.factory.post("", {"edition": edition2.id})
|
||||||
|
request.user = self.local_user
|
||||||
|
views.switch_edition(request)
|
||||||
|
|
||||||
|
self.assertIsInstance(
|
||||||
|
models.Review.objects.get(user=self.local_user, book=edition2),
|
||||||
|
models.Review,
|
||||||
|
)
|
||||||
|
with self.assertRaises(models.Review.DoesNotExist):
|
||||||
|
models.Review.objects.get(user=self.local_user, book=edition1)
|
||||||
|
|
|
@ -219,3 +219,48 @@ class ShelfViews(TestCase):
|
||||||
view(request, request.user.username, shelf.identifier)
|
view(request, request.user.username, shelf.identifier)
|
||||||
|
|
||||||
self.assertEqual(shelf.name, "To Read")
|
self.assertEqual(shelf.name, "To Read")
|
||||||
|
|
||||||
|
def test_filter_shelf_found(self, *_):
|
||||||
|
"""display books that match a filter keyword"""
|
||||||
|
models.ShelfBook.objects.create(
|
||||||
|
book=self.book,
|
||||||
|
shelf=self.shelf,
|
||||||
|
user=self.local_user,
|
||||||
|
)
|
||||||
|
shelf_book = models.ShelfBook.objects.create(
|
||||||
|
book=self.book,
|
||||||
|
shelf=self.local_user.shelf_set.first(),
|
||||||
|
user=self.local_user,
|
||||||
|
)
|
||||||
|
view = views.Shelf.as_view()
|
||||||
|
request = self.factory.get("", {"filter": shelf_book.book.title})
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.local_user.username)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
validate_html(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
self.assertEqual(len(result.context_data["books"].object_list), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
result.context_data["books"].object_list[0].title,
|
||||||
|
shelf_book.book.title,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_filter_shelf_none(self, *_):
|
||||||
|
"""display a message when no books match a filter keyword"""
|
||||||
|
models.ShelfBook.objects.create(
|
||||||
|
book=self.book,
|
||||||
|
shelf=self.shelf,
|
||||||
|
user=self.local_user,
|
||||||
|
)
|
||||||
|
view = views.Shelf.as_view()
|
||||||
|
request = self.factory.get("", {"filter": "NOPE"})
|
||||||
|
request.user = self.local_user
|
||||||
|
with patch("bookwyrm.views.shelf.shelf.is_api_request") as is_api:
|
||||||
|
is_api.return_value = False
|
||||||
|
result = view(request, self.local_user.username)
|
||||||
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
|
validate_html(result.render())
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
self.assertEqual(len(result.context_data["books"].object_list), 0)
|
||||||
|
|
|
@ -103,4 +103,13 @@ def switch_edition(request):
|
||||||
readthrough.book = new_edition
|
readthrough.book = new_edition
|
||||||
readthrough.save()
|
readthrough.save()
|
||||||
|
|
||||||
|
reviews = models.Review.objects.filter(
|
||||||
|
book__parent_work=new_edition.parent_work, user=request.user
|
||||||
|
)
|
||||||
|
for review in reviews.all():
|
||||||
|
# because ratings are a subclass of reviews,
|
||||||
|
# this will pick up both ratings and reviews
|
||||||
|
review.book = new_edition
|
||||||
|
review.save()
|
||||||
|
|
||||||
return redirect(f"/book/{new_edition.id}")
|
return redirect(f"/book/{new_edition.id}")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
""" search views"""
|
""" search views"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.contrib.postgres.search import TrigramSimilarity
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
|
@ -39,6 +40,7 @@ class Search(View):
|
||||||
|
|
||||||
endpoints = {
|
endpoints = {
|
||||||
"book": book_search,
|
"book": book_search,
|
||||||
|
"author": author_search,
|
||||||
"user": user_search,
|
"user": user_search,
|
||||||
"list": list_search,
|
"list": list_search,
|
||||||
}
|
}
|
||||||
|
@ -90,6 +92,31 @@ def book_search(request):
|
||||||
return TemplateResponse(request, "search/book.html", data)
|
return TemplateResponse(request, "search/book.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
def author_search(request):
|
||||||
|
"""search for an author"""
|
||||||
|
query = request.GET.get("q")
|
||||||
|
query = query.strip()
|
||||||
|
data = {"type": "author", "query": query}
|
||||||
|
|
||||||
|
results = (
|
||||||
|
models.Author.objects.annotate(
|
||||||
|
similarity=TrigramSimilarity("name", query),
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
similarity__gt=0.1,
|
||||||
|
)
|
||||||
|
.order_by("-similarity")
|
||||||
|
)
|
||||||
|
|
||||||
|
paginated = Paginator(results, PAGE_LENGTH)
|
||||||
|
page = paginated.get_page(request.GET.get("page"))
|
||||||
|
data["results"] = page
|
||||||
|
data["page_range"] = paginated.get_elided_page_range(
|
||||||
|
page.number, on_each_side=2, on_ends=1
|
||||||
|
)
|
||||||
|
return TemplateResponse(request, "search/author.html", data)
|
||||||
|
|
||||||
|
|
||||||
def user_search(request):
|
def user_search(request):
|
||||||
"""user search: search for a user"""
|
"""user search: search for a user"""
|
||||||
viewer = request.user
|
viewer = request.user
|
||||||
|
|
1
bw-dev
1
bw-dev
|
@ -156,6 +156,7 @@ case "$CMD" in
|
||||||
git checkout l10n_main locale/fi_FI
|
git checkout l10n_main locale/fi_FI
|
||||||
git checkout l10n_main locale/fr_FR
|
git checkout l10n_main locale/fr_FR
|
||||||
git checkout l10n_main locale/gl_ES
|
git checkout l10n_main locale/gl_ES
|
||||||
|
git checkout l10n_main locale/ko_KR
|
||||||
git checkout l10n_main locale/it_IT
|
git checkout l10n_main locale/it_IT
|
||||||
git checkout l10n_main locale/lt_LT
|
git checkout l10n_main locale/lt_LT
|
||||||
git checkout l10n_main locale/nl_NL
|
git checkout l10n_main locale/nl_NL
|
||||||
|
|
BIN
locale/ko_KR/LC_MESSAGES/django.mo
Normal file
BIN
locale/ko_KR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
7177
locale/ko_KR/LC_MESSAGES/django.po
Normal file
7177
locale/ko_KR/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load diff
|
@ -2,7 +2,7 @@ aiohttp==3.9.2
|
||||||
bleach==5.0.1
|
bleach==5.0.1
|
||||||
celery==5.2.7
|
celery==5.2.7
|
||||||
colorthief==0.2.1
|
colorthief==0.2.1
|
||||||
Django==3.2.23
|
Django==3.2.24
|
||||||
django-celery-beat==2.4.0
|
django-celery-beat==2.4.0
|
||||||
bw-file-resubmit==0.6.0rc2
|
bw-file-resubmit==0.6.0rc2
|
||||||
django-compressor==4.3.1
|
django-compressor==4.3.1
|
||||||
|
|
Loading…
Reference in a new issue