Merge branch 'main' into frontend

This commit is contained in:
Fabien Basmaison 2021-04-08 09:53:01 +02:00
commit ad3e91db7d
13 changed files with 119 additions and 33 deletions

View file

@ -8,6 +8,6 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: psf/black@20.8b1
- uses: psf/black@stable
with:
args: ". --check -l 80 -S"

View file

@ -23,6 +23,7 @@ class Person(ActivityObject):
inbox: str
publicKey: PublicKey
followers: str = None
following: str = None
outbox: str = None
endpoints: Dict = None
name: str = None

View file

@ -179,7 +179,11 @@ class AbstractConnector(AbstractMinimalConnector):
data = get_data(remote_id)
mapped_data = dict_from_mappings(data, self.author_mappings)
activity = activitypub.Author(**mapped_data)
try:
activity = activitypub.Author(**mapped_data)
except activitypub.ActivitySerializerError:
return None
# this will dedupe
return activity.to_model(model=models.Author)

View file

@ -1,5 +1,6 @@
""" interface with whatever connectors the app has """
import importlib
import logging
import re
from urllib.parse import urlparse
@ -11,6 +12,8 @@ from requests import HTTPError
from bookwyrm import models
from bookwyrm.tasks import app
logger = logging.getLogger(__name__)
class ConnectorException(HTTPError):
""" when the connector can't do what was asked """
@ -37,14 +40,17 @@ def search(query, min_confidence=0.1):
else:
try:
result_set = connector.isbn_search(isbn)
except (HTTPError, ConnectorException):
pass
except Exception as e: # pylint: disable=broad-except
logger.exception(e)
continue
# if no isbn search or results, we fallback to generic search
if result_set in (None, []):
try:
result_set = connector.search(query, min_confidence=min_confidence)
except (HTTPError, ConnectorException):
except Exception as e: # pylint: disable=broad-except
# we don't want *any* error to crash the whole search page
logger.exception(e)
continue
# if the search results look the same, ignore them

View file

@ -93,7 +93,10 @@ class Connector(AbstractConnector):
# this id is "/authors/OL1234567A"
author_id = author_blob["key"]
url = "%s%s" % (self.base_url, author_id)
yield self.get_or_create_author(url)
author = self.get_or_create_author(url)
if not author:
continue
yield author
def get_cover_url(self, cover_blob, size="L"):
""" ask openlibrary for the cover """

View file

@ -0,0 +1,33 @@
# Generated by Django 3.1.6 on 2021-04-07 15:45
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0061_auto_20210402_1435"),
]
operations = [
migrations.AlterField(
model_name="book",
name="series",
field=bookwyrm.models.fields.TextField(
blank=True, max_length=255, null=True
),
),
migrations.AlterField(
model_name="book",
name="subtitle",
field=bookwyrm.models.fields.TextField(
blank=True, max_length=255, null=True
),
),
migrations.AlterField(
model_name="book",
name="title",
field=bookwyrm.models.fields.TextField(max_length=255),
),
]

View file

@ -1,5 +1,6 @@
""" activitypub model functionality """
from base64 import b64encode
from collections import namedtuple
from functools import reduce
import json
import operator
@ -25,6 +26,15 @@ from bookwyrm.models.fields import ImageField, ManyToManyField
logger = logging.getLogger(__name__)
# I tried to separate these classes into mutliple files but I kept getting
# circular import errors so I gave up. I'm sure it could be done though!
PropertyField = namedtuple("PropertyField", ("set_activity_from_field"))
def set_activity_from_property_field(activity, obj, field):
""" assign a model property value to the activity json """
activity[field[1]] = getattr(obj, field[0])
class ActivitypubMixin:
""" add this mixin for models that are AP serializable """
@ -52,6 +62,11 @@ class ActivitypubMixin:
self.activity_fields = (
self.image_fields + self.many_to_many_fields + self.simple_fields
)
if hasattr(self, "property_fields"):
self.activity_fields += [
PropertyField(lambda a, o: set_activity_from_property_field(a, o, f))
for f in self.property_fields
]
# these are separate to avoid infinite recursion issues
self.deserialize_reverse_fields = (
@ -370,7 +385,7 @@ class CollectionItemMixin(ActivitypubMixin):
object_field = getattr(self, self.object_field)
collection_field = getattr(self, self.collection_field)
return activitypub.Add(
id=self.remote_id,
id=self.get_remote_id(),
actor=self.user.remote_id,
object=object_field,
target=collection_field.remote_id,
@ -381,7 +396,7 @@ class CollectionItemMixin(ActivitypubMixin):
object_field = getattr(self, self.object_field)
collection_field = getattr(self, self.collection_field)
return activitypub.Remove(
id=self.remote_id,
id=self.get_remote_id(),
actor=self.user.remote_id,
object=object_field,
target=collection_field.remote_id,
@ -430,7 +445,7 @@ def generate_activity(obj):
) in obj.serialize_reverse_fields:
related_field = getattr(obj, model_field_name)
activity[activity_field_name] = unfurl_related_field(
related_field, sort_field
related_field, sort_field=sort_field
)
if not activity.get("id"):
@ -440,7 +455,7 @@ def generate_activity(obj):
def unfurl_related_field(related_field, sort_field=None):
""" load reverse lookups (like public key owner or Status attachment """
if hasattr(related_field, "all"):
if sort_field and hasattr(related_field, "all"):
return [
unfurl_related_field(i) for i in related_field.order_by(sort_field).all()
]

View file

@ -53,14 +53,14 @@ class Book(BookDataModel):
connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True)
# book/work metadata
title = fields.CharField(max_length=255)
title = fields.TextField(max_length=255)
sort_title = fields.CharField(max_length=255, blank=True, null=True)
subtitle = fields.CharField(max_length=255, blank=True, null=True)
subtitle = fields.TextField(max_length=255, blank=True, null=True)
description = fields.HtmlField(blank=True, null=True)
languages = fields.ArrayField(
models.CharField(max_length=255), blank=True, default=list
)
series = fields.CharField(max_length=255, blank=True, null=True)
series = fields.TextField(max_length=255, blank=True, null=True)
series_number = fields.CharField(max_length=255, blank=True, null=True)
subjects = fields.ArrayField(
models.CharField(max_length=255), blank=True, null=True, default=list

View file

@ -112,6 +112,12 @@ class User(OrderedCollectionPageMixin, AbstractUser):
)
name_field = "username"
property_fields = [("following_link", "following")]
@property
def following_link(self):
""" just how to find out the following info """
return "{:s}/following".format(self.remote_id)
@property
def alt_text(self):

View file

@ -88,12 +88,18 @@
<div class="column is-half">
<section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2>
<p class="mb-2"><label class="label" for="id_title">{% trans "Title:" %}</label> {{ form.title }} </p>
<p class="mb-2">
<label class="label" for="id_title">{% trans "Title:" %}</label>
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
</p>
{% for error in form.title.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label> {{ form.subtitle }} </p>
<p class="mb-2">
<label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label>
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" required="" id="id_subtitle">
</p>
{% for error in form.subtitle.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
@ -124,7 +130,7 @@
<p class="mb-2">
<label class="label" for="id_first_published_date">{% trans "First published date:" %}</label>
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if book.first_published_date %} value="{{ book.first_published_date|date:'Y-m-d' }}"{% endif %}>
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %}>
</p>
{% for error in form.first_published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>
@ -132,7 +138,7 @@
<p class="mb-2">
<label class="label" for="id_published_date">{% trans "Published date:" %}</label>
<input type="date" name="published_date" class="input" id="id_published_date"{% if book.published_date %} value="{{ book.published_date|date:'Y-m-d' }}"{% endif %}>
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %}>
</p>
{% for error in form.published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>

View file

@ -1,6 +1,8 @@
""" the good stuff! the books! """
from datetime import datetime
from uuid import uuid4
from dateutil.parser import parse as dateparse
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import SearchRank, SearchVector
from django.core.files.base import ContentFile
@ -10,6 +12,7 @@ from django.db.models import Avg, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
@ -172,6 +175,20 @@ class EditBook(View):
data["confirm_mode"] = True
# this isn't preserved because it isn't part of the form obj
data["remove_authors"] = request.POST.getlist("remove_authors")
# we have to make sure the dates are passed in as datetime, they're currently a string
# QueryDicts are immutable, we need to copy
formcopy = data["form"].data.copy()
try:
formcopy["first_published_date"] = dateparse(
formcopy["first_published_date"]
)
except MultiValueDictKeyError:
pass
try:
formcopy["published_date"] = dateparse(formcopy["published_date"])
except MultiValueDictKeyError:
pass
data["form"].data = formcopy
return TemplateResponse(request, "book/edit_book.html", data)
remove_authors = request.POST.getlist("remove_authors")

View file

@ -138,7 +138,7 @@ def handle_remote_webfinger(query):
user = activitypub.resolve_remote_id(
link["href"], model=models.User
)
except KeyError:
except (KeyError, activitypub.ActivitySerializerError):
return None
return user

25
bw-dev
View file

@ -19,7 +19,6 @@ function clean {
function runweb {
docker-compose run --rm web "$@"
clean
}
function execdb {
@ -64,17 +63,16 @@ case "$CMD" in
clean
;;
makemigrations)
execweb python manage.py makemigrations "$@"
runweb python manage.py makemigrations "$@"
;;
migrate)
execweb python manage.py rename_app fedireads bookwyrm
execweb python manage.py migrate "$@"
runweb python manage.py migrate "$@"
;;
bash)
execweb bash
runweb bash
;;
shell)
execweb python manage.py shell
runweb python manage.py shell
;;
dbshell)
execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB}
@ -83,22 +81,19 @@ case "$CMD" in
docker-compose restart celery_worker
;;
test)
execweb coverage run --source='.' --omit="*/test*,celerywyrm*,bookwyrm/migrations/*" manage.py test "$@"
runweb coverage run --source='.' --omit="*/test*,celerywyrm*,bookwyrm/migrations/*" manage.py test "$@"
;;
pytest)
execweb pytest --no-cov-on-fail "$@"
;;
test_report)
execweb coverage report
runweb pytest --no-cov-on-fail "$@"
;;
collectstatic)
execweb python manage.py collectstatic --no-input
runweb python manage.py collectstatic --no-input
;;
makemessages)
execweb django-admin makemessages --no-wrap --ignore=venv3 $@
runweb django-admin makemessages --no-wrap --ignore=venv3 $@
;;
compilemessages)
execweb django-admin compilemessages --ignore venv3 $@
runweb django-admin compilemessages --ignore venv3 $@
;;
build)
docker-compose build
@ -110,7 +105,7 @@ case "$CMD" in
makeitblack
;;
populate_streams)
execweb python manage.py populate_streams
runweb python manage.py populate_streams
;;
*)
echo "Unrecognised command. Try: build, clean, up, initdb, resetdb, makemigrations, migrate, bash, shell, dbshell, restart_celery, test, pytest, test_report, black, populate_feeds"