Approve or delete import guesses

This commit is contained in:
Mouse Reeve 2021-11-12 17:10:47 -08:00
parent 221cde9be4
commit 40fff02eec
8 changed files with 118 additions and 22 deletions

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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