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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
- uses: psf/black@20.8b1 - uses: psf/black@stable
with: with:
args: ". --check -l 80 -S" args: ". --check -l 80 -S"

View file

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

View file

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

View file

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

View file

@ -93,7 +93,10 @@ class Connector(AbstractConnector):
# this id is "/authors/OL1234567A" # this id is "/authors/OL1234567A"
author_id = author_blob["key"] author_id = author_blob["key"]
url = "%s%s" % (self.base_url, author_id) 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"): def get_cover_url(self, cover_blob, size="L"):
""" ask openlibrary for the cover """ """ 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 """ """ activitypub model functionality """
from base64 import b64encode from base64 import b64encode
from collections import namedtuple
from functools import reduce from functools import reduce
import json import json
import operator import operator
@ -25,6 +26,15 @@ from bookwyrm.models.fields import ImageField, ManyToManyField
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# I tried to separate these classes into mutliple files but I kept getting # 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! # 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: class ActivitypubMixin:
""" add this mixin for models that are AP serializable """ """ add this mixin for models that are AP serializable """
@ -52,6 +62,11 @@ class ActivitypubMixin:
self.activity_fields = ( self.activity_fields = (
self.image_fields + self.many_to_many_fields + self.simple_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 # these are separate to avoid infinite recursion issues
self.deserialize_reverse_fields = ( self.deserialize_reverse_fields = (
@ -370,7 +385,7 @@ class CollectionItemMixin(ActivitypubMixin):
object_field = getattr(self, self.object_field) object_field = getattr(self, self.object_field)
collection_field = getattr(self, self.collection_field) collection_field = getattr(self, self.collection_field)
return activitypub.Add( return activitypub.Add(
id=self.remote_id, id=self.get_remote_id(),
actor=self.user.remote_id, actor=self.user.remote_id,
object=object_field, object=object_field,
target=collection_field.remote_id, target=collection_field.remote_id,
@ -381,7 +396,7 @@ class CollectionItemMixin(ActivitypubMixin):
object_field = getattr(self, self.object_field) object_field = getattr(self, self.object_field)
collection_field = getattr(self, self.collection_field) collection_field = getattr(self, self.collection_field)
return activitypub.Remove( return activitypub.Remove(
id=self.remote_id, id=self.get_remote_id(),
actor=self.user.remote_id, actor=self.user.remote_id,
object=object_field, object=object_field,
target=collection_field.remote_id, target=collection_field.remote_id,
@ -430,7 +445,7 @@ def generate_activity(obj):
) in obj.serialize_reverse_fields: ) in obj.serialize_reverse_fields:
related_field = getattr(obj, model_field_name) related_field = getattr(obj, model_field_name)
activity[activity_field_name] = unfurl_related_field( activity[activity_field_name] = unfurl_related_field(
related_field, sort_field related_field, sort_field=sort_field
) )
if not activity.get("id"): if not activity.get("id"):
@ -440,7 +455,7 @@ def generate_activity(obj):
def unfurl_related_field(related_field, sort_field=None): def unfurl_related_field(related_field, sort_field=None):
""" load reverse lookups (like public key owner or Status attachment """ """ load reverse lookups (like public key owner or Status attachment """
if hasattr(related_field, "all"): if sort_field and hasattr(related_field, "all"):
return [ return [
unfurl_related_field(i) for i in related_field.order_by(sort_field).all() 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) connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True)
# book/work metadata # 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) 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) description = fields.HtmlField(blank=True, null=True)
languages = fields.ArrayField( languages = fields.ArrayField(
models.CharField(max_length=255), blank=True, default=list 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) series_number = fields.CharField(max_length=255, blank=True, null=True)
subjects = fields.ArrayField( subjects = fields.ArrayField(
models.CharField(max_length=255), blank=True, null=True, default=list models.CharField(max_length=255), blank=True, null=True, default=list

View file

@ -112,6 +112,12 @@ class User(OrderedCollectionPageMixin, AbstractUser):
) )
name_field = "username" 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 @property
def alt_text(self): def alt_text(self):

View file

@ -88,12 +88,18 @@
<div class="column is-half"> <div class="column is-half">
<section class="block"> <section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2> <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 %} {% for error in form.title.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.subtitle.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
@ -124,7 +130,7 @@
<p class="mb-2"> <p class="mb-2">
<label class="label" for="id_first_published_date">{% trans "First published date:" %}</label> <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> </p>
{% for error in form.first_published_date.errors %} {% for error in form.first_published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -132,7 +138,7 @@
<p class="mb-2"> <p class="mb-2">
<label class="label" for="id_published_date">{% trans "Published date:" %}</label> <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> </p>
{% for error in form.published_date.errors %} {% for error in form.published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>

View file

@ -1,6 +1,8 @@
""" the good stuff! the books! """ """ the good stuff! the books! """
from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from dateutil.parser import parse as dateparse
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import SearchRank, SearchVector from django.contrib.postgres.search import SearchRank, SearchVector
from django.core.files.base import ContentFile 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.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
@ -172,6 +175,20 @@ class EditBook(View):
data["confirm_mode"] = True data["confirm_mode"] = True
# this isn't preserved because it isn't part of the form obj # this isn't preserved because it isn't part of the form obj
data["remove_authors"] = request.POST.getlist("remove_authors") 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) return TemplateResponse(request, "book/edit_book.html", data)
remove_authors = request.POST.getlist("remove_authors") remove_authors = request.POST.getlist("remove_authors")

View file

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

25
bw-dev
View file

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