forked from mirrors/bookwyrm
Approve or delete import guesses
This commit is contained in:
parent
221cde9be4
commit
40fff02eec
8 changed files with 118 additions and 22 deletions
|
@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
||||||
class Importer:
|
class Importer:
|
||||||
"""Generic class for csv data import from an outside service"""
|
"""Generic class for csv data import from an outside service"""
|
||||||
|
|
||||||
service = "Unknown"
|
service = "Import"
|
||||||
delimiter = ","
|
delimiter = ","
|
||||||
encoding = "UTF-8"
|
encoding = "UTF-8"
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ class Importer:
|
||||||
include_reviews=include_reviews,
|
include_reviews=include_reviews,
|
||||||
privacy=privacy,
|
privacy=privacy,
|
||||||
mappings=self.create_row_mappings(csv_reader.fieldnames),
|
mappings=self.create_row_mappings(csv_reader.fieldnames),
|
||||||
|
source=self.service,
|
||||||
)
|
)
|
||||||
|
|
||||||
for index, entry in rows:
|
for index, entry in rows:
|
||||||
|
@ -108,16 +109,16 @@ class Importer:
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue="low_priority")
|
@app.task(queue="low_priority")
|
||||||
def start_import_task(source, job_id):
|
def start_import_task(job_id):
|
||||||
"""trigger the child tasks for each row"""
|
"""trigger the child tasks for each row"""
|
||||||
job = ImportJob.objects.get(id=job_id)
|
job = ImportJob.objects.get(id=job_id)
|
||||||
# these are sub-tasks so that one big task doesn't use up all the memory in celery
|
# these are sub-tasks so that one big task doesn't use up all the memory in celery
|
||||||
for item in job.items.values_list("id", flat=True).all():
|
for item in job.items.values_list("id", flat=True).all():
|
||||||
import_item_task.delay(source, item)
|
import_item_task.delay(item)
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue="low_priority")
|
@app.task(queue="low_priority")
|
||||||
def import_item_task(source, item_id):
|
def import_item_task(item_id):
|
||||||
"""resolve a row into a book"""
|
"""resolve a row into a book"""
|
||||||
item = models.ImportItem.objects.get(id=item_id)
|
item = models.ImportItem.objects.get(id=item_id)
|
||||||
try:
|
try:
|
||||||
|
@ -128,17 +129,18 @@ def import_item_task(source, item_id):
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
if item.book:
|
if item.book:
|
||||||
job = item.job
|
|
||||||
# shelves book and handles reviews
|
# shelves book and handles reviews
|
||||||
handle_imported_book(source, job.user, item, job.include_reviews, job.privacy)
|
handle_imported_book(item)
|
||||||
else:
|
else:
|
||||||
item.fail_reason = _("Could not find a match for book")
|
item.fail_reason = _("Could not find a match for book")
|
||||||
|
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
def handle_imported_book(source, user, item, include_reviews, privacy):
|
def handle_imported_book(item):
|
||||||
"""process a csv and then post about it"""
|
"""process a csv and then post about it"""
|
||||||
|
job = item.job
|
||||||
|
user = job.user
|
||||||
if isinstance(item.book, models.Work):
|
if isinstance(item.book, models.Work):
|
||||||
item.book = item.book.default_edition
|
item.book = item.book.default_edition
|
||||||
if not item.book:
|
if not item.book:
|
||||||
|
@ -167,7 +169,7 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
|
||||||
read.user = user
|
read.user = user
|
||||||
read.save()
|
read.save()
|
||||||
|
|
||||||
if include_reviews and (item.rating or item.review):
|
if job.include_reviews and (item.rating or item.review):
|
||||||
# we don't know the publication date of the review,
|
# we don't know the publication date of the review,
|
||||||
# but "now" is a bad guess
|
# but "now" is a bad guess
|
||||||
published_date_guess = item.date_read or item.date_added
|
published_date_guess = item.date_read or item.date_added
|
||||||
|
@ -176,7 +178,7 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
|
||||||
review_title = (
|
review_title = (
|
||||||
"Review of {!r} on {!r}".format(
|
"Review of {!r} on {!r}".format(
|
||||||
item.book.title,
|
item.book.title,
|
||||||
source,
|
job.source,
|
||||||
)
|
)
|
||||||
if item.review
|
if item.review
|
||||||
else ""
|
else ""
|
||||||
|
@ -188,7 +190,7 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
|
||||||
content=item.review,
|
content=item.review,
|
||||||
rating=item.rating,
|
rating=item.rating,
|
||||||
published_date=published_date_guess,
|
published_date=published_date_guess,
|
||||||
privacy=privacy,
|
privacy=job.privacy,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# just a rating
|
# just a rating
|
||||||
|
@ -197,7 +199,7 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
|
||||||
book=item.book,
|
book=item.book,
|
||||||
rating=item.rating,
|
rating=item.rating,
|
||||||
published_date=published_date_guess,
|
published_date=published_date_guess,
|
||||||
privacy=privacy,
|
privacy=job.privacy,
|
||||||
)
|
)
|
||||||
# only broadcast this review to other bookwyrm instances
|
# only broadcast this review to other bookwyrm instances
|
||||||
review.save(software="bookwyrm", priority=LOW)
|
review.save(software="bookwyrm", priority=LOW)
|
||||||
|
|
19
bookwyrm/migrations/0114_importjob_source.py
Normal file
19
bookwyrm/migrations/0114_importjob_source.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 3.2.5 on 2021-11-13 00:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0113_auto_20211110_2104"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="importjob",
|
||||||
|
name="source",
|
||||||
|
field=models.CharField(default="Import", max_length=100),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
|
@ -37,6 +37,7 @@ class ImportJob(models.Model):
|
||||||
include_reviews = models.BooleanField(default=True)
|
include_reviews = models.BooleanField(default=True)
|
||||||
mappings = models.JSONField()
|
mappings = models.JSONField()
|
||||||
complete = models.BooleanField(default=False)
|
complete = models.BooleanField(default=False)
|
||||||
|
source = models.CharField(max_length=100)
|
||||||
privacy = models.CharField(
|
privacy = models.CharField(
|
||||||
max_length=255, default="public", choices=PrivacyLevels.choices
|
max_length=255, default="public", choices=PrivacyLevels.choices
|
||||||
)
|
)
|
||||||
|
@ -62,6 +63,11 @@ class ImportItem(models.Model):
|
||||||
|
|
||||||
def resolve(self):
|
def resolve(self):
|
||||||
"""try various ways to lookup a book"""
|
"""try various ways to lookup a book"""
|
||||||
|
# we might be calling this after manually adding the book,
|
||||||
|
# so no need to do searches
|
||||||
|
if self.book:
|
||||||
|
return
|
||||||
|
|
||||||
if self.isbn:
|
if self.isbn:
|
||||||
self.book = self.get_book_from_isbn()
|
self.book = self.get_book_from_isbn()
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -40,10 +40,11 @@
|
||||||
{% if manual_review_count %}
|
{% if manual_review_count %}
|
||||||
<div class="notification">
|
<div class="notification">
|
||||||
{% blocktrans trimmed count counter=manual_review_count with display_counter=manual_review_count|intcomma %}
|
{% blocktrans trimmed count counter=manual_review_count with display_counter=manual_review_count|intcomma %}
|
||||||
{{ display_counter }} item needs manual review.
|
{{ display_counter }} item needs manual approval.
|
||||||
{% plural %}
|
{% plural %}
|
||||||
{{ display_counter }} items need manual review.
|
{{ display_counter }} items need manual approval.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
<a href="{% url 'import-review' job.id %}">{% trans "Review items" %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -55,7 +56,7 @@
|
||||||
{{ display_counter }} items failed to import.
|
{{ display_counter }} items failed to import.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
<a href="{% url 'import-troubleshoot' job.id %}">
|
<a href="{% url 'import-troubleshoot' job.id %}">
|
||||||
{% trans "View and troubleshoot failed items." %}
|
{% trans "View and troubleshoot failed items" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -52,9 +52,22 @@
|
||||||
<td>
|
<td>
|
||||||
{{ item.normalized_data.authors }}
|
{{ item.normalized_data.authors }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="content is-flex">
|
||||||
<button type="submit" class="button is-success">{% trans "Approve" %}</button>
|
<form class="pr-2" name="approve-{{ item.id }}" method="POST" action="{% url 'import-approve' job.id item.id %}">
|
||||||
<button type="submit" class="button is-danger is-light is-outlined">{% trans "Delete" %}</button>
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="button is-success">
|
||||||
|
<span class="icon icon-check" aria-hidden="true"></span>
|
||||||
|
<span class="is-sr-only-mobile">{% trans "Approve" %}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form name="delete-{{ item.id }}" method="POST" action="{% url 'import-delete' job.id item.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="button is-danger is-light is-outlined">
|
||||||
|
<span class="icon icon-x" aria-hidden="true"></span>
|
||||||
|
<span class="is-sr-only-mobile">{% trans "Delete" %}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -237,17 +237,36 @@ urlpatterns = [
|
||||||
re_path(r"^search/?$", views.Search.as_view(), name="search"),
|
re_path(r"^search/?$", views.Search.as_view(), name="search"),
|
||||||
# imports
|
# imports
|
||||||
re_path(r"^import/?$", views.Import.as_view(), name="import"),
|
re_path(r"^import/?$", views.Import.as_view(), name="import"),
|
||||||
re_path(r"^import/(\d+)/?$", views.ImportStatus.as_view(), name="import-status"),
|
|
||||||
re_path(
|
re_path(
|
||||||
r"^import/(\d+)/failed/?$",
|
r"^import/(?P<job_id>\d+)/?$",
|
||||||
|
views.ImportStatus.as_view(),
|
||||||
|
name="import-status",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^import/(?P<job_id>\d+)/failed/?$",
|
||||||
views.ImportTroubleshoot.as_view(),
|
views.ImportTroubleshoot.as_view(),
|
||||||
name="import-troubleshoot",
|
name="import-troubleshoot",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^import/(\d+)/review/?$",
|
r"^import/(?P<job_id>\d+)/review/?$",
|
||||||
views.ImportManualReview.as_view(),
|
views.ImportManualReview.as_view(),
|
||||||
name="import-review",
|
name="import-review",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^import/(?P<job_id>\d+)/review/?$",
|
||||||
|
views.ImportManualReview.as_view(),
|
||||||
|
name="import-review",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^import/(?P<job_id>\d+)/review/(?P<item_id>\d+)/approve/?$",
|
||||||
|
views.approve_import_item,
|
||||||
|
name="import-approve",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^import/(?P<job_id>\d+)/review/(?P<item_id>\d+)/delete/?$",
|
||||||
|
views.delete_import_item,
|
||||||
|
name="import-delete",
|
||||||
|
),
|
||||||
# users
|
# users
|
||||||
re_path(rf"{USER_PATH}\.json$", views.User.as_view()),
|
re_path(rf"{USER_PATH}\.json$", views.User.as_view()),
|
||||||
re_path(rf"{USER_PATH}/?$", views.User.as_view(), name="user-feed"),
|
re_path(rf"{USER_PATH}/?$", views.User.as_view(), name="user-feed"),
|
||||||
|
|
|
@ -47,7 +47,11 @@ from .shelf.shelf_actions import shelve, unshelve
|
||||||
from .imports.import_data import Import
|
from .imports.import_data import Import
|
||||||
from .imports.import_status import ImportStatus
|
from .imports.import_status import ImportStatus
|
||||||
from .imports.troubleshoot import ImportTroubleshoot
|
from .imports.troubleshoot import ImportTroubleshoot
|
||||||
from .imports.manually_review import ImportManualReview
|
from .imports.manually_review import (
|
||||||
|
ImportManualReview,
|
||||||
|
approve_import_item,
|
||||||
|
delete_import_item,
|
||||||
|
)
|
||||||
|
|
||||||
# misc views
|
# misc views
|
||||||
from .author import Author, EditAuthor
|
from .author import Author, EditAuthor
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
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 bookwyrm import models
|
from bookwyrm import models
|
||||||
|
from bookwyrm.importers.importer import import_item_task
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
|
||||||
# pylint: disable= no-self-use
|
# pylint: disable= no-self-use
|
||||||
|
@ -37,3 +39,33 @@ class ImportManualReview(View):
|
||||||
}
|
}
|
||||||
|
|
||||||
return TemplateResponse(request, "import/manual_review.html", data)
|
return TemplateResponse(request, "import/manual_review.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def approve_import_item(request, job_id, item_id):
|
||||||
|
"""we guessed right"""
|
||||||
|
item = get_object_or_404(
|
||||||
|
models.ImportItem, id=item_id, job__id=job_id, book_guess__isnull=False
|
||||||
|
)
|
||||||
|
item.fail_reason = None
|
||||||
|
item.book = item.book_guess
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
# the good stuff - actually import the data
|
||||||
|
import_item_task.delay(item.id)
|
||||||
|
return redirect("import-review", job_id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def delete_import_item(request, job_id, item_id):
|
||||||
|
"""we guessed right"""
|
||||||
|
item = get_object_or_404(
|
||||||
|
models.ImportItem, id=item_id, job__id=job_id, book_guess__isnull=False
|
||||||
|
)
|
||||||
|
item.book_guess = None
|
||||||
|
item.save()
|
||||||
|
return redirect("import-review", job_id)
|
||||||
|
|
Loading…
Reference in a new issue