mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-02-03 12:52:20 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
658140f18c
11 changed files with 127 additions and 17 deletions
|
@ -43,6 +43,7 @@ def search(
|
|||
min_confidence: float = 0,
|
||||
filters: Optional[list[Any]] = None,
|
||||
return_first: bool = False,
|
||||
books: Optional[QuerySet[models.Edition]] = None,
|
||||
) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
|
||||
"""search your local database"""
|
||||
filters = filters or []
|
||||
|
@ -54,13 +55,15 @@ def search(
|
|||
# first, try searching unique identifiers
|
||||
# unique identifiers never have spaces, title/author usually do
|
||||
if not " " in query:
|
||||
results = search_identifiers(query, *filters, return_first=return_first)
|
||||
results = search_identifiers(
|
||||
query, *filters, return_first=return_first, books=books
|
||||
)
|
||||
|
||||
# if there were no identifier results...
|
||||
if not results:
|
||||
# then try searching title/author
|
||||
results = search_title_author(
|
||||
query, min_confidence, *filters, return_first=return_first
|
||||
query, min_confidence, *filters, return_first=return_first, books=books
|
||||
)
|
||||
return results
|
||||
|
||||
|
@ -98,9 +101,17 @@ def format_search_result(search_result):
|
|||
|
||||
|
||||
def search_identifiers(
|
||||
query, *filters, return_first=False
|
||||
query,
|
||||
*filters,
|
||||
return_first=False,
|
||||
books=None,
|
||||
) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
|
||||
"""tries remote_id, isbn; defined as dedupe fields on the model"""
|
||||
"""search Editions by deduplication fields
|
||||
|
||||
Best for cases when we can assume someone is searching for an exact match on
|
||||
commonly unique data identifiers like isbn or specific library ids.
|
||||
"""
|
||||
books = books or models.Edition.objects
|
||||
if connectors.maybe_isbn(query):
|
||||
# Oh did you think the 'S' in ISBN stood for 'standard'?
|
||||
normalized_isbn = query.strip().upper().rjust(10, "0")
|
||||
|
@ -111,7 +122,7 @@ def search_identifiers(
|
|||
for f in models.Edition._meta.get_fields()
|
||||
if hasattr(f, "deduplication_field") and f.deduplication_field
|
||||
]
|
||||
results = models.Edition.objects.filter(
|
||||
results = books.filter(
|
||||
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
|
||||
).distinct()
|
||||
|
||||
|
@ -121,12 +132,17 @@ def search_identifiers(
|
|||
|
||||
|
||||
def search_title_author(
|
||||
query, min_confidence, *filters, return_first=False
|
||||
query,
|
||||
min_confidence,
|
||||
*filters,
|
||||
return_first=False,
|
||||
books=None,
|
||||
) -> QuerySet[models.Edition]:
|
||||
"""searches for title and author"""
|
||||
books = books or models.Edition.objects
|
||||
query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
|
||||
results = (
|
||||
models.Edition.objects.filter(*filters, search_vector=query)
|
||||
books.filter(*filters, search_vector=query)
|
||||
.annotate(rank=SearchRank(F("search_vector"), query))
|
||||
.filter(rank__gt=min_confidence)
|
||||
.order_by("-rank")
|
||||
|
|
43
bookwyrm/management/commands/erase_deleted_user_data.py
Normal file
43
bookwyrm/management/commands/erase_deleted_user_data.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
""" Erase any data stored about deleted users """
|
||||
import sys
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.user import erase_user_data
|
||||
|
||||
# pylint: disable=missing-function-docstring
|
||||
class Command(BaseCommand):
|
||||
"""command-line options"""
|
||||
|
||||
help = "Remove Two Factor Authorisation from user"
|
||||
|
||||
def add_arguments(self, parser): # pylint: disable=no-self-use
|
||||
parser.add_argument(
|
||||
"--dryrun",
|
||||
action="store_true",
|
||||
help="Preview users to be cleared without altering the database",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options): # pylint: disable=unused-argument
|
||||
|
||||
# Check for anything fishy
|
||||
bad_state = models.User.objects.filter(is_deleted=True, is_active=True)
|
||||
if bad_state.exists():
|
||||
raise CommandError(
|
||||
f"{bad_state.count()} user(s) marked as both active and deleted"
|
||||
)
|
||||
|
||||
deleted_users = models.User.objects.filter(is_deleted=True)
|
||||
self.stdout.write(f"Found {deleted_users.count()} deleted users")
|
||||
if options["dryrun"]:
|
||||
self.stdout.write("\n".join(u.username for u in deleted_users[:5]))
|
||||
if deleted_users.count() > 5:
|
||||
self.stdout.write("... and more")
|
||||
sys.exit()
|
||||
|
||||
self.stdout.write("Erasing user data:")
|
||||
for user_id in deleted_users.values_list("id", flat=True):
|
||||
erase_user_data.delay(user_id)
|
||||
self.stdout.write(".", ending="")
|
||||
|
||||
self.stdout.write("")
|
||||
self.stdout.write("Tasks created successfully")
|
|
@ -523,6 +523,20 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
|
|||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@app.task(queue=MISC)
|
||||
def erase_user_data(user_id):
|
||||
"""Erase any custom data about this user asynchronously
|
||||
This is for deleted historical user data that pre-dates data
|
||||
being cleared automatically"""
|
||||
user = User.objects.get(id=user_id)
|
||||
user.erase_user_data()
|
||||
user.save(
|
||||
broadcast=False,
|
||||
update_fields=["email", "avatar", "preview_image", "summary", "name"],
|
||||
)
|
||||
user.erase_user_statuses(broadcast=False)
|
||||
|
||||
|
||||
@app.task(queue=MISC)
|
||||
def set_remote_server(user_id, allow_external_connections=False):
|
||||
"""figure out the user's remote server in the background"""
|
||||
|
|
|
@ -10,9 +10,7 @@
|
|||
{% elif notification.notification_type == 'FOLLOW' %}
|
||||
{% include 'notifications/items/follow.html' %}
|
||||
{% elif notification.notification_type == 'FOLLOW_REQUEST' %}
|
||||
{% if notification.related_users.0.is_active %}
|
||||
{% include 'notifications/items/follow_request.html' %}
|
||||
{% endif %}
|
||||
{% include 'notifications/items/follow_request.html' %}
|
||||
{% elif notification.notification_type == 'IMPORT' %}
|
||||
{% include 'notifications/items/import.html' %}
|
||||
{% elif notification.notification_type == 'USER_IMPORT' %}
|
||||
|
|
|
@ -101,7 +101,6 @@
|
|||
{% plural %}
|
||||
{{ formatted_count }} books
|
||||
{% endblocktrans %}
|
||||
|
||||
{% if books.has_other_pages %}
|
||||
{% blocktrans trimmed with start=books.start_index end=books.end_index %}
|
||||
(showing {{ start }}-{{ end }})
|
||||
|
@ -111,6 +110,8 @@
|
|||
{% endif %}
|
||||
{% endwith %}
|
||||
</h2>
|
||||
{% include 'shelf/shelves_filters.html' with user=user query=query %}
|
||||
|
||||
</div>
|
||||
{% if is_self and shelf.id %}
|
||||
<div class="column is-narrow">
|
||||
|
@ -209,7 +210,17 @@
|
|||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p><em>{% trans "This shelf is empty." %}</em></p>
|
||||
<p>
|
||||
<em>
|
||||
{% if shelves_filter_query %}
|
||||
{% blocktrans trimmed %}
|
||||
We couldn't find any books that matched {{ shelves_filter_query }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "This shelf is empty." %}
|
||||
{% endif %}
|
||||
</em>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
|
9
bookwyrm/templates/shelf/shelves_filter_field.html
Normal file
9
bookwyrm/templates/shelf/shelves_filter_field.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% extends 'snippets/filters_panel/filter_field.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block filter %}
|
||||
<div class="control">
|
||||
<label class="label" for="filter_query">{% 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" />
|
||||
</div>
|
||||
{% endblock %}
|
5
bookwyrm/templates/shelf/shelves_filters.html
Normal file
5
bookwyrm/templates/shelf/shelves_filters.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||
|
||||
{% block filter_fields %}
|
||||
{% include 'shelf/shelves_filter_field.html' %}
|
||||
{% endblock %}
|
|
@ -51,7 +51,7 @@ class Search(View):
|
|||
def api_book_search(request):
|
||||
"""Return books via API response"""
|
||||
query = request.GET.get("q")
|
||||
query = isbn_check(query)
|
||||
query = isbn_check_and_format(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
# only return local book results via json so we don't cascade
|
||||
book_results = search(query, min_confidence=min_confidence)
|
||||
|
@ -64,7 +64,7 @@ def book_search(request):
|
|||
"""the real business is elsewhere"""
|
||||
query = request.GET.get("q")
|
||||
# check if query is isbn
|
||||
query = isbn_check(query)
|
||||
query = isbn_check_and_format(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
search_remote = request.GET.get("remote", False) and request.user.is_authenticated
|
||||
|
||||
|
@ -159,7 +159,7 @@ def list_search(request):
|
|||
return TemplateResponse(request, "search/list.html", data)
|
||||
|
||||
|
||||
def isbn_check(query):
|
||||
def isbn_check_and_format(query):
|
||||
"""isbn10 or isbn13 check, if so remove separators"""
|
||||
if query:
|
||||
su_num = re.sub(r"(?<=\d)\D(?=\d|[xX])", "", query)
|
||||
|
|
|
@ -15,12 +15,14 @@ from bookwyrm import forms, models
|
|||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request, get_user_from_username
|
||||
from bookwyrm.book_search import search
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class Shelf(View):
|
||||
"""shelf page"""
|
||||
|
||||
# pylint: disable=R0914
|
||||
def get(self, request, username, shelf_identifier=None):
|
||||
"""display a shelf"""
|
||||
user = get_user_from_username(request.user, username)
|
||||
|
@ -32,6 +34,8 @@ class Shelf(View):
|
|||
else:
|
||||
shelves = models.Shelf.privacy_filter(request.user).filter(user=user).all()
|
||||
|
||||
shelves_filter_query = request.GET.get("filter")
|
||||
|
||||
# get the shelf and make sure the logged in user should be able to see it
|
||||
if shelf_identifier:
|
||||
shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
|
||||
|
@ -42,6 +46,7 @@ class Shelf(View):
|
|||
FakeShelf = namedtuple(
|
||||
"Shelf", ("identifier", "name", "user", "books", "privacy")
|
||||
)
|
||||
|
||||
books = (
|
||||
models.Edition.viewer_aware_objects(request.user)
|
||||
.filter(
|
||||
|
@ -50,6 +55,7 @@ class Shelf(View):
|
|||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
shelf = FakeShelf("all", _("All books"), user, books, "public")
|
||||
|
||||
if is_api_request(request) and shelf_identifier:
|
||||
|
@ -86,6 +92,9 @@ class Shelf(View):
|
|||
|
||||
books = sort_books(books, request.GET.get("sort"))
|
||||
|
||||
if shelves_filter_query:
|
||||
books = search(shelves_filter_query, books=books)
|
||||
|
||||
paginated = Paginator(
|
||||
books,
|
||||
PAGE_LENGTH,
|
||||
|
@ -103,6 +112,8 @@ class Shelf(View):
|
|||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"shelves_filter_query": shelves_filter_query,
|
||||
"size": "small",
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "shelf/shelf.html", data)
|
||||
|
|
3
bw-dev
3
bw-dev
|
@ -246,6 +246,9 @@ case "$CMD" in
|
|||
remove_remote_user_preview_images)
|
||||
runweb python manage.py remove_remote_user_preview_images
|
||||
;;
|
||||
erase_deleted_user_data)
|
||||
runweb python manage.py erase_deleted_user_data "$@"
|
||||
;;
|
||||
copy_media_to_s3)
|
||||
awscommand "bookwyrm_media_volume:/images"\
|
||||
"s3 cp /images s3://${AWS_STORAGE_BUCKET_NAME}/images\
|
||||
|
|
|
@ -18,7 +18,7 @@ Markdown==3.4.1
|
|||
packaging==21.3
|
||||
Pillow==10.0.1
|
||||
psycopg2==2.9.5
|
||||
pycryptodome==3.16.0
|
||||
pycryptodome==3.19.1
|
||||
python-dateutil==2.8.2
|
||||
redis==4.5.4
|
||||
requests==2.31.0
|
||||
|
|
Loading…
Reference in a new issue