mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-30 05:21:08 +00:00
Merge pull request #2333 from bookwyrm-social/cancel-imports
Let users and admins cancel imports
This commit is contained in:
commit
4e1da6a759
25 changed files with 354 additions and 175 deletions
|
@ -1,15 +1,7 @@
|
||||||
""" handle reading a csv from an external service, defaults are from Goodreads """
|
""" handle reading a csv from an external service, defaults are from Goodreads """
|
||||||
import csv
|
import csv
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from bookwyrm.models import ImportJob, ImportItem
|
from bookwyrm.models import ImportJob, ImportItem
|
||||||
from bookwyrm.tasks import app, LOW
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Importer:
|
class Importer:
|
||||||
|
@ -118,127 +110,3 @@ class Importer:
|
||||||
# this will re-normalize the raw data
|
# this will re-normalize the raw data
|
||||||
self.create_item(job, item.index, item.data)
|
self.create_item(job, item.index, item.data)
|
||||||
return job
|
return job
|
||||||
|
|
||||||
def start_import(self, job): # pylint: disable=no-self-use
|
|
||||||
"""initalizes a csv import job"""
|
|
||||||
result = start_import_task.delay(job.id)
|
|
||||||
job.task_id = result.id
|
|
||||||
job.save()
|
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue="low_priority")
|
|
||||||
def start_import_task(job_id):
|
|
||||||
"""trigger the child tasks for each row"""
|
|
||||||
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
|
|
||||||
for item in job.items.values_list("id", flat=True).all():
|
|
||||||
import_item_task.delay(item)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue="low_priority")
|
|
||||||
def import_item_task(item_id):
|
|
||||||
"""resolve a row into a book"""
|
|
||||||
item = models.ImportItem.objects.get(id=item_id)
|
|
||||||
try:
|
|
||||||
item.resolve()
|
|
||||||
except Exception as err: # pylint: disable=broad-except
|
|
||||||
item.fail_reason = _("Error loading book")
|
|
||||||
item.save()
|
|
||||||
item.update_job()
|
|
||||||
raise err
|
|
||||||
|
|
||||||
if item.book:
|
|
||||||
# shelves book and handles reviews
|
|
||||||
handle_imported_book(item)
|
|
||||||
else:
|
|
||||||
item.fail_reason = _("Could not find a match for book")
|
|
||||||
|
|
||||||
item.save()
|
|
||||||
item.update_job()
|
|
||||||
|
|
||||||
|
|
||||||
def handle_imported_book(item):
|
|
||||||
"""process a csv and then post about it"""
|
|
||||||
job = item.job
|
|
||||||
user = job.user
|
|
||||||
if isinstance(item.book, models.Work):
|
|
||||||
item.book = item.book.default_edition
|
|
||||||
if not item.book:
|
|
||||||
item.fail_reason = _("Error loading book")
|
|
||||||
item.save()
|
|
||||||
return
|
|
||||||
if not isinstance(item.book, models.Edition):
|
|
||||||
item.book = item.book.edition
|
|
||||||
|
|
||||||
existing_shelf = models.ShelfBook.objects.filter(book=item.book, user=user).exists()
|
|
||||||
|
|
||||||
# shelve the book if it hasn't been shelved already
|
|
||||||
if item.shelf and not existing_shelf:
|
|
||||||
desired_shelf = models.Shelf.objects.get(identifier=item.shelf, user=user)
|
|
||||||
shelved_date = item.date_added or timezone.now()
|
|
||||||
models.ShelfBook(
|
|
||||||
book=item.book, shelf=desired_shelf, user=user, shelved_date=shelved_date
|
|
||||||
).save(priority=LOW)
|
|
||||||
|
|
||||||
for read in item.reads:
|
|
||||||
# check for an existing readthrough with the same dates
|
|
||||||
if models.ReadThrough.objects.filter(
|
|
||||||
user=user,
|
|
||||||
book=item.book,
|
|
||||||
start_date=read.start_date,
|
|
||||||
finish_date=read.finish_date,
|
|
||||||
).exists():
|
|
||||||
continue
|
|
||||||
read.book = item.book
|
|
||||||
read.user = user
|
|
||||||
read.save()
|
|
||||||
|
|
||||||
if job.include_reviews and (item.rating or item.review) and not item.linked_review:
|
|
||||||
# we don't know the publication date of the review,
|
|
||||||
# but "now" is a bad guess
|
|
||||||
published_date_guess = item.date_read or item.date_added
|
|
||||||
if item.review:
|
|
||||||
# pylint: disable=consider-using-f-string
|
|
||||||
review_title = "Review of {!r} on {!r}".format(
|
|
||||||
item.book.title,
|
|
||||||
job.source,
|
|
||||||
)
|
|
||||||
review = models.Review.objects.filter(
|
|
||||||
user=user,
|
|
||||||
book=item.book,
|
|
||||||
name=review_title,
|
|
||||||
rating=item.rating,
|
|
||||||
published_date=published_date_guess,
|
|
||||||
).first()
|
|
||||||
if not review:
|
|
||||||
review = models.Review(
|
|
||||||
user=user,
|
|
||||||
book=item.book,
|
|
||||||
name=review_title,
|
|
||||||
content=item.review,
|
|
||||||
rating=item.rating,
|
|
||||||
published_date=published_date_guess,
|
|
||||||
privacy=job.privacy,
|
|
||||||
)
|
|
||||||
review.save(software="bookwyrm", priority=LOW)
|
|
||||||
else:
|
|
||||||
# just a rating
|
|
||||||
review = models.ReviewRating.objects.filter(
|
|
||||||
user=user,
|
|
||||||
book=item.book,
|
|
||||||
published_date=published_date_guess,
|
|
||||||
rating=item.rating,
|
|
||||||
).first()
|
|
||||||
if not review:
|
|
||||||
review = models.ReviewRating(
|
|
||||||
user=user,
|
|
||||||
book=item.book,
|
|
||||||
rating=item.rating,
|
|
||||||
published_date=published_date_guess,
|
|
||||||
privacy=job.privacy,
|
|
||||||
)
|
|
||||||
review.save(software="bookwyrm", priority=LOW)
|
|
||||||
|
|
||||||
# only broadcast this review to other bookwyrm instances
|
|
||||||
item.linked_review = review
|
|
||||||
item.save()
|
|
||||||
|
|
32
bookwyrm/migrations/0160_auto_20221105_2030.py
Normal file
32
bookwyrm/migrations/0160_auto_20221105_2030.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 3.2.15 on 2022-11-05 20:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0159_auto_20220924_0634"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="importitem",
|
||||||
|
name="task_id",
|
||||||
|
field=models.CharField(blank=True, max_length=200, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="importjob",
|
||||||
|
name="status",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("pending", "Pending"),
|
||||||
|
("active", "Active"),
|
||||||
|
("complete", "Complete"),
|
||||||
|
("stopped", "Stopped"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
28
bookwyrm/migrations/0161_alter_importjob_status.py
Normal file
28
bookwyrm/migrations/0161_alter_importjob_status.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.2.15 on 2022-11-05 20:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0160_auto_20221105_2030"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="importjob",
|
||||||
|
name="status",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("pending", "Pending"),
|
||||||
|
("active", "Active"),
|
||||||
|
("complete", "Complete"),
|
||||||
|
("stopped", "Stopped"),
|
||||||
|
],
|
||||||
|
default="pending",
|
||||||
|
max_length=50,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
bookwyrm/migrations/0162_importjob_task_id.py
Normal file
18
bookwyrm/migrations/0162_importjob_task_id.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.15 on 2022-11-05 22:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0161_alter_importjob_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="importjob",
|
||||||
|
name="task_id",
|
||||||
|
field=models.CharField(blank=True, max_length=200, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -5,9 +5,21 @@ import dateutil.parser
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from bookwyrm.connectors import connector_manager
|
from bookwyrm.connectors import connector_manager
|
||||||
from bookwyrm.models import ReadThrough, User, Book, Edition
|
from bookwyrm.models import (
|
||||||
|
User,
|
||||||
|
Book,
|
||||||
|
Edition,
|
||||||
|
Work,
|
||||||
|
ShelfBook,
|
||||||
|
Shelf,
|
||||||
|
ReadThrough,
|
||||||
|
Review,
|
||||||
|
ReviewRating,
|
||||||
|
)
|
||||||
|
from bookwyrm.tasks import app, LOW
|
||||||
from .fields import PrivacyLevels
|
from .fields import PrivacyLevels
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +43,14 @@ def construct_search_term(title, author):
|
||||||
return " ".join([title, author])
|
return " ".join([title, author])
|
||||||
|
|
||||||
|
|
||||||
|
ImportStatuses = [
|
||||||
|
("pending", _("Pending")),
|
||||||
|
("active", _("Active")),
|
||||||
|
("complete", _("Complete")),
|
||||||
|
("stopped", _("Stopped")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ImportJob(models.Model):
|
class ImportJob(models.Model):
|
||||||
"""entry for a specific request for book data import"""
|
"""entry for a specific request for book data import"""
|
||||||
|
|
||||||
|
@ -39,10 +59,44 @@ class ImportJob(models.Model):
|
||||||
updated_date = models.DateTimeField(default=timezone.now)
|
updated_date = models.DateTimeField(default=timezone.now)
|
||||||
include_reviews = models.BooleanField(default=True)
|
include_reviews = models.BooleanField(default=True)
|
||||||
mappings = models.JSONField()
|
mappings = models.JSONField()
|
||||||
complete = models.BooleanField(default=False)
|
|
||||||
source = models.CharField(max_length=100)
|
source = models.CharField(max_length=100)
|
||||||
privacy = models.CharField(max_length=255, default="public", choices=PrivacyLevels)
|
privacy = models.CharField(max_length=255, default="public", choices=PrivacyLevels)
|
||||||
retry = models.BooleanField(default=False)
|
retry = models.BooleanField(default=False)
|
||||||
|
task_id = models.CharField(max_length=200, null=True, blank=True)
|
||||||
|
|
||||||
|
complete = models.BooleanField(default=False)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=50, choices=ImportStatuses, default="pending", null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_job(self):
|
||||||
|
"""Report that the job has started"""
|
||||||
|
task = start_import_task.delay(self.id)
|
||||||
|
self.task_id = task.id
|
||||||
|
|
||||||
|
self.status = "active"
|
||||||
|
self.save(update_fields=["status", "task_id"])
|
||||||
|
|
||||||
|
def complete_job(self):
|
||||||
|
"""Report that the job has completed"""
|
||||||
|
self.status = "complete"
|
||||||
|
self.complete = True
|
||||||
|
self.pending_items.update(fail_reason=_("Import stopped"))
|
||||||
|
self.save(update_fields=["status", "complete"])
|
||||||
|
|
||||||
|
def stop_job(self):
|
||||||
|
"""Stop the job"""
|
||||||
|
self.status = "stopped"
|
||||||
|
self.complete = True
|
||||||
|
self.save(update_fields=["status", "complete"])
|
||||||
|
self.pending_items.update(fail_reason=_("Import stopped"))
|
||||||
|
|
||||||
|
# stop starting
|
||||||
|
app.control.revoke(self.task_id, terminate=True)
|
||||||
|
tasks = self.pending_items.filter(task_id__isnull=False).values_list(
|
||||||
|
"task_id", flat=True
|
||||||
|
)
|
||||||
|
app.control.revoke(list(tasks))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pending_items(self):
|
def pending_items(self):
|
||||||
|
@ -97,15 +151,18 @@ class ImportItem(models.Model):
|
||||||
linked_review = models.ForeignKey(
|
linked_review = models.ForeignKey(
|
||||||
"Review", on_delete=models.SET_NULL, null=True, blank=True
|
"Review", on_delete=models.SET_NULL, null=True, blank=True
|
||||||
)
|
)
|
||||||
|
task_id = models.CharField(max_length=200, null=True, blank=True)
|
||||||
|
|
||||||
def update_job(self):
|
def update_job(self):
|
||||||
"""let the job know when the items get work done"""
|
"""let the job know when the items get work done"""
|
||||||
job = self.job
|
job = self.job
|
||||||
|
if job.complete:
|
||||||
|
return
|
||||||
|
|
||||||
job.updated_date = timezone.now()
|
job.updated_date = timezone.now()
|
||||||
job.save()
|
job.save()
|
||||||
if not job.pending_items.exists() and not job.complete:
|
if not job.pending_items.exists() and not job.complete:
|
||||||
job.complete = True
|
job.complete_job()
|
||||||
job.save(update_fields=["complete"])
|
|
||||||
|
|
||||||
def resolve(self):
|
def resolve(self):
|
||||||
"""try various ways to lookup a book"""
|
"""try various ways to lookup a book"""
|
||||||
|
@ -269,3 +326,136 @@ class ImportItem(models.Model):
|
||||||
return "{} by {}".format(
|
return "{} by {}".format(
|
||||||
self.normalized_data.get("title"), self.normalized_data.get("authors")
|
self.normalized_data.get("title"), self.normalized_data.get("authors")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(queue="low_priority")
|
||||||
|
def start_import_task(job_id):
|
||||||
|
"""trigger the child tasks for each row"""
|
||||||
|
job = ImportJob.objects.get(id=job_id)
|
||||||
|
# don't start the job if it was stopped from the UI
|
||||||
|
if job.complete:
|
||||||
|
return
|
||||||
|
|
||||||
|
# these are sub-tasks so that one big task doesn't use up all the memory in celery
|
||||||
|
for item in job.items.all():
|
||||||
|
task = import_item_task.delay(item.id)
|
||||||
|
item.task_id = task.id
|
||||||
|
item.save()
|
||||||
|
job.status = "active"
|
||||||
|
job.save()
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(queue="low_priority")
|
||||||
|
def import_item_task(item_id):
|
||||||
|
"""resolve a row into a book"""
|
||||||
|
item = ImportItem.objects.get(id=item_id)
|
||||||
|
# make sure the job has not been stopped
|
||||||
|
if item.job.complete:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
item.resolve()
|
||||||
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
item.fail_reason = _("Error loading book")
|
||||||
|
item.save()
|
||||||
|
item.update_job()
|
||||||
|
raise err
|
||||||
|
|
||||||
|
if item.book:
|
||||||
|
# shelves book and handles reviews
|
||||||
|
handle_imported_book(item)
|
||||||
|
else:
|
||||||
|
item.fail_reason = _("Could not find a match for book")
|
||||||
|
|
||||||
|
item.save()
|
||||||
|
item.update_job()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_imported_book(item):
|
||||||
|
"""process a csv and then post about it"""
|
||||||
|
job = item.job
|
||||||
|
if job.complete:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = job.user
|
||||||
|
if isinstance(item.book, Work):
|
||||||
|
item.book = item.book.default_edition
|
||||||
|
if not item.book:
|
||||||
|
item.fail_reason = _("Error loading book")
|
||||||
|
item.save()
|
||||||
|
return
|
||||||
|
if not isinstance(item.book, Edition):
|
||||||
|
item.book = item.book.edition
|
||||||
|
|
||||||
|
existing_shelf = ShelfBook.objects.filter(book=item.book, user=user).exists()
|
||||||
|
|
||||||
|
# shelve the book if it hasn't been shelved already
|
||||||
|
if item.shelf and not existing_shelf:
|
||||||
|
desired_shelf = Shelf.objects.get(identifier=item.shelf, user=user)
|
||||||
|
shelved_date = item.date_added or timezone.now()
|
||||||
|
ShelfBook(
|
||||||
|
book=item.book, shelf=desired_shelf, user=user, shelved_date=shelved_date
|
||||||
|
).save(priority=LOW)
|
||||||
|
|
||||||
|
for read in item.reads:
|
||||||
|
# check for an existing readthrough with the same dates
|
||||||
|
if ReadThrough.objects.filter(
|
||||||
|
user=user,
|
||||||
|
book=item.book,
|
||||||
|
start_date=read.start_date,
|
||||||
|
finish_date=read.finish_date,
|
||||||
|
).exists():
|
||||||
|
continue
|
||||||
|
read.book = item.book
|
||||||
|
read.user = user
|
||||||
|
read.save()
|
||||||
|
|
||||||
|
if job.include_reviews and (item.rating or item.review) and not item.linked_review:
|
||||||
|
# we don't know the publication date of the review,
|
||||||
|
# but "now" is a bad guess
|
||||||
|
published_date_guess = item.date_read or item.date_added
|
||||||
|
if item.review:
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
|
review_title = "Review of {!r} on {!r}".format(
|
||||||
|
item.book.title,
|
||||||
|
job.source,
|
||||||
|
)
|
||||||
|
review = Review.objects.filter(
|
||||||
|
user=user,
|
||||||
|
book=item.book,
|
||||||
|
name=review_title,
|
||||||
|
rating=item.rating,
|
||||||
|
published_date=published_date_guess,
|
||||||
|
).first()
|
||||||
|
if not review:
|
||||||
|
review = Review(
|
||||||
|
user=user,
|
||||||
|
book=item.book,
|
||||||
|
name=review_title,
|
||||||
|
content=item.review,
|
||||||
|
rating=item.rating,
|
||||||
|
published_date=published_date_guess,
|
||||||
|
privacy=job.privacy,
|
||||||
|
)
|
||||||
|
review.save(software="bookwyrm", priority=LOW)
|
||||||
|
else:
|
||||||
|
# just a rating
|
||||||
|
review = ReviewRating.objects.filter(
|
||||||
|
user=user,
|
||||||
|
book=item.book,
|
||||||
|
published_date=published_date_guess,
|
||||||
|
rating=item.rating,
|
||||||
|
).first()
|
||||||
|
if not review:
|
||||||
|
review = ReviewRating(
|
||||||
|
user=user,
|
||||||
|
book=item.book,
|
||||||
|
rating=item.rating,
|
||||||
|
published_date=published_date_guess,
|
||||||
|
privacy=job.privacy,
|
||||||
|
)
|
||||||
|
review.save(software="bookwyrm", priority=LOW)
|
||||||
|
|
||||||
|
# only broadcast this review to other bookwyrm instances
|
||||||
|
item.linked_review = review
|
||||||
|
item.save()
|
||||||
|
|
|
@ -120,17 +120,26 @@
|
||||||
<td>{{ job.updated_date }}</td>
|
<td>{{ job.updated_date }}</td>
|
||||||
<td>{{ job.item_count|intcomma }}</td>
|
<td>{{ job.item_count|intcomma }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if job.complete %}
|
<span
|
||||||
<span class="tag is-success">
|
{% if job.status == "stopped" %}
|
||||||
{% trans "Completed" %}
|
class="tag is-danger"
|
||||||
|
{% elif job.status == "pending" %}
|
||||||
|
class="tag is-warning"
|
||||||
|
{% elif job.complete %}
|
||||||
|
class="tag"
|
||||||
|
{% else %}
|
||||||
|
class="tag is-success"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% if job.status %}
|
||||||
|
{{ job.status }}
|
||||||
|
{{ job.status_display }}
|
||||||
|
{% elif job.complete %}
|
||||||
|
{% trans "Complete" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Active" %}
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
|
||||||
<span class="tag is-warning">
|
|
||||||
{% blocktrans trimmed with percent=job.percent_complete %}
|
|
||||||
Active, {{ percent }}% complete
|
|
||||||
{% endblocktrans %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -66,6 +66,13 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not job.complete %}
|
||||||
|
<form name="stop-import" action="{% url 'import-stop' job.id %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="button is-danger" type="submit">{% trans "Stop import" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if manual_review_count and not legacy %}
|
{% if manual_review_count and not legacy %}
|
||||||
<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 %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'components/modal.html' %}
|
{% extends 'components/modal.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block modal-title %}{% trans "Mark import as complete?" %}{% endblock %}
|
{% block modal-title %}{% trans "Stop import?" %}{% endblock %}
|
||||||
|
|
||||||
{% block modal-body %}
|
{% block modal-body %}
|
||||||
{% trans "This action cannot be un-done" %}
|
{% trans "This action cannot be un-done" %}
|
||||||
|
|
|
@ -27,11 +27,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-container block content">
|
<div class="table-container block content">
|
||||||
{% if status == "active" %}
|
|
||||||
<div class="notification is-warning">
|
|
||||||
<p>{% trans "Marking an import as complete will <em>not</em> stop it." %}</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<table class="table is-striped is-fullwidth">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
{% url 'settings-imports' as url %}
|
{% url 'settings-imports' as url %}
|
||||||
|
@ -82,7 +77,7 @@
|
||||||
{% if status == "active" %}
|
{% if status == "active" %}
|
||||||
<td>
|
<td>
|
||||||
{% join "complete" import.id as modal_id %}
|
{% join "complete" import.id as modal_id %}
|
||||||
<button type="button" data-modal-open="{{ modal_id }}" class="button is-danger">{% trans "Mark as complete" %}</button>
|
<button type="button" data-modal-open="{{ modal_id }}" class="button is-danger">{% trans "Stop import" %}</button>
|
||||||
{% include "settings/imports/complete_import_modal.html" with id=modal_id %}
|
{% include "settings/imports/complete_import_modal.html" with id=modal_id %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -91,7 +86,7 @@
|
||||||
{% if not imports %}
|
{% if not imports %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<em>{% trans "No matching imports founds." %} </em>
|
<em>{% trans "No matching imports found." %} </em>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.test import TestCase
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.importers import CalibreImporter
|
from bookwyrm.importers import CalibreImporter
|
||||||
from bookwyrm.importers.importer import handle_imported_book
|
from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=consider-using-with
|
# pylint: disable=consider-using-with
|
||||||
|
@ -16,6 +16,7 @@ from bookwyrm.importers.importer import handle_imported_book
|
||||||
class CalibreImport(TestCase):
|
class CalibreImport(TestCase):
|
||||||
"""importing from Calibre csv"""
|
"""importing from Calibre csv"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""use a test csv"""
|
"""use a test csv"""
|
||||||
self.importer = CalibreImporter()
|
self.importer = CalibreImporter()
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.test import TestCase
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.importers import GoodreadsImporter
|
from bookwyrm.importers import GoodreadsImporter
|
||||||
from bookwyrm.importers.importer import handle_imported_book
|
from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
|
@ -23,6 +23,7 @@ def make_date(*args):
|
||||||
class GoodreadsImport(TestCase):
|
class GoodreadsImport(TestCase):
|
||||||
"""importing from goodreads csv"""
|
"""importing from goodreads csv"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""use a test csv"""
|
"""use a test csv"""
|
||||||
self.importer = GoodreadsImporter()
|
self.importer = GoodreadsImporter()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
""" testing import """
|
""" testing import """
|
||||||
|
from collections import namedtuple
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import datetime
|
import datetime
|
||||||
|
@ -9,8 +10,8 @@ import responses
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.importers import Importer
|
from bookwyrm.importers import Importer
|
||||||
from bookwyrm.importers.importer import start_import_task, import_item_task
|
from bookwyrm.models.import_job import start_import_task, import_item_task
|
||||||
from bookwyrm.importers.importer import handle_imported_book
|
from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
|
@ -25,6 +26,7 @@ def make_date(*args):
|
||||||
class GenericImporter(TestCase):
|
class GenericImporter(TestCase):
|
||||||
"""importing from csv"""
|
"""importing from csv"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""use a test csv"""
|
"""use a test csv"""
|
||||||
|
|
||||||
|
@ -103,9 +105,13 @@ class GenericImporter(TestCase):
|
||||||
import_job = self.importer.create_job(
|
import_job = self.importer.create_job(
|
||||||
self.local_user, self.csv, False, "unlisted"
|
self.local_user, self.csv, False, "unlisted"
|
||||||
)
|
)
|
||||||
with patch("bookwyrm.importers.importer.start_import_task.delay") as mock:
|
MockTask = namedtuple("Task", ("id"))
|
||||||
self.importer.start_import(import_job)
|
with patch("bookwyrm.models.import_job.start_import_task.delay") as mock:
|
||||||
|
mock.return_value = MockTask(123)
|
||||||
|
import_job.start_job()
|
||||||
self.assertEqual(mock.call_count, 1)
|
self.assertEqual(mock.call_count, 1)
|
||||||
|
import_job.refresh_from_db()
|
||||||
|
self.assertEqual(import_job.task_id, "123")
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_start_import_task(self, *_):
|
def test_start_import_task(self, *_):
|
||||||
|
@ -114,7 +120,9 @@ class GenericImporter(TestCase):
|
||||||
self.local_user, self.csv, False, "unlisted"
|
self.local_user, self.csv, False, "unlisted"
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("bookwyrm.importers.importer.import_item_task.delay") as mock:
|
MockTask = namedtuple("Task", ("id"))
|
||||||
|
with patch("bookwyrm.models.import_job.import_item_task.delay") as mock:
|
||||||
|
mock.return_value = MockTask(123)
|
||||||
start_import_task(import_job.id)
|
start_import_task(import_job.id)
|
||||||
|
|
||||||
self.assertEqual(mock.call_count, 4)
|
self.assertEqual(mock.call_count, 4)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.test import TestCase
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.importers import LibrarythingImporter
|
from bookwyrm.importers import LibrarythingImporter
|
||||||
from bookwyrm.importers.importer import handle_imported_book
|
from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
|
@ -23,6 +23,7 @@ def make_date(*args):
|
||||||
class LibrarythingImport(TestCase):
|
class LibrarythingImport(TestCase):
|
||||||
"""importing from librarything tsv"""
|
"""importing from librarything tsv"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""use a test tsv"""
|
"""use a test tsv"""
|
||||||
self.importer = LibrarythingImporter()
|
self.importer = LibrarythingImporter()
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.test import TestCase
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.importers import OpenLibraryImporter
|
from bookwyrm.importers import OpenLibraryImporter
|
||||||
from bookwyrm.importers.importer import handle_imported_book
|
from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
|
@ -23,6 +23,7 @@ def make_date(*args):
|
||||||
class OpenLibraryImport(TestCase):
|
class OpenLibraryImport(TestCase):
|
||||||
"""importing from openlibrary csv"""
|
"""importing from openlibrary csv"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""use a test csv"""
|
"""use a test csv"""
|
||||||
self.importer = OpenLibraryImporter()
|
self.importer = OpenLibraryImporter()
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.test import TestCase
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.importers import StorygraphImporter
|
from bookwyrm.importers import StorygraphImporter
|
||||||
from bookwyrm.importers.importer import handle_imported_book
|
from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
|
@ -23,6 +23,7 @@ def make_date(*args):
|
||||||
class StorygraphImport(TestCase):
|
class StorygraphImport(TestCase):
|
||||||
"""importing from storygraph csv"""
|
"""importing from storygraph csv"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""use a test csv"""
|
"""use a test csv"""
|
||||||
self.importer = StorygraphImporter()
|
self.importer = StorygraphImporter()
|
||||||
|
|
|
@ -14,6 +14,7 @@ from bookwyrm.tests.validate_html import validate_html
|
||||||
class ImportViews(TestCase):
|
class ImportViews(TestCase):
|
||||||
"""goodreads import views"""
|
"""goodreads import views"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""we need basic test data and mocks"""
|
"""we need basic test data and mocks"""
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
@ -84,7 +85,7 @@ class ImportViews(TestCase):
|
||||||
request = self.factory.post("", form.data)
|
request = self.factory.post("", form.data)
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
|
|
||||||
with patch("bookwyrm.importers.Importer.start_import"):
|
with patch("bookwyrm.models.import_job.ImportJob.start_job"):
|
||||||
view(request)
|
view(request)
|
||||||
job = models.ImportJob.objects.get()
|
job = models.ImportJob.objects.get()
|
||||||
self.assertFalse(job.include_reviews)
|
self.assertFalse(job.include_reviews)
|
||||||
|
@ -102,6 +103,6 @@ class ImportViews(TestCase):
|
||||||
)
|
)
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
with patch("bookwyrm.importers.importer.import_item_task.delay") as mock:
|
with patch("bookwyrm.models.import_job.import_item_task.delay") as mock:
|
||||||
views.retry_item(request, job.id, item.id)
|
views.retry_item(request, job.id, item.id)
|
||||||
self.assertEqual(mock.call_count, 1)
|
self.assertEqual(mock.call_count, 1)
|
||||||
|
|
|
@ -11,6 +11,7 @@ from bookwyrm import models, views
|
||||||
class ImportManualReviewViews(TestCase):
|
class ImportManualReviewViews(TestCase):
|
||||||
"""goodreads import views"""
|
"""goodreads import views"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""we need basic test data and mocks"""
|
"""we need basic test data and mocks"""
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
@ -59,7 +60,7 @@ class ImportManualReviewViews(TestCase):
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
|
|
||||||
with patch("bookwyrm.importers.importer.import_item_task.delay") as mock:
|
with patch("bookwyrm.models.import_job.import_item_task.delay") as mock:
|
||||||
views.approve_import_item(request, self.job.id, import_item.id)
|
views.approve_import_item(request, self.job.id, import_item.id)
|
||||||
self.assertEqual(mock.call_count, 1)
|
self.assertEqual(mock.call_count, 1)
|
||||||
import_item.refresh_from_db()
|
import_item.refresh_from_db()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
""" test for app action functionality """
|
""" test for app action functionality """
|
||||||
|
from collections import namedtuple
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -11,6 +12,7 @@ from bookwyrm import models, views
|
||||||
class ImportTroubleshootViews(TestCase):
|
class ImportTroubleshootViews(TestCase):
|
||||||
"""goodreads import views"""
|
"""goodreads import views"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""we need basic test data and mocks"""
|
"""we need basic test data and mocks"""
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
@ -48,7 +50,9 @@ class ImportTroubleshootViews(TestCase):
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
|
|
||||||
with patch("bookwyrm.importers.Importer.start_import"):
|
MockTask = namedtuple("Task", ("id"))
|
||||||
|
with patch("bookwyrm.models.import_job.start_import_task.delay") as mock:
|
||||||
|
mock.return_value = MockTask(123)
|
||||||
view(request, import_job.id)
|
view(request, import_job.id)
|
||||||
|
|
||||||
self.assertEqual(models.ImportJob.objects.count(), 2)
|
self.assertEqual(models.ImportJob.objects.count(), 2)
|
||||||
|
|
|
@ -353,6 +353,11 @@ urlpatterns = [
|
||||||
views.ImportStatus.as_view(),
|
views.ImportStatus.as_view(),
|
||||||
name="import-status",
|
name="import-status",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^import/(?P<job_id>\d+)/stop/?$",
|
||||||
|
views.stop_import,
|
||||||
|
name="import-stop",
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^import/(?P<job_id>\d+)/retry/(?P<item_id>\d+)/?$",
|
r"^import/(?P<job_id>\d+)/retry/(?P<item_id>\d+)/?$",
|
||||||
views.retry_item,
|
views.retry_item,
|
||||||
|
|
|
@ -74,7 +74,7 @@ from .shelf.shelf_actions import shelve, unshelve
|
||||||
|
|
||||||
# csv import
|
# csv import
|
||||||
from .imports.import_data import Import
|
from .imports.import_data import Import
|
||||||
from .imports.import_status import ImportStatus, retry_item
|
from .imports.import_status import ImportStatus, retry_item, stop_import
|
||||||
from .imports.troubleshoot import ImportTroubleshoot
|
from .imports.troubleshoot import ImportTroubleshoot
|
||||||
from .imports.manually_review import (
|
from .imports.manually_review import (
|
||||||
ImportManualReview,
|
ImportManualReview,
|
||||||
|
|
|
@ -40,6 +40,5 @@ class ImportList(View):
|
||||||
def post(self, request, import_id):
|
def post(self, request, import_id):
|
||||||
"""Mark an import as complete"""
|
"""Mark an import as complete"""
|
||||||
import_job = get_object_or_404(models.ImportJob, id=import_id)
|
import_job = get_object_or_404(models.ImportJob, id=import_id)
|
||||||
import_job.complete = True
|
import_job.stop_job()
|
||||||
import_job.save()
|
|
||||||
return redirect("settings-imports")
|
return redirect("settings-imports")
|
||||||
|
|
|
@ -97,6 +97,6 @@ class Import(View):
|
||||||
except (UnicodeDecodeError, ValueError, KeyError):
|
except (UnicodeDecodeError, ValueError, KeyError):
|
||||||
return HttpResponseBadRequest(_("Not a valid csv file"))
|
return HttpResponseBadRequest(_("Not a valid csv file"))
|
||||||
|
|
||||||
importer.start_import(job)
|
job.start_job()
|
||||||
|
|
||||||
return redirect(f"/import/{job.id}")
|
return redirect(f"/import/{job.id}")
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.importers import GoodreadsImporter
|
from bookwyrm.importers import GoodreadsImporter
|
||||||
from bookwyrm.importers.importer import import_item_task
|
from bookwyrm.models.import_job 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
|
||||||
|
@ -74,3 +74,12 @@ def retry_item(request, job_id, item_id):
|
||||||
)
|
)
|
||||||
import_item_task.delay(item.id)
|
import_item_task.delay(item.id)
|
||||||
return redirect("import-status", job_id)
|
return redirect("import-status", job_id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def stop_import(request, job_id):
|
||||||
|
"""scrap that"""
|
||||||
|
job = get_object_or_404(models.ImportJob, id=job_id, user=request.user)
|
||||||
|
job.stop_job()
|
||||||
|
return redirect("import-status", job_id)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.views import View
|
||||||
from django.views.decorators.http import require_POST
|
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.models.import_job 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
|
||||||
|
|
|
@ -52,5 +52,5 @@ class ImportTroubleshoot(View):
|
||||||
job,
|
job,
|
||||||
items,
|
items,
|
||||||
)
|
)
|
||||||
importer.start_import(job)
|
job.start_job()
|
||||||
return redirect(f"/import/{job.id}")
|
return redirect(f"/import/{job.id}")
|
||||||
|
|
Loading…
Reference in a new issue