Skip to content

Commit 98bf187

Browse files
committed
Merge branch 'main' into production
2 parents 658140f + 61a6ee2 commit 98bf187

File tree

16 files changed

+192
-16
lines changed

16 files changed

+192
-16
lines changed

.env.example

+3
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,6 @@ TWO_FACTOR_LOGIN_MAX_SECONDS=60
137137
# and AWS_S3_CUSTOM_DOMAIN (if used) are added by default.
138138
# Value should be a comma-separated list of host names.
139139
CSP_ADDITIONAL_HOSTS=
140+
# The last number here means "megabytes"
141+
# Increase if users are having trouble uploading BookWyrm export files.
142+
DATA_UPLOAD_MAX_MEMORY_SIZE = (1024**2 * 100)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.23 on 2024-01-16 10:28
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("bookwyrm", "0191_merge_20240102_0326"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="sitesettings",
15+
name="user_exports_enabled",
16+
field=models.BooleanField(default=False),
17+
),
18+
]

bookwyrm/models/site.py

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ class SiteSettings(SiteModel):
9696
imports_enabled = models.BooleanField(default=True)
9797
import_size_limit = models.IntegerField(default=0)
9898
import_limit_reset = models.IntegerField(default=0)
99+
user_exports_enabled = models.BooleanField(default=False)
99100
user_import_time_limit = models.IntegerField(default=48)
100101

101102
field_tracker = FieldTracker(fields=["name", "instance_tagline", "logo"])

bookwyrm/settings.py

+2
Original file line numberDiff line numberDiff line change
@@ -442,3 +442,5 @@
442442
# Do not change this setting unless you already have an existing
443443
# user with the same username - in which case you should change it!
444444
INSTANCE_ACTOR_USERNAME = "bookwyrm.instance.actor"
445+
446+
DATA_UPLOAD_MAX_MEMORY_SIZE = env.int("DATA_UPLOAD_MAX_MEMORY_SIZE", (1024**2 * 100))

bookwyrm/templates/moved.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
<div class="notification is-warning">
2525
<p>
26-
{% id_to_username request.user.moved_to as username %}
26+
{% id_to_username request.user.moved_to as username %}
2727
{% blocktrans trimmed with moved_to=user.moved_to %}
2828
<strong>You have moved your account</strong> to <a href="{{ moved_to }}">{{ username }}</a>
2929
{% endblocktrans %}

bookwyrm/templates/notifications/items/move_user.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
{% block description %}
1616
{% if related_user_moved_to %}
17-
{% id_to_username request.user.moved_to as username %}
17+
{% id_to_username related_user_moved_to as username %}
1818
{% blocktrans trimmed %}
1919
{{ related_user }} has moved to <a href="{{ related_user_moved_to }}">{{ username }}</a>
2020
{% endblocktrans %}

bookwyrm/templates/preferences/export-user.html

+5-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ <h2 class="is-size-5">Your file will not include:</h2>
4646
{% trans "If you wish to migrate any statuses (comments, reviews, or quotes) you must either set the account you are moving to as an <strong>alias</strong> of this one, or <strong>move</strong> this account to the new account, before you import your user data." %}
4747
{% endspaceless %}
4848
</p>
49-
{% if next_available %}
49+
{% if not site.user_exports_enabled %}
50+
<p class="notification is-danger">
51+
{% trans "New user exports are currently disabled." %}
52+
</p>
53+
{% elif next_available %}
5054
<p class="notification is-warning">
5155
{% blocktrans trimmed %}
5256
You will be able to create a new export file at {{ next_available }}

bookwyrm/templates/settings/imports/imports.html

+50-1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,33 @@
9090
</div>
9191
</form>
9292
</details>
93+
94+
{% if site.user_exports_enabled %}
95+
<details class="details-panel box">
96+
<summary>
97+
<span role="heading" aria-level="2" class="title is-6">
98+
{% trans "Disable starting new user exports" %}
99+
</span>
100+
<span class="details-close icon icon-x" aria-hidden="true"></span>
101+
</summary>
102+
<form
103+
name="disable-user-exports"
104+
id="disable-user-exports"
105+
method="POST"
106+
action="{% url 'settings-user-exports-disable' %}"
107+
>
108+
<div class="notification">
109+
{% trans "This is only intended to be used when things have gone very wrong with exports and you need to pause the feature while addressing issues." %}
110+
{% trans "While exports are disabled, users will not be allowed to start new user exports, but existing exports will not be affected." %}
111+
</div>
112+
{% csrf_token %}
113+
<div class="control">
114+
<button type="submit" class="button is-danger">
115+
{% trans "Disable user exports" %}
116+
</button>
117+
</div>
118+
</form>
119+
</details>
93120
<details class="details-panel box">
94121
<summary>
95122
<span role="heading" aria-level="2" class="title is-6">
@@ -108,7 +135,7 @@
108135
{% trans "Set the value to 0 to not enforce any limit." %}
109136
</div>
110137
<div class="align.to-t">
111-
<label for="limit">{% trans "Restrict user imports and exports to once every " %}</label>
138+
<label for="limit">{% trans "Limit how often users can import and export user data" %}</label>
112139
<input name="limit" class="input is-w-xs is-h-em" type="text" placeholder="0" value="{{ user_import_time_limit }}">
113140
<label>{% trans "hours" %}</label>
114141
{% csrf_token %}
@@ -120,6 +147,28 @@
120147
</div>
121148
</form>
122149
</details>
150+
{% else %}
151+
<form
152+
name="enable-user-imports"
153+
id="enable-user-imports"
154+
method="POST"
155+
action="{% url 'settings-user-exports-enable' %}"
156+
class="box"
157+
>
158+
<div class="notification is-danger is-light">
159+
<p class="my-2">{% trans "Users are currently unable to start new user exports. This is the default setting." %}</p>
160+
{% if use_s3 %}
161+
<p>{% trans "It is not currently possible to provide user exports when using s3 storage. The BookWyrm development team are working on a fix for this." %}</p>
162+
{% endif %}
163+
</div>
164+
{% csrf_token %}
165+
<div class="control">
166+
<button type="submit" class="button is-success" {% if use_s3 %}disabled{% endif %}>
167+
{% trans "Enable user exports" %}
168+
</button>
169+
</div>
170+
</form>
171+
{% endif %}
123172
</div>
124173
<div class="block">
125174
<h4 class="title is-4">{% trans "Book Imports" %}</h4>

bookwyrm/templatetags/utilities.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ def id_to_username(user_id):
125125
name = parts[-1]
126126
value = f"{name}@{domain}"
127127

128-
return value
128+
return value
129+
return "a new user account"
129130

130131

131132
@register.filter(name="get_file_size")

bookwyrm/tests/views/preferences/test_export.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ class ExportViews(TestCase):
1818
"""viewing and creating statuses"""
1919

2020
@classmethod
21-
def setUpTestData(self): # pylint: disable=bad-classmethod-argument
21+
def setUpTestData(
22+
self,
23+
): # pylint: disable=bad-classmethod-argument, disable=invalid-name
2224
"""we need basic test data and mocks"""
2325
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
2426
"bookwyrm.activitystreams.populate_stream_task.delay"
@@ -40,6 +42,7 @@ def setUpTestData(self): # pylint: disable=bad-classmethod-argument
4042
bnf_id="beep",
4143
)
4244

45+
# pylint: disable=invalid-name
4346
def setUp(self):
4447
"""individual test setup"""
4548
self.factory = RequestFactory()
@@ -53,11 +56,12 @@ def tst_export_get(self, *_):
5356

5457
def test_export_file(self, *_):
5558
"""simple export"""
56-
models.ShelfBook.objects.create(
59+
shelfbook = models.ShelfBook.objects.create(
5760
shelf=self.local_user.shelf_set.first(),
5861
user=self.local_user,
5962
book=self.book,
6063
)
64+
book_date = str.encode(f"{shelfbook.shelved_date.date()}")
6165
request = self.factory.post("")
6266
request.user = self.local_user
6367
export = views.Export.as_view()(request)
@@ -66,7 +70,7 @@ def test_export_file(self, *_):
6670
# pylint: disable=line-too-long
6771
self.assertEqual(
6872
export.content,
69-
b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,aasin,isfdb,isbn_10,isbn_13,oclc_number,rating,review_name,review_cw,review_content\r\nTest Book,,"
70-
+ self.book.remote_id.encode("utf-8")
71-
+ b",,,,,beep,,,,,,123456789X,9781234567890,,,,,\r\n",
73+
b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,aasin,isfdb,isbn_10,isbn_13,oclc_number,start_date,finish_date,stopped_date,rating,review_name,review_cw,review_content,review_published,shelf,shelf_name,shelf_date\r\n"
74+
+ b"Test Book,,%b,,,,,beep,,,,,,123456789X,9781234567890,,,,,,,,,,to-read,To Read,%b\r\n"
75+
% (self.book.remote_id.encode("utf-8"), book_date),
7276
)

bookwyrm/urls.py

+10
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,16 @@
338338
views.disable_imports,
339339
name="settings-imports-disable",
340340
),
341+
re_path(
342+
r"^settings/user-exports/enable/?$",
343+
views.enable_user_exports,
344+
name="settings-user-exports-enable",
345+
),
346+
re_path(
347+
r"^settings/user-exports/disable/?$",
348+
views.disable_user_exports,
349+
name="settings-user-exports-disable",
350+
),
341351
re_path(
342352
r"^settings/imports/enable/?$",
343353
views.enable_imports,

bookwyrm/views/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
set_import_size_limit,
1919
set_user_import_completed,
2020
set_user_import_limit,
21+
enable_user_exports,
22+
disable_user_exports,
2123
)
2224
from .admin.ip_blocklist import IPBlocklist
2325
from .admin.invite import ManageInvites, Invite, InviteRequest

bookwyrm/views/admin/imports.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from bookwyrm import models
1111
from bookwyrm.views.helpers import redirect_to_referer
12-
from bookwyrm.settings import PAGE_LENGTH
12+
from bookwyrm.settings import PAGE_LENGTH, USE_S3
1313

1414

1515
# pylint: disable=no-self-use
@@ -59,6 +59,7 @@ def get(self, request, status="active"):
5959
"import_size_limit": site_settings.import_size_limit,
6060
"import_limit_reset": site_settings.import_limit_reset,
6161
"user_import_time_limit": site_settings.user_import_time_limit,
62+
"use_s3": USE_S3,
6263
}
6364
return TemplateResponse(request, "settings/imports/imports.html", data)
6465

@@ -126,3 +127,25 @@ def set_user_import_limit(request):
126127
site.user_import_time_limit = int(request.POST.get("limit"))
127128
site.save(update_fields=["user_import_time_limit"])
128129
return redirect("settings-imports")
130+
131+
132+
@require_POST
133+
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
134+
# pylint: disable=unused-argument
135+
def enable_user_exports(request):
136+
"""Allow users to export account data"""
137+
site = models.SiteSettings.objects.get()
138+
site.user_exports_enabled = True
139+
site.save(update_fields=["user_exports_enabled"])
140+
return redirect("settings-imports")
141+
142+
143+
@require_POST
144+
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
145+
# pylint: disable=unused-argument
146+
def disable_user_exports(request):
147+
"""Don't allow users to export account data"""
148+
site = models.SiteSettings.objects.get()
149+
site.user_exports_enabled = False
150+
site.save(update_fields=["user_exports_enabled"])
151+
return redirect("settings-imports")

bookwyrm/views/preferences/export.py

+52-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
from bookwyrm.models.bookwyrm_export_job import BookwyrmExportJob
1818
from bookwyrm.settings import PAGE_LENGTH
1919

20-
# pylint: disable=no-self-use
20+
21+
# pylint: disable=no-self-use,too-many-locals
2122
@method_decorator(login_required, name="dispatch")
2223
class Export(View):
2324
"""Let users export data"""
@@ -54,7 +55,19 @@ def post(self, request):
5455
fields = (
5556
["title", "author_text"]
5657
+ deduplication_fields
57-
+ ["rating", "review_name", "review_cw", "review_content"]
58+
+ [
59+
"start_date",
60+
"finish_date",
61+
"stopped_date",
62+
"rating",
63+
"review_name",
64+
"review_cw",
65+
"review_content",
66+
"review_published",
67+
"shelf",
68+
"shelf_name",
69+
"shelf_date",
70+
]
5871
)
5972
writer.writerow(fields)
6073

@@ -70,6 +83,24 @@ def post(self, request):
7083

7184
book.rating = review_rating.rating if review_rating else None
7285

86+
readthrough = (
87+
models.ReadThrough.objects.filter(user=request.user, book=book)
88+
.order_by("-start_date", "-finish_date")
89+
.first()
90+
)
91+
if readthrough:
92+
book.start_date = (
93+
readthrough.start_date.date() if readthrough.start_date else None
94+
)
95+
book.finish_date = (
96+
readthrough.finish_date.date() if readthrough.finish_date else None
97+
)
98+
book.stopped_date = (
99+
readthrough.stopped_date.date()
100+
if readthrough.stopped_date
101+
else None
102+
)
103+
73104
review = (
74105
models.Review.objects.filter(
75106
user=request.user, book=book, content__isnull=False
@@ -78,9 +109,27 @@ def post(self, request):
78109
.first()
79110
)
80111
if review:
112+
book.review_published = (
113+
review.published_date.date() if review.published_date else None
114+
)
81115
book.review_name = review.name
82116
book.review_cw = review.content_warning
83-
book.review_content = review.raw_content
117+
book.review_content = (
118+
review.raw_content if review.raw_content else review.content
119+
) # GoodReads imported reviews do not have raw_content, but content.
120+
121+
shelfbook = (
122+
models.ShelfBook.objects.filter(user=request.user, book=book)
123+
.order_by("-shelved_date", "-created_date", "-updated_date")
124+
.last()
125+
)
126+
if shelfbook:
127+
book.shelf = shelfbook.shelf.identifier
128+
book.shelf_name = shelfbook.shelf.name
129+
book.shelf_date = (
130+
shelfbook.shelved_date.date() if shelfbook.shelved_date else None
131+
)
132+
84133
writer.writerow([getattr(book, field, "") or "" for field in fields])
85134

86135
return HttpResponse(

nginx/development

+6-1
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,18 @@ server {
6464
# directly serve images and static files from the
6565
# bookwyrm filesystem using sendfile.
6666
# make the logs quieter by not reporting these requests
67-
location ~ ^/(images|static)/ {
67+
location ~ \.(bmp|ico|jpg|jpeg|png|tif|tiff|webp|css|js)$ {
6868
root /app;
6969
try_files $uri =404;
7070
add_header X-Cache-Status STATIC;
7171
access_log off;
7272
}
7373

74+
# block access to any non-image files from images or static
75+
location ~ ^/images/ {
76+
return 403;
77+
}
78+
7479
# monitor the celery queues with flower, no caching enabled
7580
location /flower/ {
7681
proxy_pass http://flower:8888;

nginx/production

+6-1
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,17 @@ server {
9696
# # directly serve images and static files from the
9797
# # bookwyrm filesystem using sendfile.
9898
# # make the logs quieter by not reporting these requests
99-
# location ~ ^/(images|static)/ {
99+
# location ~ \.(bmp|ico|jpg|jpeg|png|tif|tiff|webp|css|js)$ {
100100
# root /app;
101101
# try_files $uri =404;
102102
# add_header X-Cache-Status STATIC;
103103
# access_log off;
104104
# }
105+
106+
# # block access to any non-image files from images or static
107+
# location ~ ^/images/ {
108+
# return 403;
109+
# }
105110
#
106111
# # monitor the celery queues with flower, no caching enabled
107112
# location /flower/ {

0 commit comments

Comments
 (0)