mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-04-23 02:34:19 +00:00
Enable connectors to be managed in admin interface
This patch creates a "connectors" page in the admin interface. Admins can activate new connectors from here after they are added in a version of BookWyrm. The can also disable and re-enable connectors, and change the priority order for all connectors. I have created a connector.update() function for future use. This will allow admins to easily update an existing connector when e.g. the API endpoints change.
This commit is contained in:
parent
a709121846
commit
90cad57dba
12 changed files with 446 additions and 2 deletions
bookwyrm
|
@ -3,4 +3,4 @@ from .settings import CONNECTORS
|
|||
from .abstract_connector import ConnectorException
|
||||
from .abstract_connector import get_data, get_image, maybe_isbn
|
||||
|
||||
from .connector_manager import search, first_search_result
|
||||
from .connector_manager import search, first_search_result, create_finna_connector
|
||||
|
|
|
@ -206,3 +206,26 @@ def raise_not_valid_url(url: str) -> None:
|
|||
|
||||
if models.FederatedServer.is_blocked(url):
|
||||
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
|
||||
|
||||
|
||||
def create_finna_connector() -> None:
|
||||
"""create a Finna connector"""
|
||||
|
||||
models.Connector.objects.create(
|
||||
identifier="api.finna.fi",
|
||||
name="Finna API",
|
||||
connector_file="finna",
|
||||
base_url="https://www.finna.fi",
|
||||
books_url="https://api.finna.fi/api/v1/record" "?id=",
|
||||
covers_url="https://api.finna.fi",
|
||||
search_url="https://api.finna.fi/api/v1/search?limit=20"
|
||||
"&filter[]=format%3a%220%2fBook%2f%22"
|
||||
"&field[]=title&field[]=recordPage&field[]=authors"
|
||||
"&field[]=year&field[]=id&field[]=formats&field[]=images"
|
||||
"&lookfor=",
|
||||
isbn_search_url="https://api.finna.fi/api/v1/search?limit=1"
|
||||
"&filter[]=format%3a%220%2fBook%2f%22"
|
||||
"&field[]=title&field[]=recordPage&field[]=authors&field[]=year"
|
||||
"&field[]=id&field[]=formats&field[]=images"
|
||||
"&lookfor=isbn:",
|
||||
)
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
""" settings book data connectors """
|
||||
|
||||
CONNECTORS = ["openlibrary", "inventaire", "bookwyrm_connector", "finna"]
|
||||
CONNECTORS = [
|
||||
"openlibrary",
|
||||
"inventaire",
|
||||
"bookwyrm_connector",
|
||||
"finna",
|
||||
]
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
""" manages interfaces with external sources of book data """
|
||||
from typing import Optional
|
||||
|
||||
from django.db import models
|
||||
from bookwyrm.connectors.settings import CONNECTORS
|
||||
|
||||
|
@ -29,3 +31,34 @@ class Connector(BookWyrmModel):
|
|||
|
||||
def __str__(self):
|
||||
return f"{self.identifier} ({self.id})"
|
||||
|
||||
def deactivate(self, reason: Optional[str] = None) -> None:
|
||||
"""Make an active connector inactive. We do not delete connectors
|
||||
because they have books and authors associated with them."""
|
||||
|
||||
self.active = False
|
||||
self.deactivation_reason = reason
|
||||
self.save(update_fields=["active", "deactivation_reason"])
|
||||
|
||||
def activate(self) -> None:
|
||||
"""Make an inactive connector active again"""
|
||||
|
||||
self.active = True
|
||||
self.deactivation_reason = None
|
||||
self.save(update_fields=["active", "deactivation_reason"])
|
||||
|
||||
def change_priority(self, priority: int) -> None:
|
||||
"""Change the priority value for a connector
|
||||
This determines the order they appear in book search"""
|
||||
|
||||
self.priority = priority
|
||||
self.save(update_fields=["priority"])
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update the settings for this connector. e.g. if the
|
||||
API endpoints change."""
|
||||
|
||||
# example
|
||||
# if self.identifier == "openlibrary.org":
|
||||
# self.isbn_search_url = "https://openlibrary.org/search.json?isbn="
|
||||
# self.save(update_fields=["isbn_search_url"])
|
||||
|
|
36
bookwyrm/templates/settings/connectors/available.html
Normal file
36
bookwyrm/templates/settings/connectors/available.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
{% load i18n %}
|
||||
|
||||
<li class="block">
|
||||
<div class="card ">
|
||||
<div class="card-header has-background-info-light">
|
||||
<h4 class="m-3"><strong>
|
||||
{% if connector == "finna" %}
|
||||
Finna
|
||||
{% elif connector == "awesome_connector" %}
|
||||
Awesome connector
|
||||
{% endif %}
|
||||
</strong></h4>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
{% if connector == "finna" %}
|
||||
{% trans "Finna.fi is a search service that collects material from hundreds of Finnish organisations under one roof." %}
|
||||
{% elif connector == "awesome_connector" %}
|
||||
This new connector is really good, you'll like it.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer is-stacked-mobile is-align-items-stretch">
|
||||
<form
|
||||
name="create-connector"
|
||||
method="post"
|
||||
action="{% url 'settings-create-connector' %}"
|
||||
class="card-footer-item m-2"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="connector_file" value={{connector}}>
|
||||
<button type="submit" class="button is-small is-info">{% trans "Create new connector" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
66
bookwyrm/templates/settings/connectors/connector.html
Normal file
66
bookwyrm/templates/settings/connectors/connector.html
Normal file
|
@ -0,0 +1,66 @@
|
|||
{% load i18n %}
|
||||
|
||||
<li class="block">
|
||||
<div class="card">
|
||||
<div class="card-header {% if connector.active %}has-background-success-light{%else%}has-background-danger-light{%endif%}">
|
||||
<p class="m-2">
|
||||
{% if connector.name %}<strong>{{connector.name}}</strong> - {% endif %}{{connector.identifier}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer is-stacked-mobile has-background-tertiary is-align-items-stretch">
|
||||
|
||||
{% if connector.active %}
|
||||
<form
|
||||
name="set-position-{{ connector.id }}"
|
||||
method="post"
|
||||
action="{% url 'settings-connector-priority' connector.id %}"
|
||||
class="card-footer-item"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<div class="field has-addons mb-0">
|
||||
<div class="control">
|
||||
<label for="input-priority" class="button is-transparent is-small">{% trans "Priority" %}</label>
|
||||
</div>
|
||||
<div class="control">
|
||||
<input id="input_priority_{{ connector.id }}" class="input is-small" type="number" min="1" name="priority" value="{{connector.priority}}">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-info is-small is-tablet">{% trans "Set" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="card-footer-item">
|
||||
<div>
|
||||
<h3 class="menu-label">{% trans 'Deactivation reason:' %}</h3>
|
||||
<p>{{ connector.deactivation_reason }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if connector.active %}
|
||||
<form
|
||||
name="deactivate-{{ connector.id }}"
|
||||
method="post"
|
||||
action="{% url 'settings-deactivate-connector' connector.id %}"
|
||||
class="card-footer-item m-2"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item" value="{{ connector.id }}">
|
||||
<button type="submit" class="button is-small is-danger">{% trans "Deactivate" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form
|
||||
name="activate-{{ connector.id }}"
|
||||
method="post"
|
||||
action="{% url 'settings-activate-connector' connector.id %}"
|
||||
class="card-footer-item"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item" value="{{ connector.id }}">
|
||||
<button type="submit" class="button is-small is-success">{% trans "Activate" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
40
bookwyrm/templates/settings/connectors/connectors.html
Normal file
40
bookwyrm/templates/settings/connectors/connectors.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Connector Settings" %}{% endblock %}
|
||||
|
||||
{% block header %}{% trans "Connector Settings" %}{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="columns mt-3">
|
||||
<section class="column is-three-quarters">
|
||||
{% blocktrans %}
|
||||
<p class="mb-2">Connectors are sources of data about books and authors.</p>
|
||||
<p class="mb-2">The priority determines the order in which search results appear. The highest priority is <code>1</code>. The default priority is <code>2</code>.</p>
|
||||
{% endblocktrans %}
|
||||
<p class="mb-2">
|
||||
{% trans "Connector settings only determine whether a connector will be used to deliver search results. To manage more interactions with other federated servers, including domain blocks, see" %} <a href="{% url 'settings-federation' %}"> {% trans "Federated Instances" %}</a>.
|
||||
</p>
|
||||
<h4 class="title is-4 my-6">External Connectors</h4>
|
||||
<ul class="ordered-list">
|
||||
{% for connector in update_connectors %}
|
||||
{% include "settings/connectors/update.html" %}
|
||||
{% endfor %}
|
||||
{% for connector in other_connectors %}
|
||||
{% include "settings/connectors/connector.html" %}
|
||||
{% endfor %}
|
||||
{% if available_connectors %}
|
||||
{% for connector in available_connectors %}
|
||||
{% include "settings/connectors/available.html" %}
|
||||
{% endfor %}
|
||||
{% endif%}
|
||||
</ul>
|
||||
<h4 class="title is-4 my-6">BookWyrm Connectors</h4>
|
||||
<ul class="ordered-list">
|
||||
{% for connector in bookwyrm_connectors %}
|
||||
{% include "settings/connectors/connector.html" %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
84
bookwyrm/templates/settings/connectors/update.html
Normal file
84
bookwyrm/templates/settings/connectors/update.html
Normal file
|
@ -0,0 +1,84 @@
|
|||
{% load i18n %}
|
||||
|
||||
<li class="block">
|
||||
<div class="card">
|
||||
<div class="card-header has-background-warning p-3">
|
||||
<span class="icon icon-warning is-size-4 p-3 is-pulled-right" aria-hidden="true"></span>
|
||||
{% if connector.name %}<strong>{{connector.name}}</strong> - {% endif %}{{connector.identifier}}
|
||||
</div>
|
||||
<div class="card-footer is-stacked-mobile is-align-items-stretch">
|
||||
|
||||
{% if connector.active %}
|
||||
<form
|
||||
name="set-position-{{ connector.id }}"
|
||||
method="post"
|
||||
action="{% url 'settings-connector-priority' connector.id %}"
|
||||
class="card-footer-item m-2"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<div class="field has-addons mb-0">
|
||||
<div class="control">
|
||||
<label for="input-priority" class="button is-transparent is-small">{% trans "Priority" %}</label>
|
||||
</div>
|
||||
<div class="control">
|
||||
<input id="input_priority_{{ connector.id }}" class="input is-small" type="number" min="1" name="priority" value="{{connector.priority}}">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-info is-small is-tablet">{% trans "Set" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div>
|
||||
<h3 class="menu-label">{% trans 'Deactivation reason:' %}</h3>
|
||||
<p>{{ connector.deactivation_reason }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if connector.active %}
|
||||
<form
|
||||
name="deactivate-{{ connector.id }}"
|
||||
method="post"
|
||||
action="{% url 'settings-deactivate-connector' connector.id %}"
|
||||
class="card-footer-item"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item" value="{{ connector.id }}">
|
||||
<button type="submit" class="button is-small is-danger">{% trans "Deactivate" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form
|
||||
name="activate-{{ connector.id }}"
|
||||
method="post"
|
||||
action="{% url 'settings-activate-connector' connector.id %}"
|
||||
class="card-footer-item"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item" value="{{ connector.id }}">
|
||||
<button type="submit" class="button is-small is-success">{% trans "Activate" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer is-stacked-mobile is-align-items-stretch">
|
||||
<div class="card-footer-item">
|
||||
<div>
|
||||
<div>
|
||||
<span class="has-text-weight-bold">{{ connector.identifier}} </span> {% trans 'should be updated. Check recent release notes for more information.' %}
|
||||
</div>
|
||||
<form
|
||||
name="update-{{ connector.id }}"
|
||||
method="post"
|
||||
action="{% url 'settings-update-connector' connector.id %}"
|
||||
class="card-footer-item"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item" value="{{ connector.id }}">
|
||||
<button type="submit" class="button is-small is-warning">{% trans "Update" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
|
@ -93,6 +93,10 @@
|
|||
{% url 'settings-email-config' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Configuration" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% url 'settings-connectors' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Connectors" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.edit_instance_settings %}
|
||||
|
|
|
@ -139,6 +139,36 @@ urlpatterns = [
|
|||
views.delete_announcement,
|
||||
name="settings-announcements-delete",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/connectors/?$",
|
||||
views.ConnectorSettings.as_view(),
|
||||
name="settings-connectors",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/connectors/(?P<connector_id>\d+)/deactivate?$",
|
||||
views.deactivate_connector,
|
||||
name="settings-deactivate-connector",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/connectors/(?P<connector_id>\d+)/activate?$",
|
||||
views.activate_connector,
|
||||
name="settings-activate-connector",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/connectors/(?P<connector_id>\d+)/priority?$",
|
||||
views.set_connector_priority,
|
||||
name="settings-connector-priority",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/connectors/create?$",
|
||||
views.create_connector,
|
||||
name="settings-create-connector",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/connectors/(?P<connector_id>\d+)/update?$",
|
||||
views.update_connector,
|
||||
name="settings-update-connector",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/email-preview/?$",
|
||||
views.admin.email_config.email_preview,
|
||||
|
|
|
@ -5,6 +5,14 @@ from .admin.announcements import EditAnnouncement, delete_announcement
|
|||
from .admin.automod import AutoMod, automod_delete, run_automod
|
||||
from .admin.automod import schedule_automod_task, unschedule_automod_task
|
||||
from .admin.celery_status import CeleryStatus, celery_ping
|
||||
from .admin.connectors import (
|
||||
ConnectorSettings,
|
||||
deactivate_connector,
|
||||
activate_connector,
|
||||
set_connector_priority,
|
||||
create_connector,
|
||||
update_connector,
|
||||
)
|
||||
from .admin.schedule import ScheduledTasks
|
||||
from .admin.dashboard import Dashboard
|
||||
from .admin.federation import Federation, FederatedServer
|
||||
|
|
115
bookwyrm/views/admin/connectors.py
Normal file
115
bookwyrm/views/admin/connectors.py
Normal file
|
@ -0,0 +1,115 @@
|
|||
""" manage book data sources """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm.models import Connector
|
||||
from bookwyrm.connectors import create_finna_connector
|
||||
from bookwyrm.connectors.settings import CONNECTORS
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class ConnectorSettings(View):
|
||||
"""show connector settings page"""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get(self, request):
|
||||
"""list of connectors"""
|
||||
|
||||
# connectors where the previous defaults need to be updated
|
||||
# for example when an API endpoint changes and we want admins
|
||||
# to update their connector settings
|
||||
|
||||
update_connectors = Connector.objects.none() # for now
|
||||
|
||||
# example - this would pick up all openlibrary connectors where
|
||||
# the isbn_search_url is not "https://openlibrary.org/search.json?isbn="
|
||||
|
||||
# update_connectors = Connector.objects.filter(
|
||||
# connector_file="openlibrary"
|
||||
# ).exclude(isbn_search_url="https://openlibrary.org/search.json?isbn=")
|
||||
|
||||
# book API source like Open Library etc
|
||||
other_connectors = (
|
||||
Connector.objects.exclude(connector_file="bookwyrm_connector")
|
||||
.order_by("name")
|
||||
.difference(update_connectors)
|
||||
)
|
||||
|
||||
# other BookWyrm instances
|
||||
bookwrym_connectors = Connector.objects.filter(
|
||||
connector_file="bookwyrm_connector"
|
||||
).order_by("identifier")
|
||||
|
||||
# Optional and new connectors e.g. Finna
|
||||
# These are not yet Connector objects so we have to describe them in the
|
||||
# template at settings/connectors/available.html
|
||||
available_connectors = []
|
||||
for val in CONNECTORS:
|
||||
if Connector.objects.filter(connector_file=val).count() == 0:
|
||||
available_connectors.append(val)
|
||||
|
||||
data = {
|
||||
"update_connectors": update_connectors,
|
||||
"bookwyrm_connectors": bookwrym_connectors,
|
||||
"other_connectors": other_connectors,
|
||||
"available_connectors": available_connectors,
|
||||
}
|
||||
return TemplateResponse(request, "settings/connectors/connectors.html", data)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def deactivate_connector(request, connector_id):
|
||||
"""we don't want to use this connector"""
|
||||
|
||||
connector = get_object_or_404(Connector, id=connector_id)
|
||||
connector.deactivate(reason="manual")
|
||||
# TODO: should we allow free text for a deactivation reason?
|
||||
return redirect("/settings/connectors/")
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def activate_connector(request, connector_id: int):
|
||||
"""oops changed our mind"""
|
||||
|
||||
connector = get_object_or_404(Connector, id=connector_id)
|
||||
connector.activate()
|
||||
return redirect("/settings/connectors/")
|
||||
|
||||
|
||||
def set_connector_priority(request, connector_id: int):
|
||||
"""what order should connectors appear in?"""
|
||||
|
||||
priority = request.POST.get("priority")
|
||||
connector = get_object_or_404(Connector, id=connector_id)
|
||||
connector.change_priority(priority=priority)
|
||||
return redirect("/settings/connectors/")
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def update_connector(request, connector_id: int):
|
||||
"""update connector info such as API endpoints"""
|
||||
|
||||
connector = get_object_or_404(Connector, id=connector_id)
|
||||
# see models.Connector to determine what update() does
|
||||
connector.update()
|
||||
return redirect("/settings/connectors/")
|
||||
|
||||
|
||||
def create_connector(request):
|
||||
"""create a new optional connector"""
|
||||
|
||||
connector_file = request.POST.get("connector_file")
|
||||
|
||||
# if you add a new connector, add a check here
|
||||
# and make a create_xxx_connector() function in connector_manager.py
|
||||
if connector_file == "finna":
|
||||
create_finna_connector()
|
||||
|
||||
return redirect("/settings/connectors/")
|
Loading…
Reference in a new issue