""" handle reading a csv from an external service, defaults are from GoodReads """ import csv import logging from django.utils import timezone from bookwyrm import models from bookwyrm.models import ImportJob, ImportItem from bookwyrm.tasks import app logger = logging.getLogger(__name__) class Importer: """Generic class for csv data import from an outside service""" service = "Unknown" delimiter = "," encoding = "UTF-8" mandatory_fields = ["Title", "Author"] def create_job(self, user, csv_file, include_reviews, privacy): """check over a csv and creates a database entry for the job""" job = ImportJob.objects.create( user=user, include_reviews=include_reviews, privacy=privacy ) for index, entry in enumerate( list(csv.DictReader(csv_file, delimiter=self.delimiter)) ): if not all(x in entry for x in self.mandatory_fields): raise ValueError("Author and title must be in data.") entry = self.parse_fields(entry) self.save_item(job, index, entry) return job def save_item(self, job, index, data): # pylint: disable=no-self-use """creates and saves an import item""" ImportItem(job=job, index=index, data=data).save() def parse_fields(self, entry): """updates csv data with additional info""" entry.update({"import_source": self.service}) return entry def create_retry_job(self, user, original_job, items): """retry items that didn't import""" job = ImportJob.objects.create( user=user, include_reviews=original_job.include_reviews, privacy=original_job.privacy, retry=True, ) for item in items: self.save_item(job, item.index, item.data) return job def start_import(self, job): """initalizes a csv import job""" result = import_data.delay(self.service, job.id) job.task_id = result.id job.save() @app.task def import_data(source, job_id): """does the actual lookup work in a celery task""" job = ImportJob.objects.get(id=job_id) try: for item in job.items.all(): try: item.resolve() except Exception as err: # pylint: disable=broad-except logger.exception(err) item.fail_reason = "Error loading book" item.save() continue if item.book: item.save() # shelves book and handles reviews handle_imported_book( source, job.user, item, job.include_reviews, job.privacy ) else: item.fail_reason = "Could not find a match for book" item.save() finally: job.complete = True job.save() def handle_imported_book(source, user, item, include_reviews, privacy): """process a csv and then post about it""" if isinstance(item.book, models.Work): item.book = item.book.default_edition if not item.book: return 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.objects.create( book=item.book, shelf=desired_shelf, user=user, shelved_date=shelved_date ) 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 include_reviews and (item.rating or item.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: review_title = ( "Review of {!r} on {!r}".format( item.book.title, source, ) if item.review else "" ) models.Review.objects.create( user=user, book=item.book, name=review_title, content=item.review, rating=item.rating, published_date=published_date_guess, privacy=privacy, ) else: # just a rating models.ReviewRating.objects.create( user=user, book=item.book, rating=item.rating, published_date=published_date_guess, privacy=privacy, )