diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py
index 745aa3aab..d3aca4471 100644
--- a/bookwyrm/activitypub/book.py
+++ b/bookwyrm/activitypub/book.py
@@ -92,3 +92,4 @@ class Author(BookData):
bio: str = ""
wikipediaLink: str = ""
type: str = "Author"
+ website: str = ""
diff --git a/bookwyrm/forms/author.py b/bookwyrm/forms/author.py
index 3d71d4034..5b54a07b5 100644
--- a/bookwyrm/forms/author.py
+++ b/bookwyrm/forms/author.py
@@ -15,6 +15,7 @@ class AuthorForm(CustomForm):
"aliases",
"bio",
"wikipedia_link",
+ "website",
"born",
"died",
"openlibrary_key",
@@ -31,6 +32,7 @@ class AuthorForm(CustomForm):
"wikipedia_link": forms.TextInput(
attrs={"aria-describedby": "desc_wikipedia_link"}
),
+ "website": forms.TextInput(attrs={"aria-describedby": "desc_website"}),
"born": forms.SelectDateWidget(attrs={"aria-describedby": "desc_born"}),
"died": forms.SelectDateWidget(attrs={"aria-describedby": "desc_died"}),
"openlibrary_key": forms.TextInput(
diff --git a/bookwyrm/importers/importer.py b/bookwyrm/importers/importer.py
index e4ee2c31a..4c2abb521 100644
--- a/bookwyrm/importers/importer.py
+++ b/bookwyrm/importers/importer.py
@@ -1,7 +1,8 @@
""" handle reading a csv from an external service, defaults are from Goodreads """
import csv
+from datetime import timedelta
from django.utils import timezone
-from bookwyrm.models import ImportJob, ImportItem
+from bookwyrm.models import ImportJob, ImportItem, SiteSettings
class Importer:
@@ -33,6 +34,7 @@ class Importer:
"reading": ["currently-reading", "reading", "currently reading"],
}
+ # pylint: disable=too-many-locals
def create_job(self, user, csv_file, include_reviews, privacy):
"""check over a csv and creates a database entry for the job"""
csv_reader = csv.DictReader(csv_file, delimiter=self.delimiter)
@@ -49,7 +51,13 @@ class Importer:
source=self.service,
)
+ enforce_limit, allowed_imports = self.get_import_limit(user)
+ if enforce_limit and allowed_imports <= 0:
+ job.complete_job()
+ return job
for index, entry in rows:
+ if enforce_limit and index >= allowed_imports:
+ break
self.create_item(job, index, entry)
return job
@@ -99,6 +107,24 @@ class Importer:
"""use the dataclass to create the formatted row of data"""
return {k: entry.get(v) for k, v in mappings.items()}
+ def get_import_limit(self, user): # pylint: disable=no-self-use
+ """check if import limit is set and return how many imports are left"""
+ site_settings = SiteSettings.objects.get()
+ import_size_limit = site_settings.import_size_limit
+ import_limit_reset = site_settings.import_limit_reset
+ enforce_limit = import_size_limit and import_limit_reset
+ allowed_imports = 0
+
+ if enforce_limit:
+ time_range = timezone.now() - timedelta(days=import_limit_reset)
+ import_jobs = ImportJob.objects.filter(
+ user=user, created_date__gte=time_range
+ )
+ # pylint: disable=consider-using-generator
+ imported_books = sum([job.successful_item_count for job in import_jobs])
+ allowed_imports = import_size_limit - imported_books
+ return enforce_limit, allowed_imports
+
def create_retry_job(self, user, original_job, items):
"""retry items that didn't import"""
job = ImportJob.objects.create(
@@ -110,7 +136,13 @@ class Importer:
mappings=original_job.mappings,
retry=True,
)
- for item in items:
+ enforce_limit, allowed_imports = self.get_import_limit(user)
+ if enforce_limit and allowed_imports <= 0:
+ job.complete_job()
+ return job
+ for index, item in enumerate(items):
+ if enforce_limit and index >= allowed_imports:
+ break
# this will re-normalize the raw data
self.create_item(job, item.index, item.data)
return job
diff --git a/bookwyrm/migrations/0167_sitesettings_import_size_limit.py b/bookwyrm/migrations/0167_sitesettings_import_size_limit.py
new file mode 100644
index 000000000..fdbfaf51d
--- /dev/null
+++ b/bookwyrm/migrations/0167_sitesettings_import_size_limit.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.16 on 2022-12-05 13:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0166_sitesettings_imports_enabled"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="sitesettings",
+ name="import_size_limit",
+ field=models.IntegerField(default=0),
+ ),
+ migrations.AddField(
+ model_name="sitesettings",
+ name="import_limit_reset",
+ field=models.IntegerField(default=0),
+ ),
+ ]
diff --git a/bookwyrm/migrations/0171_merge_20221219_2020.py b/bookwyrm/migrations/0171_merge_20221219_2020.py
new file mode 100644
index 000000000..53d44872f
--- /dev/null
+++ b/bookwyrm/migrations/0171_merge_20221219_2020.py
@@ -0,0 +1,13 @@
+# Generated by Django 3.2.16 on 2022-12-19 20:20
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0167_sitesettings_import_size_limit"),
+ ("bookwyrm", "0170_merge_0168_auto_20221205_2331_0169_auto_20221206_0902"),
+ ]
+
+ operations = []
diff --git a/bookwyrm/migrations/0173_author_website.py b/bookwyrm/migrations/0173_author_website.py
new file mode 100644
index 000000000..fda3debf1
--- /dev/null
+++ b/bookwyrm/migrations/0173_author_website.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.2.16 on 2023-01-15 08:38
+
+import bookwyrm.models.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0172_alter_user_preferred_language"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="author",
+ name="website",
+ field=bookwyrm.models.fields.CharField(
+ blank=True, max_length=255, null=True
+ ),
+ ),
+ ]
diff --git a/bookwyrm/migrations/0173_merge_20230102_1444.py b/bookwyrm/migrations/0173_merge_20230102_1444.py
new file mode 100644
index 000000000..c3e37a76f
--- /dev/null
+++ b/bookwyrm/migrations/0173_merge_20230102_1444.py
@@ -0,0 +1,13 @@
+# Generated by Django 3.2.16 on 2023-01-02 14:44
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0171_merge_20221219_2020"),
+ ("bookwyrm", "0172_alter_user_preferred_language"),
+ ]
+
+ operations = []
diff --git a/bookwyrm/migrations/0174_merge_20230111_1523.py b/bookwyrm/migrations/0174_merge_20230111_1523.py
new file mode 100644
index 000000000..fd57083f6
--- /dev/null
+++ b/bookwyrm/migrations/0174_merge_20230111_1523.py
@@ -0,0 +1,12 @@
+# Generated by Django 3.2.16 on 2023-01-11 15:23
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0173_merge_20230102_1444"),
+ ]
+
+ operations = []
diff --git a/bookwyrm/migrations/0175_merge_0173_author_website_0174_merge_20230111_1523.py b/bookwyrm/migrations/0175_merge_0173_author_website_0174_merge_20230111_1523.py
new file mode 100644
index 000000000..a215076b4
--- /dev/null
+++ b/bookwyrm/migrations/0175_merge_0173_author_website_0174_merge_20230111_1523.py
@@ -0,0 +1,13 @@
+# Generated by Django 3.2.16 on 2023-01-19 20:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0173_author_website"),
+ ("bookwyrm", "0174_merge_20230111_1523"),
+ ]
+
+ operations = []
diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py
index b1d0510c9..5c0c087b2 100644
--- a/bookwyrm/models/author.py
+++ b/bookwyrm/models/author.py
@@ -25,6 +25,10 @@ class Author(BookDataModel):
isfdb = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
+
+ website = fields.CharField(
+ max_length=255, blank=True, null=True, deduplication_field=True
+ )
# idk probably other keys would be useful here?
born = fields.DateTimeField(blank=True, null=True)
died = fields.DateTimeField(blank=True, null=True)
diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py
index 533a37b30..747565161 100644
--- a/bookwyrm/models/site.py
+++ b/bookwyrm/models/site.py
@@ -90,6 +90,8 @@ class SiteSettings(SiteModel):
# controls
imports_enabled = models.BooleanField(default=True)
+ import_size_limit = models.IntegerField(default=0)
+ import_limit_reset = models.IntegerField(default=0)
field_tracker = FieldTracker(fields=["name", "instance_tagline", "logo"])
diff --git a/bookwyrm/static/css/bookwyrm/utilities/_size.scss b/bookwyrm/static/css/bookwyrm/utilities/_size.scss
index cbc74d7ab..258aa9a73 100644
--- a/bookwyrm/static/css/bookwyrm/utilities/_size.scss
+++ b/bookwyrm/static/css/bookwyrm/utilities/_size.scss
@@ -40,6 +40,10 @@
width: 500px !important;
}
+.is-h-em {
+ height: 1em !important;
+}
+
.is-h-xs {
height: 80px !important;
}
diff --git a/bookwyrm/templates/author/author.html b/bookwyrm/templates/author/author.html
index ade654568..909f2435c 100644
--- a/bookwyrm/templates/author/author.html
+++ b/bookwyrm/templates/author/author.html
@@ -28,7 +28,7 @@
{% firstof author.aliases author.born author.died as details %}
- {% firstof author.wikipedia_link author.openlibrary_key author.inventaire_id author.isni author.isfdb as links %}
+ {% firstof author.wikipedia_link author.website author.openlibrary_key author.inventaire_id author.isni author.isfdb as links %}
{% if details or links %}
{% if details %}
@@ -73,6 +73,14 @@
{% endif %}
+ {% if author.website %}
+
+ {% endif %}
+
{% if author.isni %}
-
+ {% if not import_limit_reset and not import_size_limit or allowed_imports > 0 %}
+
+ {% else %}
+
+ {% trans "You've reached the import limit." %}
+ {% endif%}
{% else %}
diff --git a/bookwyrm/templates/lists/layout.html b/bookwyrm/templates/lists/layout.html
index e61d72b56..7e7b9d074 100644
--- a/bookwyrm/templates/lists/layout.html
+++ b/bookwyrm/templates/lists/layout.html
@@ -1,8 +1,13 @@
{% extends 'layout.html' %}
{% load i18n %}
+{% load list_page_tags %}
{% block title %}{{ list.name }}{% endblock %}
+{% block opengraph %}
+ {% include 'snippets/opengraph.html' with title=list|opengraph_title description=list|opengraph_description %}
+{% endblock %}
+
{% block content %}
diff --git a/bookwyrm/templates/search/barcode_modal.html b/bookwyrm/templates/search/barcode_modal.html
index 519adfd3b..9a1f3b961 100644
--- a/bookwyrm/templates/search/barcode_modal.html
+++ b/bookwyrm/templates/search/barcode_modal.html
@@ -29,7 +29,7 @@
{% trans "Scanning..." context "barcode scanner" %}
- {% trans "Align your book's barcode with the camera." %}
+ {% trans "Align your book's barcode with the camera." %}
diff --git a/bookwyrm/templates/settings/imports/imports.html b/bookwyrm/templates/settings/imports/imports.html
index 135af34ed..108003d85 100644
--- a/bookwyrm/templates/settings/imports/imports.html
+++ b/bookwyrm/templates/settings/imports/imports.html
@@ -57,8 +57,39 @@
{% endif %}
+
+
+
+ {% trans "Limit the amount of imports" %}
+
+
+
+
+
-
diff --git a/bookwyrm/templates/user/user.html b/bookwyrm/templates/user/user.html
index 2e84b8ba2..6d194e90b 100755
--- a/bookwyrm/templates/user/user.html
+++ b/bookwyrm/templates/user/user.html
@@ -4,6 +4,10 @@
{% block title %}{{ user.display_name }}{% endblock %}
+{% block head_links %}
+
+{% endblock %}
+
{% block header %}
diff --git a/bookwyrm/templatetags/list_page_tags.py b/bookwyrm/templatetags/list_page_tags.py
new file mode 100644
index 000000000..c5445050f
--- /dev/null
+++ b/bookwyrm/templatetags/list_page_tags.py
@@ -0,0 +1,25 @@
+""" template filters for list page """
+from django import template
+from django.utils.translation import gettext_lazy as _, ngettext
+
+from bookwyrm import models
+
+
+register = template.Library()
+
+
+@register.filter(name="opengraph_title")
+def get_opengraph_title(book_list: models.List) -> str:
+ """Construct title for Open Graph"""
+ return _("Book List: %(name)s") % {"name": book_list.name}
+
+
+@register.filter(name="opengraph_description")
+def get_opengraph_description(book_list: models.List) -> str:
+ """Construct description for Open Graph"""
+ num_books = book_list.books.all().count()
+ num_books_str = ngettext(
+ "%(num)d book - by %(user)s", "%(num)d books - by %(user)s", num_books
+ ) % {"num": num_books, "user": book_list.user}
+
+ return f"{book_list.description} {num_books_str}"
diff --git a/bookwyrm/tests/importers/test_calibre_import.py b/bookwyrm/tests/importers/test_calibre_import.py
index 57c781b22..37b206458 100644
--- a/bookwyrm/tests/importers/test_calibre_import.py
+++ b/bookwyrm/tests/importers/test_calibre_import.py
@@ -28,7 +28,7 @@ class CalibreImport(TestCase):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True
)
-
+ models.SiteSettings.objects.create()
work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
diff --git a/bookwyrm/tests/importers/test_goodreads_import.py b/bookwyrm/tests/importers/test_goodreads_import.py
index 815166691..88f8eb3f4 100644
--- a/bookwyrm/tests/importers/test_goodreads_import.py
+++ b/bookwyrm/tests/importers/test_goodreads_import.py
@@ -35,7 +35,7 @@ class GoodreadsImport(TestCase):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True
)
-
+ models.SiteSettings.objects.create()
work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
diff --git a/bookwyrm/tests/importers/test_importer.py b/bookwyrm/tests/importers/test_importer.py
index ef7f2448b..51346f1a1 100644
--- a/bookwyrm/tests/importers/test_importer.py
+++ b/bookwyrm/tests/importers/test_importer.py
@@ -39,7 +39,7 @@ class GenericImporter(TestCase):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True
)
-
+ models.SiteSettings.objects.create()
work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
@@ -360,3 +360,16 @@ class GenericImporter(TestCase):
self.assertFalse(
models.Review.objects.filter(book=self.book, user=self.local_user).exists()
)
+
+ def test_import_limit(self, *_):
+ """checks if import limit works"""
+ site_settings = models.SiteSettings.objects.get()
+ site_settings.import_size_limit = 2
+ site_settings.import_limit_reset = 2
+ site_settings.save()
+
+ import_job = self.importer.create_job(
+ self.local_user, self.csv, False, "public"
+ )
+ import_items = models.ImportItem.objects.filter(job=import_job).all()
+ self.assertEqual(len(import_items), 2)
diff --git a/bookwyrm/tests/importers/test_librarything_import.py b/bookwyrm/tests/importers/test_librarything_import.py
index 3fe752b40..71a1c9796 100644
--- a/bookwyrm/tests/importers/test_librarything_import.py
+++ b/bookwyrm/tests/importers/test_librarything_import.py
@@ -37,6 +37,7 @@ class LibrarythingImport(TestCase):
self.local_user = models.User.objects.create_user(
"mmai", "mmai@mmai.mmai", "password", local=True
)
+ models.SiteSettings.objects.create()
work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
diff --git a/bookwyrm/tests/importers/test_openlibrary_import.py b/bookwyrm/tests/importers/test_openlibrary_import.py
index b91de1d1b..82b5ec3ea 100644
--- a/bookwyrm/tests/importers/test_openlibrary_import.py
+++ b/bookwyrm/tests/importers/test_openlibrary_import.py
@@ -35,7 +35,7 @@ class OpenLibraryImport(TestCase):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True
)
-
+ models.SiteSettings.objects.create()
work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
diff --git a/bookwyrm/tests/importers/test_storygraph_import.py b/bookwyrm/tests/importers/test_storygraph_import.py
index 16a8222ff..0befbeb3f 100644
--- a/bookwyrm/tests/importers/test_storygraph_import.py
+++ b/bookwyrm/tests/importers/test_storygraph_import.py
@@ -35,7 +35,7 @@ class StorygraphImport(TestCase):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True
)
-
+ models.SiteSettings.objects.create()
work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py
index b96ce1bcf..73bdd3755 100644
--- a/bookwyrm/urls.py
+++ b/bookwyrm/urls.py
@@ -321,6 +321,11 @@ urlpatterns = [
views.enable_imports,
name="settings-imports-enable",
),
+ re_path(
+ r"^settings/imports/set-limit/?$",
+ views.set_import_size_limit,
+ name="settings-imports-set-limit",
+ ),
re_path(
r"^settings/celery/?$", views.CeleryStatus.as_view(), name="settings-celery"
),
diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py
index 063371b7f..353275c46 100644
--- a/bookwyrm/views/__init__.py
+++ b/bookwyrm/views/__init__.py
@@ -11,7 +11,12 @@ from .admin.federation import AddFederatedServer, ImportServerBlocklist
from .admin.federation import block_server, unblock_server, refresh_server
from .admin.email_blocklist import EmailBlocklist
from .admin.email_config import EmailConfig
-from .admin.imports import ImportList, disable_imports, enable_imports
+from .admin.imports import (
+ ImportList,
+ disable_imports,
+ enable_imports,
+ set_import_size_limit,
+)
from .admin.ip_blocklist import IPBlocklist
from .admin.invite import ManageInvites, Invite, InviteRequest
from .admin.invite import ManageInviteRequests, ignore_invite_request
diff --git a/bookwyrm/views/admin/email_config.py b/bookwyrm/views/admin/email_config.py
index 474c3ea5a..03e85f8b0 100644
--- a/bookwyrm/views/admin/email_config.py
+++ b/bookwyrm/views/admin/email_config.py
@@ -39,7 +39,7 @@ def view_data():
"email_backend": settings.EMAIL_BACKEND,
"email_host": settings.EMAIL_HOST,
"email_port": settings.EMAIL_PORT,
- "Email_host_user": settings.EMAIL_HOST_USER,
+ "email_host_user": settings.EMAIL_HOST_USER,
"email_use_tls": settings.EMAIL_USE_TLS,
"email_use_ssl": settings.EMAIL_USE_SSL,
"email_sender": settings.EMAIL_SENDER,
diff --git a/bookwyrm/views/admin/imports.py b/bookwyrm/views/admin/imports.py
index fe04a0f2b..066bc42e4 100644
--- a/bookwyrm/views/admin/imports.py
+++ b/bookwyrm/views/admin/imports.py
@@ -38,6 +38,8 @@ class ImportList(View):
paginated = Paginator(imports, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
+
+ site_settings = models.SiteSettings.objects.get()
data = {
"imports": page,
"page_range": paginated.get_elided_page_range(
@@ -45,6 +47,8 @@ class ImportList(View):
),
"status": status,
"sort": sort,
+ "import_size_limit": site_settings.import_size_limit,
+ "import_limit_reset": site_settings.import_limit_reset,
}
return TemplateResponse(request, "settings/imports/imports.html", data)
@@ -76,3 +80,17 @@ def enable_imports(request):
site.imports_enabled = True
site.save(update_fields=["imports_enabled"])
return redirect("settings-imports")
+
+
+@require_POST
+@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
+# pylint: disable=unused-argument
+def set_import_size_limit(request):
+ """Limit the amount of books users can import at once"""
+ site = models.SiteSettings.objects.get()
+ import_size_limit = int(request.POST.get("limit"))
+ import_limit_reset = int(request.POST.get("reset"))
+ site.import_size_limit = import_size_limit
+ site.import_limit_reset = import_limit_reset
+ site.save(update_fields=["import_size_limit", "import_limit_reset"])
+ return redirect("settings-imports")
diff --git a/bookwyrm/views/imports/import_data.py b/bookwyrm/views/imports/import_data.py
index f0bb2e698..01812e1d5 100644
--- a/bookwyrm/views/imports/import_data.py
+++ b/bookwyrm/views/imports/import_data.py
@@ -51,6 +51,19 @@ class Import(View):
elif seconds:
data["recent_avg_minutes"] = seconds / 60
+ site_settings = models.SiteSettings.objects.get()
+ time_range = timezone.now() - datetime.timedelta(
+ days=site_settings.import_limit_reset
+ )
+ import_jobs = models.ImportJob.objects.filter(
+ user=request.user, created_date__gte=time_range
+ )
+ # pylint: disable=consider-using-generator
+ imported_books = sum([job.successful_item_count for job in import_jobs])
+ data["import_size_limit"] = site_settings.import_size_limit
+ data["import_limit_reset"] = site_settings.import_limit_reset
+ data["allowed_imports"] = site_settings.import_size_limit - imported_books
+
return TemplateResponse(request, "import/import.html", data)
def post(self, request):