Merge branch 'main' into authorized-fetch

This commit is contained in:
Mouse Reeve 2023-01-26 06:22:03 -08:00 committed by GitHub
commit f4de00088f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 685 additions and 27 deletions

View file

@ -92,3 +92,4 @@ class Author(BookData):
bio: str = ""
wikipediaLink: str = ""
type: str = "Author"
website: str = ""

View file

@ -91,6 +91,7 @@ class RegistrationForm(CustomForm):
"invite_request_question",
"invite_question_text",
"require_confirm_email",
"default_user_auth_group",
]
widgets = {

View file

@ -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(

View file

@ -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

View file

@ -117,10 +117,12 @@ def init_connectors():
def init_settings():
"""info about the instance"""
group_editor = Group.objects.filter(name="editor").first()
models.SiteSettings.objects.create(
support_link="https://www.patreon.com/bookwyrm",
support_title="Patreon",
install_mode=True,
default_user_auth_group=group_editor,
)

View file

@ -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),
),
]

View file

@ -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 = []

View file

@ -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
),
),
]

View file

@ -0,0 +1,34 @@
# Generated by Django 3.2.16 on 2022-12-27 21:34
from django.db import migrations, models
import django.db.models.deletion
def backfill_sitesettings(apps, schema_editor):
db_alias = schema_editor.connection.alias
group_model = apps.get_model("auth", "Group")
editor_group = group_model.objects.using(db_alias).filter(name="editor").first()
sitesettings_model = apps.get_model("bookwyrm", "SiteSettings")
sitesettings_model.objects.update(default_user_auth_group=editor_group)
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0175_merge_0173_author_website_0174_merge_20230111_1523"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="default_user_auth_group",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.RESTRICT,
to="auth.group",
),
),
migrations.RunPython(backfill_sitesettings, migrations.RunPython.noop),
]

View file

@ -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 = []

View file

@ -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 = []

View file

@ -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 = []

View file

@ -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)

View file

@ -3,6 +3,7 @@ import datetime
from urllib.parse import urljoin
import uuid
import django.contrib.auth.models as auth_models
from django.core.exceptions import PermissionDenied
from django.db import models, IntegrityError
from django.dispatch import receiver
@ -70,6 +71,9 @@ class SiteSettings(SiteModel):
allow_invite_requests = models.BooleanField(default=True)
invite_request_question = models.BooleanField(default=False)
require_confirm_email = models.BooleanField(default=True)
default_user_auth_group = models.ForeignKey(
auth_models.Group, null=True, blank=True, on_delete=models.PROTECT
)
invite_question_text = models.CharField(
max_length=255, blank=True, default="What is your favourite book?"
@ -90,6 +94,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"])

View file

@ -3,9 +3,9 @@ import re
from urllib.parse import urlparse
from django.apps import apps
from django.contrib.auth.models import AbstractUser, Group
from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import ArrayField, CICharField
from django.core.exceptions import PermissionDenied
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
from django.dispatch import receiver
from django.db import models, transaction
from django.utils import timezone
@ -356,8 +356,14 @@ class User(OrderedCollectionPageMixin, AbstractUser):
# make users editors by default
try:
self.groups.add(Group.objects.get(name="editor"))
except Group.DoesNotExist:
group = (
apps.get_model("bookwyrm.SiteSettings")
.objects.get()
.default_user_auth_group
)
if group:
self.groups.add(group)
except ObjectDoesNotExist:
# this should only happen in tests
pass

View file

@ -40,6 +40,10 @@
width: 500px !important;
}
.is-h-em {
height: 1em !important;
}
.is-h-xs {
height: 80px !important;
}

View file

@ -28,7 +28,7 @@
<meta itemprop="name" content="{{ author.name }}">
{% 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 %}
<div class="column is-3">
{% if details %}
@ -73,6 +73,14 @@
</div>
{% endif %}
{% if author.website %}
<div>
<a itemprop="sameAs" href="{{ author.website }}" rel="nofollow noopener noreferrer" target="_blank">
{% trans "Website" %}
</a>
</div>
{% endif %}
{% if author.isni %}
<div class="mt-1">
<a itemprop="sameAs" href="{{ author.isni_link }}" rel="nofollow noopener noreferrer" target="_blank">

View file

@ -57,6 +57,10 @@
{% include 'snippets/form_errors.html' with errors_list=form.wikipedia_link.errors id="desc_wikipedia_link" %}
<p class="field"><label class="label" for="id_website">{% trans "Website:" %}</label> {{ form.website }}</p>
{% include 'snippets/form_errors.html' with errors_list=form.website.errors id="desc_website" %}
<div class="field">
<label class="label" for="id_born">{% trans "Birth date:" %}</label>
<input type="date" name="born" value="{{ form.born.value|date:'Y-m-d' }}" class="input" id="id_born">

View file

@ -8,7 +8,7 @@
{% block title %}{{ book|book_title }}{% endblock %}
{% block opengraph %}
{% include 'snippets/opengraph.html' with image=book.preview_image %}
{% include 'snippets/opengraph.html' with title=book.title description=book|book_description image=book.preview_image %}
{% endblock %}
{% block content %}

View file

@ -15,6 +15,12 @@
{% endif %}
{% if site.imports_enabled %}
{% if import_size_limit and import_limit_reset %}
<div class="notification">
<p>{% blocktrans %}Currently you are allowed to import {{ import_size_limit }} books every {{ import_limit_reset }} days.{% endblocktrans %}</p>
<p>{% blocktrans %}You have {{ allowed_imports }} left.{% endblocktrans %}</p>
</div>
{% endif %}
{% if recent_avg_hours or recent_avg_minutes %}
<div class="notification">
<p>
@ -90,7 +96,12 @@
</div>
</div>
</div>
<button class="button is-primary" type="submit">{% trans "Import" %}</button>
{% if not import_limit_reset and not import_size_limit or allowed_imports > 0 %}
<button class="button is-primary" type="submit">{% trans "Import" %}</button>
{% else %}
<button class="button is-primary is-disabled" type="submit">{% trans "Import" %}</button>
<p>{% trans "You've reached the import limit." %}</p>
{% endif%}
</form>
{% else %}
<div class="box notification has-text-centered is-warning m-6 content">

View file

@ -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 %}
<header class="columns content is-mobile">
<div class="column">

View file

@ -29,7 +29,7 @@
<template id="barcode-scanning">
<span class="icon icon-barcode"></span>
<span class="is-size-5">{% trans "Scanning..." context "barcode scanner" %}</span><br/>
<span>{% trans "Align your book's barcode with the camera." %}</span>
<span>{% trans "Align your book's barcode with the camera." %}</span><span class="isbn"></span>
</template>
<template id="barcode-found">
<span class="icon icon-check"></span>

View file

@ -57,8 +57,39 @@
</div>
</form>
{% endif %}
<details class="details-panel box">
<summary>
<span role="heading" aria-level="2" class="title is-6">
{% trans "Limit the amount of imports" %}
</span>
<span class="details-close icon icon-x" aria-hidden="true"></span>
</summary>
<form
name="imports-set-limit"
id="imports-set-limit"
method="POST"
action="{% url 'settings-imports-set-limit' %}"
>
<div class="notification">
{% trans "Some users might try to import a large number of books, which you want to limit." %}
{% trans "Set the value to 0 to not enforce any limit." %}
</div>
<div class="align.to-t">
<label for="limit">{% trans "Set import limit to" %}</label>
<input name="limit" class="input is-w-xs is-h-em" type="text" placeholder="0" value="{{ import_size_limit }}">
<label for="reset">{% trans "books every" %}</label>
<input name="reset" class="input is-w-xs is-h-em" type="text" placeholder="0" value="{{ import_limit_reset }}">
<label>{% trans "days." %}</label>
{% csrf_token %}
<div class="control">
<button type="submit" class="button is-warning">
{% trans "Set limit" %}
</button>
</div>
</div>
</form>
</details>
</div>
<div class="block">
<div class="tabs">
<ul>

View file

@ -38,6 +38,23 @@
{% trans "Allow registration" %}
</label>
</div>
<div class="block">
<label class="label" for="id_default_user_auth_group">
{% trans "Default access level:" %}
</label>
<div class="select">
<select name="default_user_auth_group" id="id_default_user_auth_group" desc_default_user_auth_group="desc_default_user_auth_group">
{% for instance in form.default_user_auth_group.field.queryset %}
<option value="{{ instance.pk }}" {% if instance.pk == form.default_user_auth_group.value %}selected{% endif %}>
{{ instance.name|title }}
</option>
{% endfor %}
<option value="" {% if not form.default_user_auth_group.value %}selected{% endif %}>
User
</option>
</select>
</div>
</div>
<div class="field">
<label class="label mb-0" for="id_require_confirm_email">
{{ form.require_confirm_email }}

View file

@ -4,6 +4,10 @@
{% block title %}{{ user.display_name }}{% endblock %}
{% block head_links %}
<link rel="alternate" type="application/rss+xml" href="{{ user.local_path }}/rss" title="{{ user.display_name }} - {{ site.name }}" />
{% endblock %}
{% block header %}
<div class="columns is-mobile">
<div class="column">
@ -66,10 +70,52 @@
<h2 class="title column">{% trans "User Activity" %}</h2>
{% if user.local %}
<div class="column is-narrow">
<a target="_blank" href="{{ user.local_path }}/rss" rel="nofollow noopener noreferrer">
<span class="icon icon-rss" aria-hidden="true"></span>
<span class="is-hidden-mobile">{% trans "RSS feed" %}</span>
</a>
<details class="dropdown">
<summary
class="is-relative pulldown-menu dropdown-trigger"
aria-label="{% trans 'Show RSS Options' %}"
role="button"
aria-haspopup="menu"
>
<span class="">
<span class="icon icon-rss" aria-hidden="true"></span>
<span class="">{% trans "RSS feed" %}</span>
</span>
<span class="icon icon-arrow-down is-hidden-mobile" aria-hidden="true"></span>
<span class="summary-on-open">
<span class="icon icon-arrow-left is-small" aria-hidden="true"></span>
{% trans "Back" %}
</span>
</summary>
<div class="dropdown-menu">
<ul
class="dropdown-content"
role="menu"
>
<li role="menuitem">
<a target="_blank" href="{{ user.local_path }}/rss" class="navbar-item" rel="nofollow noopener noreferrer">
{% trans "Complete feed" %}
</a>
</li>
<li role="menuitem">
<a target="_blank" href="{{ user.local_path }}/rss-reviews" class="navbar-item" rel="nofollow noopener noreferrer">
{% trans "Reviews only" %}
</a>
</li>
<li role="menuitem">
<a target="_blank" href="{{ user.local_path }}/rss-quotes" class="navbar-item" rel="nofollow noopener noreferrer">
{% trans "Quotes only" %}
</a>
</li>
<li role="menuitem">
<a target="_blank" href="{{ user.local_path }}/rss-comments" class="navbar-item" rel="nofollow noopener noreferrer">
{% trans "Comments only" %}
</a>
</li>
</ul>
</div>
</details>
</div>
{% endif %}
</div>

View file

@ -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}"

View file

@ -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",

View file

@ -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",

View file

@ -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)

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -63,9 +63,19 @@ class InitDB(TestCase):
def test_init_settings(self):
"""Create the settings file"""
initdb.init_groups()
group_editor = Group.objects.get(name="editor")
initdb.init_settings()
settings = models.SiteSettings.objects.get()
self.assertEqual(settings.name, "BookWyrm")
self.assertEqual(settings.default_user_auth_group, group_editor)
def test_init_settings_without_groups(self):
"""Create the settings, but without groups existing already"""
initdb.init_settings()
settings = models.SiteSettings.objects.get()
self.assertIsNone(settings.default_user_auth_group)
def test_init_link_domains(self):
"""Common trusted domains for links"""

View file

@ -109,6 +109,36 @@ class User(TestCase):
self.assertEqual(activity["id"], self.user.outbox)
self.assertEqual(activity["totalItems"], 0)
def test_save_auth_group(self):
user_attrs = {"local": True}
site = models.SiteSettings.get()
site.default_user_auth_group = Group.objects.get(name="editor")
site.save()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
user = models.User.objects.create_user(
f"test2{DOMAIN}",
"test2@bookwyrm.test",
localname="test2",
**user_attrs,
)
self.assertEqual(list(user.groups.all()), [Group.objects.get(name="editor")])
site.default_user_auth_group = None
site.save()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
user = models.User.objects.create_user(
f"test1{DOMAIN}",
"test1@bookwyrm.test",
localname="test1",
**user_attrs,
)
self.assertEqual(len(user.groups.all()), 0)
def test_set_remote_server(self):
server = models.FederatedServer.objects.create(
server_name=DOMAIN, application_type="test type", application_version=3

View file

@ -35,6 +35,7 @@ Sender = namedtuple("Sender", ("remote_id", "key_pair"))
class Signature(TestCase):
"""signature test"""
# pylint: disable=invalid-name
def setUp(self):
"""create users and test data"""
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
@ -88,7 +89,6 @@ class Signature(TestCase):
signature = make_signature(
"post", signer or sender, self.rat.inbox, now, digest
)
with patch("bookwyrm.views.inbox.activity_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"):
return self.send(signature, now, send_data or data, digest)

View file

@ -1,6 +1,7 @@
""" test for app action functionality """
from unittest.mock import patch
import responses
from responses import matchers
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
@ -45,6 +46,44 @@ class EditBookViews(TestCase):
remote_id="https://example.com/book/1",
parent_work=self.work,
)
# pylint: disable=line-too-long
self.authors_body = "<?xml version='1.0' encoding='UTF-8' ?><?xml-stylesheet type='text/xsl' href='http://isni.oclc.org/sru/DB=1.2/?xsl=searchRetrieveResponse' ?><srw:searchRetrieveResponse xmlns:srw='http://www.loc.gov/zing/srw/' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:diag='http://www.loc.gov/zing/srw/diagnostic/' xmlns:xcql='http://www.loc.gov/zing/cql/xcql/'><srw:version>1.1</srw:version><srw:records><srw:record><isniUnformatted>0000000084510024</isniUnformatted></srw:record></srw:records></srw:searchRetrieveResponse>"
# pylint: disable=line-too-long
self.author_body = "<?xml version='1.0' encoding='UTF-8' ?><?xml-stylesheet type='text/xsl' href='http://isni.oclc.org/sru/DB=1.2/?xsl=searchRetrieveResponse' ?><srw:searchRetrieveResponse xmlns:srw='http://www.loc.gov/zing/srw/' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:diag='http://www.loc.gov/zing/srw/diagnostic/' xmlns:xcql='http://www.loc.gov/zing/cql/xcql/'><srw:records><srw:record><srw:recordData><responseRecord><ISNIAssigned><isniUnformatted>0000000084510024</isniUnformatted><isniURI>https://isni.org/isni/0000000084510024</isniURI><dataConfidence>60</dataConfidence><ISNIMetadata><identity><personOrFiction><personalName><surname>Catherine Amy Dawson Scott</surname><nameTitle>poet and novelist</nameTitle><nameUse>public</nameUse><source>VIAF</source><source>WKP</source><subsourceIdentifier>Q544961</subsourceIdentifier></personalName><personalName><forename>C. A.</forename><surname>Dawson Scott</surname><marcDate>1865-1934</marcDate><nameUse>public</nameUse><source>VIAF</source><source>NLP</source><subsourceIdentifier>a28927850</subsourceIdentifier></personalName><sources><codeOfSource>VIAF</codeOfSource><sourceIdentifier>45886165</sourceIdentifier><reference><class>ALL</class><role>CRE</role><URI>http://viaf.org/viaf/45886165</URI></reference></sources><externalInformation><information>Wikipedia</information><URI>https://en.wikipedia.org/wiki/Catherine_Amy_Dawson_Scott</URI></externalInformation></ISNIMetadata></ISNIAssigned></responseRecord></srw:recordData></srw:record></srw:records></srw:searchRetrieveResponse>"
responses.get(
"http://isni.oclc.org/sru/",
content_type="text/xml",
match=[
matchers.query_param_matcher(
{"query": 'pica.na="Sappho"'}, strict_match=False
)
],
body=self.authors_body,
)
responses.get(
"http://isni.oclc.org/sru/",
content_type="text/xml",
match=[
matchers.query_param_matcher(
{"query": 'pica.na="Some Guy"'}, strict_match=False
)
],
body=self.authors_body,
)
responses.get(
"http://isni.oclc.org/sru/",
content_type="text/xml",
match=[
matchers.query_param_matcher(
{"query": 'pica.isn="0000000084510024"'}, strict_match=False
)
],
body=self.author_body,
)
models.SiteSettings.objects.create()
@ -97,6 +136,7 @@ class EditBookViews(TestCase):
result.context_data["cover_url"], "http://local.host/cover.jpg"
)
@responses.activate
def test_edit_book_add_author(self):
"""lets a user edit a book with new authors"""
view = views.EditBook.as_view()
@ -227,6 +267,7 @@ class EditBookViews(TestCase):
self.book.refresh_from_db()
self.assertTrue(self.book.cover)
@responses.activate
def test_add_authors_helper(self):
"""converts form input into author matches"""
form = forms.EditionForm(instance=self.book)

View file

@ -15,6 +15,7 @@ from bookwyrm import models, views
class Inbox(TestCase):
"""readthrough tests"""
# pylint: disable=invalid-name
def setUp(self):
"""basic user and book data"""
self.client = Client()
@ -119,7 +120,7 @@ class Inbox(TestCase):
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = True
with patch("bookwyrm.views.inbox.activity_task.delay"):
with patch("bookwyrm.views.inbox.activity_task.apply_async"):
result = self.client.post(
"/inbox", json.dumps(activity), content_type="application/json"
)

View file

@ -82,3 +82,48 @@ class RssFeedView(TestCase):
self.assertEqual(result.status_code, 200)
self.assertIn(b"a sickening sense", result.content)
def test_rss_comment_only(self, *_):
"""load an rss feed"""
models.Comment.objects.create(
content="comment test content",
user=self.local_user,
book=self.book,
)
view = rss_feed.RssCommentsOnlyFeed()
request = self.factory.get("/user/rss_user/rss")
request.user = self.local_user
result = view(request, username=self.local_user.username)
self.assertEqual(result.status_code, 200)
self.assertIn(b"Example Edition", result.content)
def test_rss_review_only(self, *_):
"""load an rss feed"""
models.Review.objects.create(
name="Review name",
content="test content",
rating=3,
user=self.local_user,
book=self.book,
)
view = rss_feed.RssReviewsOnlyFeed()
request = self.factory.get("/user/rss_user/rss")
request.user = self.local_user
result = view(request, username=self.local_user.username)
self.assertEqual(result.status_code, 200)
def test_rss_quotation_only(self, *_):
"""load an rss feed"""
models.Quotation.objects.create(
quote="a sickening sense",
content="test content",
user=self.local_user,
book=self.book,
)
view = rss_feed.RssQuotesOnlyFeed()
request = self.factory.get("/user/rss_user/rss")
request.user = self.local_user
result = view(request, username=self.local_user.username)
self.assertEqual(result.status_code, 200)
self.assertIn(b"a sickening sense", result.content)

View file

@ -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"
),
@ -419,6 +424,21 @@ urlpatterns = [
re_path(rf"{USER_PATH}/?$", views.User.as_view(), name="user-feed"),
re_path(rf"^@(?P<username>{regex.USERNAME})$", views.user_redirect),
re_path(rf"{USER_PATH}/rss/?$", views.rss_feed.RssFeed(), name="user-rss"),
re_path(
rf"{USER_PATH}/rss-reviews/?$",
views.rss_feed.RssReviewsOnlyFeed(),
name="user-reviews-rss",
),
re_path(
rf"{USER_PATH}/rss-quotes/?$",
views.rss_feed.RssQuotesOnlyFeed(),
name="user-quotes-rss",
),
re_path(
rf"{USER_PATH}/rss-comments/?$",
views.rss_feed.RssCommentsOnlyFeed(),
name="user-comments-rss",
),
re_path(
rf"{USER_PATH}/(?P<direction>(followers|following))(.json)?/?$",
views.Relationships.as_view(),

View file

@ -85,6 +85,9 @@ def find_authors_by_name(name_string, description=False):
# build list of possible authors
possible_authors = []
for element in root.iter("responseRecord"):
# TODO: we don't seem to do anything with the
# personal_name variable - is this code block needed?
personal_name = element.find(".//forename/..")
if not personal_name:
continue

View file

@ -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
@ -132,7 +137,12 @@ from .outbox import Outbox
from .reading import ReadThrough, delete_readthrough, delete_progressupdate
from .reading import ReadingStatus
from .report import Report
from .rss_feed import RssFeed
from .rss_feed import (
RssFeed,
RssReviewsOnlyFeed,
RssQuotesOnlyFeed,
RssCommentsOnlyFeed,
)
from .search import Search
from .setup import InstanceConfig, CreateAdmin
from .status import CreateStatus, EditStatus, DeleteStatus, update_progress

View file

@ -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,

View file

@ -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")

View file

@ -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):

View file

@ -14,7 +14,7 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from bookwyrm import activitypub, models
from bookwyrm.tasks import app, MEDIUM
from bookwyrm.tasks import app, MEDIUM, HIGH
from bookwyrm.signatures import Signature
from bookwyrm.utils import regex
@ -60,7 +60,11 @@ class Inbox(View):
return HttpResponse()
return HttpResponse(status=401)
activity_task.delay(activity_json)
# Make activities relating to follow/unfollow a high priority
high = ["Follow", "Accept", "Reject", "Block", "Unblock", "Undo"]
priority = HIGH if activity_json["type"] in high else MEDIUM
activity_task.apply_async(args=(activity_json,), queue=priority)
return HttpResponse()

View file

@ -3,6 +3,7 @@
from django.contrib.syndication.views import Feed
from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _
from ..models import Review, Quotation, Comment
from .helpers import get_user_from_username
@ -42,3 +43,117 @@ class RssFeed(Feed):
def item_link(self, item):
"""link to the status"""
return item.local_path
class RssReviewsOnlyFeed(Feed):
"""serialize user's reviews in rss feed"""
description_template = "rss/content.html"
def item_title(self, item):
"""render the item title"""
if hasattr(item, "pure_name") and item.pure_name:
return item.pure_name
title_template = get_template("snippets/status/header_content.html")
title = title_template.render({"status": item})
template = get_template("rss/title.html")
return template.render({"user": item.user, "item_title": title}).strip()
def get_object(self, request, username): # pylint: disable=arguments-differ
"""the user who's posts get serialized"""
return get_user_from_username(request.user, username)
def link(self, obj):
"""link to the user's profile"""
return obj.local_path
def title(self, obj):
"""title of the rss feed entry"""
return _(f"Reviews from {obj.display_name}")
def items(self, obj):
"""the user's activity feed"""
return Review.objects.filter(
user=obj,
privacy__in=["public", "unlisted"],
)[:10]
def item_link(self, item):
"""link to the status"""
return item.local_path
class RssQuotesOnlyFeed(Feed):
"""serialize user's quotes in rss feed"""
description_template = "rss/content.html"
def item_title(self, item):
"""render the item title"""
if hasattr(item, "pure_name") and item.pure_name:
return item.pure_name
title_template = get_template("snippets/status/header_content.html")
title = title_template.render({"status": item})
template = get_template("rss/title.html")
return template.render({"user": item.user, "item_title": title}).strip()
def get_object(self, request, username): # pylint: disable=arguments-differ
"""the user who's posts get serialized"""
return get_user_from_username(request.user, username)
def link(self, obj):
"""link to the user's profile"""
return obj.local_path
def title(self, obj):
"""title of the rss feed entry"""
return _(f"Quotes from {obj.display_name}")
def items(self, obj):
"""the user's activity feed"""
return Quotation.objects.filter(
user=obj,
privacy__in=["public", "unlisted"],
)[:10]
def item_link(self, item):
"""link to the status"""
return item.local_path
class RssCommentsOnlyFeed(Feed):
"""serialize user's quotes in rss feed"""
description_template = "rss/content.html"
def item_title(self, item):
"""render the item title"""
if hasattr(item, "pure_name") and item.pure_name:
return item.pure_name
title_template = get_template("snippets/status/header_content.html")
title = title_template.render({"status": item})
template = get_template("rss/title.html")
return template.render({"user": item.user, "item_title": title}).strip()
def get_object(self, request, username): # pylint: disable=arguments-differ
"""the user who's posts get serialized"""
return get_user_from_username(request.user, username)
def link(self, obj):
"""link to the user's profile"""
return obj.local_path
def title(self, obj):
"""title of the rss feed entry"""
return _(f"Comments from {obj.display_name}")
def items(self, obj):
"""the user's activity feed"""
return Comment.objects.filter(
user=obj,
privacy__in=["public", "unlisted"],
)[:10]
def item_link(self, item):
"""link to the status"""
return item.local_path