Skip to content

Commit

Permalink
Merge v0.7.2 (production) into spinoza
Browse files Browse the repository at this point in the history
Conflicts:
	requirements.txt
  • Loading branch information
dato committed Mar 3, 2024
2 parents ed7a0fe + 98bf187 commit f7de8c0
Show file tree
Hide file tree
Showing 260 changed files with 20,321 additions and 5,317 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,6 @@ TWO_FACTOR_LOGIN_MAX_SECONDS=60
# and AWS_S3_CUSTOM_DOMAIN (if used) are added by default.
# Value should be a comma-separated list of host names.
CSP_ADDITIONAL_HOSTS=
# The last number here means "megabytes"
# Increase if users are having trouble uploading BookWyrm export files.
DATA_UPLOAD_MAX_MEMORY_SIZE = (1024**2 * 100)
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.0
0.7.1
20 changes: 8 additions & 12 deletions bookwyrm/activitypub/base_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def serialize(self, **kwargs):
omit = kwargs.get("omit", ())
data = self.__dict__.copy()
# recursively serialize
for (k, v) in data.items():
for k, v in data.items():
try:
if issubclass(type(v), ActivityObject):
data[k] = v.serialize()
Expand Down Expand Up @@ -396,19 +396,15 @@ def resolve_remote_id(

def get_representative():
"""Get or create an actor representing the instance
to sign requests to 'secure mastodon' servers"""
username = f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}"
email = "bookwyrm@localhost"
try:
user = models.User.objects.get(username=username)
except models.User.DoesNotExist:
user = models.User.objects.create_user(
username=username,
email=email,
to sign outgoing HTTP GET requests"""
return models.User.objects.get_or_create(
username=f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}",
defaults=dict(
email="bookwyrm@localhost",
local=True,
localname=INSTANCE_ACTOR_USERNAME,
)
return user
),
)[0]


def get_activitypub_data(url):
Expand Down
16 changes: 13 additions & 3 deletions bookwyrm/activitypub/verbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,19 @@ class Reject(Verb):
type: str = "Reject"

def action(self, allow_external_connections=True):
"""reject a follow request"""
obj = self.object.to_model(save=False, allow_create=False)
obj.reject()
"""reject a follow or follow request"""

for model_name in ["UserFollowRequest", "UserFollows", None]:
model = apps.get_model(f"bookwyrm.{model_name}") if model_name else None
if obj := self.object.to_model(
model=model,
save=False,
allow_create=False,
allow_external_connections=allow_external_connections,
):
# Reject the first model that can be built.
obj.reject()
break


@dataclass(init=False)
Expand Down
32 changes: 24 additions & 8 deletions bookwyrm/book_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
Expand All @@ -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

Expand Down Expand Up @@ -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")
Expand All @@ -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()

Expand All @@ -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")
Expand All @@ -137,7 +153,7 @@ def search_title_author(

# filter out multiple editions of the same work
list_results = []
for work_id in set(editions_of_work[:30]):
for work_id in editions_of_work[:30]:
result = (
results.filter(parent_work=work_id)
.order_by("-rank", "-edition_rank")
Expand Down
7 changes: 3 additions & 4 deletions bookwyrm/forms/books.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
""" using django model forms """
from django import forms

from file_resubmit.widgets import ResubmitImageWidget

from bookwyrm import models
from bookwyrm.models.fields import ClearableFileInputWithWarning
from .custom_form import CustomForm
from .widgets import ArrayWidget, SelectDateWidget, Select

Expand Down Expand Up @@ -70,9 +71,7 @@ class Meta:
"published_date": SelectDateWidget(
attrs={"aria-describedby": "desc_published_date"}
),
"cover": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_cover"}
),
"cover": ResubmitImageWidget(attrs={"aria-describedby": "desc_cover"}),
"physical_format": Select(
attrs={"aria-describedby": "desc_physical_format"}
),
Expand Down
4 changes: 4 additions & 0 deletions bookwyrm/forms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class ImportForm(forms.Form):
csv_file = forms.FileField()


class ImportUserForm(forms.Form):
archive_file = forms.FileField()


class ShelfForm(CustomForm):
class Meta:
model = models.Shelf
Expand Down
1 change: 1 addition & 0 deletions bookwyrm/importers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" import classes """

from .importer import Importer
from .bookwyrm_import import BookwyrmImporter
from .calibre_import import CalibreImporter
from .goodreads_import import GoodreadsImporter
from .librarything_import import LibrarythingImporter
Expand Down
24 changes: 24 additions & 0 deletions bookwyrm/importers/bookwyrm_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Import data from Bookwyrm export files"""
from django.http import QueryDict

from bookwyrm.models import User
from bookwyrm.models.bookwyrm_import_job import BookwyrmImportJob


class BookwyrmImporter:
"""Import a Bookwyrm User export file.
This is kind of a combination of an importer and a connector.
"""

# pylint: disable=no-self-use
def process_import(
self, user: User, archive_file: bytes, settings: QueryDict
) -> BookwyrmImportJob:
"""import user data from a Bookwyrm export file"""

required = [k for k in settings if settings.get(k) == "on"]

job = BookwyrmImportJob.objects.create(
user=user, archive_file=archive_file, required=required
)
return job
43 changes: 43 additions & 0 deletions bookwyrm/management/commands/erase_deleted_user_data.py
Original file line number Diff line number Diff line change
@@ -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")
1 change: 1 addition & 0 deletions bookwyrm/middleware/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
""" look at all this nice middleware! """
from .timezone_middleware import TimezoneMiddleware
from .ip_middleware import IPBlocklistMiddleware
from .file_too_big import FileTooBig
30 changes: 30 additions & 0 deletions bookwyrm/middleware/file_too_big.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Middleware to display a custom 413 error page"""

from django.http import HttpResponse
from django.shortcuts import render
from django.core.exceptions import RequestDataTooBig


class FileTooBig:
"""Middleware to display a custom page when a
RequestDataTooBig exception is thrown"""

def __init__(self, get_response):
"""boilerplate __init__ from Django docs"""

self.get_response = get_response

def __call__(self, request):
"""If RequestDataTooBig is thrown, render the 413 error page"""

try:
body = request.body # pylint: disable=unused-variable

except RequestDataTooBig:

rendered = render(request, "413.html")
response = HttpResponse(rendered)
return response

response = self.get_response(request)
return response
14 changes: 0 additions & 14 deletions bookwyrm/migrations/0184_auto_20231106_0421.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,6 @@ def update_deleted_users(apps, schema_editor):
).update(is_deleted=True)


def erase_deleted_user_data(apps, schema_editor):
"""Retroactively clear user data"""
for user in User.objects.filter(is_deleted=True):
user.erase_user_data()
user.save(
broadcast=False,
update_fields=["email", "avatar", "preview_image", "summary", "name"],
)
user.erase_user_statuses(broadcast=False)


class Migration(migrations.Migration):

dependencies = [
Expand All @@ -43,7 +32,4 @@ class Migration(migrations.Migration):
migrations.RunPython(
update_deleted_users, reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
erase_deleted_user_data, reverse_code=migrations.RunPython.noop
),
]
Loading

0 comments on commit f7de8c0

Please sign in to comment.