Merge branch 'main' into isni-poc

This commit is contained in:
Hugh Rundle 2021-11-22 08:53:58 +11:00
commit a218fa21ea
142 changed files with 14141 additions and 3230 deletions

View file

@ -36,7 +36,7 @@ FLOWER_PORT=8888
#FLOWER_USER=mouse #FLOWER_USER=mouse
#FLOWER_PASSWORD=changeme #FLOWER_PASSWORD=changeme
EMAIL_HOST="smtp.mailgun.org" EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_USER=mail@your.domain.here
EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_HOST_PASSWORD=emailpassword123

View file

@ -36,7 +36,7 @@ FLOWER_PORT=8888
FLOWER_USER=mouse FLOWER_USER=mouse
FLOWER_PASSWORD=changeme FLOWER_PASSWORD=changeme
EMAIL_HOST="smtp.mailgun.org" EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_USER=mail@your.domain.here
EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_HOST_PASSWORD=emailpassword123

View file

@ -7,7 +7,7 @@ from django.utils import timezone
from bookwyrm import models from bookwyrm import models
from bookwyrm.redis_store import RedisStore, r from bookwyrm.redis_store import RedisStore, r
from bookwyrm.tasks import app from bookwyrm.tasks import app, LOW, MEDIUM, HIGH
class ActivityStream(RedisStore): class ActivityStream(RedisStore):
@ -277,7 +277,18 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
def add_status_on_create_command(sender, instance, created): def add_status_on_create_command(sender, instance, created):
"""runs this code only after the database commit completes""" """runs this code only after the database commit completes"""
add_status_task.delay(instance.id, increment_unread=created) priority = HIGH
# check if this is an old status, de-prioritize if so
# (this will happen if federation is very slow, or, more expectedly, on csv import)
one_day = 60 * 60 * 24
if (instance.created_date - instance.published_date).seconds > one_day:
priority = LOW
add_status_task.apply_async(
args=(instance.id,),
kwargs={"increment_unread": created},
queue=priority,
)
if sender == models.Boost: if sender == models.Boost:
handle_boost_task.delay(instance.id) handle_boost_task.delay(instance.id)
@ -409,7 +420,7 @@ def remove_statuses_on_unshelve(sender, instance, *args, **kwargs):
# ---- TASKS # ---- TASKS
@app.task(queue="low_priority") @app.task(queue=LOW)
def add_book_statuses_task(user_id, book_id): def add_book_statuses_task(user_id, book_id):
"""add statuses related to a book on shelve""" """add statuses related to a book on shelve"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
@ -417,7 +428,7 @@ def add_book_statuses_task(user_id, book_id):
BooksStream().add_book_statuses(user, book) BooksStream().add_book_statuses(user, book)
@app.task(queue="low_priority") @app.task(queue=LOW)
def remove_book_statuses_task(user_id, book_id): def remove_book_statuses_task(user_id, book_id):
"""remove statuses about a book from a user's books feed""" """remove statuses about a book from a user's books feed"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
@ -425,7 +436,7 @@ def remove_book_statuses_task(user_id, book_id):
BooksStream().remove_book_statuses(user, book) BooksStream().remove_book_statuses(user, book)
@app.task(queue="medium_priority") @app.task(queue=MEDIUM)
def populate_stream_task(stream, user_id): def populate_stream_task(stream, user_id):
"""background task for populating an empty activitystream""" """background task for populating an empty activitystream"""
user = models.User.objects.get(id=user_id) user = models.User.objects.get(id=user_id)
@ -433,7 +444,7 @@ def populate_stream_task(stream, user_id):
stream.populate_streams(user) stream.populate_streams(user)
@app.task(queue="medium_priority") @app.task(queue=MEDIUM)
def remove_status_task(status_ids): def remove_status_task(status_ids):
"""remove a status from any stream it might be in""" """remove a status from any stream it might be in"""
# this can take an id or a list of ids # this can take an id or a list of ids
@ -446,7 +457,7 @@ def remove_status_task(status_ids):
stream.remove_object_from_related_stores(status) stream.remove_object_from_related_stores(status)
@app.task(queue="high_priority") @app.task(queue=HIGH)
def add_status_task(status_id, increment_unread=False): def add_status_task(status_id, increment_unread=False):
"""add a status to any stream it should be in""" """add a status to any stream it should be in"""
status = models.Status.objects.get(id=status_id) status = models.Status.objects.get(id=status_id)
@ -458,7 +469,7 @@ def add_status_task(status_id, increment_unread=False):
stream.add_status(status, increment_unread=increment_unread) stream.add_status(status, increment_unread=increment_unread)
@app.task(queue="medium_priority") @app.task(queue=MEDIUM)
def remove_user_statuses_task(viewer_id, user_id, stream_list=None): def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
"""remove all statuses by a user from a viewer's stream""" """remove all statuses by a user from a viewer's stream"""
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values() stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
@ -468,7 +479,7 @@ def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
stream.remove_user_statuses(viewer, user) stream.remove_user_statuses(viewer, user)
@app.task(queue="medium_priority") @app.task(queue=MEDIUM)
def add_user_statuses_task(viewer_id, user_id, stream_list=None): def add_user_statuses_task(viewer_id, user_id, stream_list=None):
"""add all statuses by a user to a viewer's stream""" """add all statuses by a user to a viewer's stream"""
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values() stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
@ -478,7 +489,7 @@ def add_user_statuses_task(viewer_id, user_id, stream_list=None):
stream.add_user_statuses(viewer, user) stream.add_user_statuses(viewer, user)
@app.task(queue="medium_priority") @app.task(queue=MEDIUM)
def handle_boost_task(boost_id): def handle_boost_task(boost_id):
"""remove the original post and other, earlier boosts""" """remove the original post and other, earlier boosts"""
instance = models.Status.objects.get(id=boost_id) instance = models.Status.objects.get(id=boost_id)

View file

@ -82,6 +82,8 @@ def search_identifiers(query, *filters, return_first=False):
*filters, reduce(operator.or_, (Q(**f) for f in or_filters)) *filters, reduce(operator.or_, (Q(**f) for f in or_filters))
).distinct() ).distinct()
if results.count() <= 1: if results.count() <= 1:
if return_first:
return results.first()
return results return results
# when there are multiple editions of the same work, pick the default. # when there are multiple editions of the same work, pick the default.
@ -124,6 +126,7 @@ def search_title_author(query, min_confidence, *filters, return_first=False):
result = default result = default
else: else:
result = editions.first() result = editions.first()
if return_first: if return_first:
return result return result
list_results.append(result) list_results.append(result)

View file

@ -67,7 +67,7 @@ class Connector(AbstractConnector):
extracted = list(data.get("entities").values()) extracted = list(data.get("entities").values())
try: try:
data = extracted[0] data = extracted[0]
except KeyError: except (KeyError, IndexError):
raise ConnectorException("Invalid book data") raise ConnectorException("Invalid book data")
# flatten the data so that images, uri, and claims are on the same level # flatten the data so that images, uri, and claims are on the same level
return { return {
@ -128,6 +128,7 @@ class Connector(AbstractConnector):
def load_edition_data(self, work_uri): def load_edition_data(self, work_uri):
"""get a list of editions for a work""" """get a list of editions for a work"""
# pylint: disable=line-too-long
url = f"{self.books_url}?action=reverse-claims&property=wdt:P629&value={work_uri}&sort=true" url = f"{self.books_url}?action=reverse-claims&property=wdt:P629&value={work_uri}&sort=true"
return get_data(url) return get_data(url)

View file

@ -10,14 +10,9 @@ from bookwyrm.settings import DOMAIN
def email_data(): def email_data():
"""fields every email needs""" """fields every email needs"""
site = models.SiteSettings.objects.get() site = models.SiteSettings.objects.get()
if site.logo_small:
logo_path = f"/images/{site.logo_small.url}"
else:
logo_path = "/static/images/logo-small.png"
return { return {
"site_name": site.name, "site_name": site.name,
"logo": logo_path, "logo": site.logo_small_url,
"domain": DOMAIN, "domain": DOMAIN,
"user": None, "user": None,
} }
@ -46,6 +41,18 @@ def password_reset_email(reset_code):
send_email.delay(reset_code.user.email, *format_email("password_reset", data)) send_email.delay(reset_code.user.email, *format_email("password_reset", data))
def moderation_report_email(report):
"""a report was created"""
data = email_data()
data["reporter"] = report.reporter.localname or report.reporter.username
data["reportee"] = report.user.localname or report.user.username
data["report_link"] = report.remote_id
for admin in models.User.objects.filter(groups__name__in=["admin", "moderator"]):
data["user"] = admin.display_name
send_email.delay(admin.email, *format_email("moderation_report", data))
def format_email(email_name, data): def format_email(email_name, data):
"""render the email templates""" """render the email templates"""
subject = get_template(f"email/{email_name}/subject.html").render(data).strip() subject = get_template(f"email/{email_name}/subject.html").render(data).strip()

View file

@ -201,12 +201,18 @@ class EditionForm(CustomForm):
class AuthorForm(CustomForm): class AuthorForm(CustomForm):
class Meta: class Meta:
model = models.Author model = models.Author
exclude = [ fields = [
"remote_id", "last_edited_by",
"origin_id", "name",
"created_date", "aliases",
"updated_date", "bio",
"search_vector", "wikipedia_link",
"born",
"died",
"openlibrary_key",
"inventaire_id",
"librarything_key",
"goodreads_key",
] ]

View file

@ -7,10 +7,3 @@ class GoodreadsImporter(Importer):
For a more complete example of overriding see librarything_import.py""" For a more complete example of overriding see librarything_import.py"""
service = "Goodreads" service = "Goodreads"
def parse_fields(self, entry):
"""handle the specific fields in goodreads csvs"""
entry.update({"import_source": self.service})
# add missing 'Date Started' field
entry.update({"Date Started": None})
return entry

View file

@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
from bookwyrm import models from bookwyrm import models
from bookwyrm.models import ImportJob, ImportItem from bookwyrm.models import ImportJob, ImportItem
from bookwyrm.tasks import app from bookwyrm.tasks import app, LOW
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,33 +15,90 @@ 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"
mandatory_fields = ["Title", "Author"]
# these are from Goodreads
row_mappings_guesses = [
("id", ["id", "book id"]),
("title", ["title"]),
("authors", ["author", "authors", "primary author"]),
("isbn_10", ["isbn10", "isbn"]),
("isbn_13", ["isbn13", "isbn", "isbns"]),
("shelf", ["shelf", "exclusive shelf", "read status"]),
("review_name", ["review name"]),
("review_body", ["my review", "review"]),
("rating", ["my rating", "rating", "star rating"]),
("date_added", ["date added", "entry date", "added"]),
("date_started", ["date started", "started"]),
("date_finished", ["date finished", "last date read", "date read", "finished"]),
]
date_fields = ["date_added", "date_started", "date_finished"]
shelf_mapping_guesses = {
"to-read": ["to-read"],
"read": ["read"],
"reading": ["currently-reading", "reading"],
}
def create_job(self, user, csv_file, include_reviews, privacy): def create_job(self, user, csv_file, include_reviews, privacy):
"""check over a csv and creates a database entry for the job""" """check over a csv and creates a database entry for the job"""
csv_reader = csv.DictReader(csv_file, delimiter=self.delimiter)
rows = enumerate(list(csv_reader))
job = ImportJob.objects.create( job = ImportJob.objects.create(
user=user, include_reviews=include_reviews, privacy=privacy user=user,
include_reviews=include_reviews,
privacy=privacy,
mappings=self.create_row_mappings(csv_reader.fieldnames),
source=self.service,
) )
for index, entry in enumerate(
list(csv.DictReader(csv_file, delimiter=self.delimiter)) for index, entry in rows:
): self.create_item(job, index, entry)
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 return job
def save_item(self, job, index, data): # pylint: disable=no-self-use def update_legacy_job(self, job):
"""creates and saves an import item""" """patch up a job that was in the old format"""
ImportItem(job=job, index=index, data=data).save() items = job.items
headers = list(items.first().data.keys())
job.mappings = self.create_row_mappings(headers)
job.updated_date = timezone.now()
job.save()
def parse_fields(self, entry): for item in items.all():
"""updates csv data with additional info""" normalized = self.normalize_row(item.data, job.mappings)
entry.update({"import_source": self.service}) normalized["shelf"] = self.get_shelf(normalized)
return entry item.normalized_data = normalized
item.save()
def create_row_mappings(self, headers):
"""guess what the headers mean"""
mappings = {}
for (key, guesses) in self.row_mappings_guesses:
value = [h for h in headers if h.lower() in guesses]
value = value[0] if len(value) else None
if value:
headers.remove(value)
mappings[key] = value
return mappings
def create_item(self, job, index, data):
"""creates and saves an import item"""
normalized = self.normalize_row(data, job.mappings)
normalized["shelf"] = self.get_shelf(normalized)
ImportItem(job=job, index=index, data=data, normalized_data=normalized).save()
def get_shelf(self, normalized_row):
"""determine which shelf to use"""
shelf_name = normalized_row["shelf"]
shelf = [
s for (s, gs) in self.shelf_mapping_guesses.items() if shelf_name in gs
]
return shelf[0] if shelf else None
def normalize_row(self, entry, mappings): # pylint: disable=no-self-use
"""use the dataclass to create the formatted row of data"""
return {k: entry.get(v) for k, v in mappings.items()}
def create_retry_job(self, user, original_job, items): def create_retry_job(self, user, original_job, items):
"""retry items that didn't import""" """retry items that didn't import"""
@ -49,55 +106,65 @@ class Importer:
user=user, user=user,
include_reviews=original_job.include_reviews, include_reviews=original_job.include_reviews,
privacy=original_job.privacy, privacy=original_job.privacy,
# TODO: allow users to adjust mappings
mappings=original_job.mappings,
retry=True, retry=True,
) )
for item in items: for item in items:
self.save_item(job, item.index, item.data) # this will re-normalize the raw data
self.create_item(job, item.index, item.data)
return job return job
def start_import(self, job): def start_import(self, job): # pylint: disable=no-self-use
"""initalizes a csv import job""" """initalizes a csv import job"""
result = import_data.delay(self.service, job.id) result = start_import_task.delay(job.id)
job.task_id = result.id job.task_id = result.id
job.save() job.save()
@app.task(queue="low_priority") @app.task(queue="low_priority")
def import_data(source, job_id): def start_import_task(job_id):
"""does the actual lookup work in a celery task""" """trigger the child tasks for each row"""
job = ImportJob.objects.get(id=job_id) job = ImportJob.objects.get(id=job_id)
try: # these are sub-tasks so that one big task doesn't use up all the memory in celery
for item in job.items.all(): 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: try:
item.resolve() item.resolve()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
logger.exception(err)
item.fail_reason = _("Error loading book") item.fail_reason = _("Error loading book")
item.save() item.save()
continue item.update_job()
raise err
if item.book or item.book_guess:
item.save()
if item.book: if item.book:
# shelves book and handles reviews # shelves book and handles reviews
handle_imported_book( handle_imported_book(item)
source, job.user, item, job.include_reviews, job.privacy
)
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()
finally: item.update_job()
job.complete = True
job.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:
item.fail_reason = _("Error loading book")
item.save()
return 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() existing_shelf = models.ShelfBook.objects.filter(book=item.book, user=user).exists()
@ -105,9 +172,9 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
if item.shelf and not existing_shelf: if item.shelf and not existing_shelf:
desired_shelf = models.Shelf.objects.get(identifier=item.shelf, user=user) desired_shelf = models.Shelf.objects.get(identifier=item.shelf, user=user)
shelved_date = item.date_added or timezone.now() shelved_date = item.date_added or timezone.now()
models.ShelfBook.objects.create( models.ShelfBook(
book=item.book, shelf=desired_shelf, user=user, shelved_date=shelved_date book=item.book, shelf=desired_shelf, user=user, shelved_date=shelved_date
) ).save(priority=LOW)
for read in item.reads: for read in item.reads:
# check for an existing readthrough with the same dates # check for an existing readthrough with the same dates
@ -122,35 +189,52 @@ 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) and not item.linked_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
if item.review: if item.review:
# pylint: disable=consider-using-f-string # pylint: disable=consider-using-f-string
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 review = models.Review.objects.filter(
else "" user=user,
) book=item.book,
models.Review.objects.create( name=review_title,
rating=item.rating,
published_date=published_date_guess,
).first()
if not review:
review = models.Review(
user=user, user=user,
book=item.book, book=item.book,
name=review_title, name=review_title,
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,
) )
review.save(software="bookwyrm", priority=LOW)
else: else:
# just a rating # just a rating
models.ReviewRating.objects.create( 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, user=user,
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,
) )
review.save(software="bookwyrm", priority=LOW)
# only broadcast this review to other bookwyrm instances
item.linked_review = review
item.save()

View file

@ -1,7 +1,5 @@
""" handle reading a csv from librarything """ """ handle reading a tsv from librarything """
import re import re
import math
from . import Importer from . import Importer
@ -11,32 +9,18 @@ class LibrarythingImporter(Importer):
service = "LibraryThing" service = "LibraryThing"
delimiter = "\t" delimiter = "\t"
encoding = "ISO-8859-1" encoding = "ISO-8859-1"
# mandatory_fields : fields matching the book title and author
mandatory_fields = ["Title", "Primary Author"]
def parse_fields(self, entry): def normalize_row(self, entry, mappings): # pylint: disable=no-self-use
"""custom parsing for librarything""" """use the dataclass to create the formatted row of data"""
data = {} remove_brackets = lambda v: re.sub(r"\[|\]", "", v) if v else None
data["import_source"] = self.service normalized = {k: remove_brackets(entry.get(v)) for k, v in mappings.items()}
data["Book Id"] = entry["Book Id"] isbn_13 = normalized["isbn_13"].split(", ")
data["Title"] = entry["Title"] normalized["isbn_13"] = isbn_13[1] if len(isbn_13) > 0 else None
data["Author"] = entry["Primary Author"] return normalized
data["ISBN13"] = entry["ISBN"]
data["My Review"] = entry["Review"]
if entry["Rating"]:
data["My Rating"] = math.ceil(float(entry["Rating"]))
else:
data["My Rating"] = ""
data["Date Added"] = re.sub(r"\[|\]", "", entry["Entry Date"])
data["Date Started"] = re.sub(r"\[|\]", "", entry["Date Started"])
data["Date Read"] = re.sub(r"\[|\]", "", entry["Date Read"])
data["Exclusive Shelf"] = None def get_shelf(self, normalized_row):
if data["Date Read"]: if normalized_row["date_finished"]:
data["Exclusive Shelf"] = "read" return "read"
elif data["Date Started"]: if normalized_row["date_started"]:
data["Exclusive Shelf"] = "reading" return "reading"
else: return "to-read"
data["Exclusive Shelf"] = "to-read"
return data

View file

@ -1,7 +1,4 @@
""" handle reading a csv from librarything """ """ handle reading a csv from storygraph"""
import re
import math
from . import Importer from . import Importer
@ -9,26 +6,3 @@ class StorygraphImporter(Importer):
"""csv downloads from librarything""" """csv downloads from librarything"""
service = "Storygraph" service = "Storygraph"
# mandatory_fields : fields matching the book title and author
mandatory_fields = ["Title"]
def parse_fields(self, entry):
"""custom parsing for storygraph"""
data = {}
data["import_source"] = self.service
data["Title"] = entry["Title"]
data["Author"] = entry["Authors"] if "Authors" in entry else entry["Author"]
data["ISBN13"] = entry["ISBN"]
data["My Review"] = entry["Review"]
if entry["Star Rating"]:
data["My Rating"] = math.ceil(float(entry["Star Rating"]))
else:
data["My Rating"] = ""
data["Date Added"] = re.sub(r"[/]", "-", entry["Date Added"])
data["Date Read"] = re.sub(r"[/]", "-", entry["Last Date Read"])
data["Exclusive Shelf"] = (
{"read": "read", "currently-reading": "reading", "to-read": "to-read"}
).get(entry["Read Status"], None)
return data

View file

@ -0,0 +1,25 @@
# Generated by Django 3.2.5 on 2021-11-10 21:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0112_auto_20211022_0844"),
]
operations = [
migrations.AddField(
model_name="importitem",
name="normalized_data",
field=models.JSONField(default={}),
preserve_default=False,
),
migrations.AddField(
model_name="importjob",
name="mappings",
field=models.JSONField(default={}),
preserve_default=False,
),
]

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

@ -0,0 +1,24 @@
# Generated by Django 3.2.5 on 2021-11-13 19:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0114_importjob_source"),
]
operations = [
migrations.AddField(
model_name="importitem",
name="linked_review",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="bookwyrm.review",
),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 3.2.5 on 2021-11-14 17:34
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0115_importitem_linked_review"),
]
operations = [
migrations.RemoveField(
model_name="importjob",
name="task_id",
),
migrations.AddField(
model_name="importjob",
name="updated_date",
field=models.DateTimeField(default=django.utils.timezone.now),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 3.2.5 on 2021-11-15 18:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0116_auto_20211114_1734"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("de-de", "Deutsch (German)"),
("es-es", "Español (Spanish)"),
("fr-fr", "Français (French)"),
("lt-lt", "lietuvių (Lithuanian)"),
("pt-br", "Português - Brasil (Brazilian Portuguese)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,33 @@
# Generated by Django 3.2.5 on 2021-11-17 18:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0117_alter_user_preferred_language"),
]
operations = [
migrations.AlterField(
model_name="user",
name="preferred_language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("de-de", "Deutsch (German)"),
("es-es", "Español (Spanish)"),
("gl-es", "Galego (Galician)"),
("fr-fr", "Français (French)"),
("lt-lt", "Lietuvių (Lithuanian)"),
("pt-br", "Português - Brasil (Brazilian Portuguese)"),
("zh-hans", "简体中文 (Simplified Chinese)"),
("zh-hant", "繁體中文 (Traditional Chinese)"),
],
max_length=255,
null=True,
),
),
]

View file

@ -20,7 +20,7 @@ from django.utils.http import http_date
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import USER_AGENT, PAGE_LENGTH from bookwyrm.settings import USER_AGENT, PAGE_LENGTH
from bookwyrm.signatures import make_signature, make_digest from bookwyrm.signatures import make_signature, make_digest
from bookwyrm.tasks import app from bookwyrm.tasks import app, MEDIUM
from bookwyrm.models.fields import ImageField, ManyToManyField from bookwyrm.models.fields import ImageField, ManyToManyField
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,7 +29,6 @@ logger = logging.getLogger(__name__)
PropertyField = namedtuple("PropertyField", ("set_activity_from_field")) PropertyField = namedtuple("PropertyField", ("set_activity_from_field"))
# pylint: disable=invalid-name # pylint: disable=invalid-name
def set_activity_from_property_field(activity, obj, field): def set_activity_from_property_field(activity, obj, field):
"""assign a model property value to the activity json""" """assign a model property value to the activity json"""
@ -126,12 +125,15 @@ class ActivitypubMixin:
# there OUGHT to be only one match # there OUGHT to be only one match
return match.first() return match.first()
def broadcast(self, activity, sender, software=None): def broadcast(self, activity, sender, software=None, queue=MEDIUM):
"""send out an activity""" """send out an activity"""
broadcast_task.delay( broadcast_task.apply_async(
args=(
sender.id, sender.id,
json.dumps(activity, cls=activitypub.ActivityEncoder), json.dumps(activity, cls=activitypub.ActivityEncoder),
self.get_recipients(software=software), self.get_recipients(software=software),
),
queue=queue,
) )
def get_recipients(self, software=None): def get_recipients(self, software=None):
@ -195,7 +197,7 @@ class ActivitypubMixin:
class ObjectMixin(ActivitypubMixin): class ObjectMixin(ActivitypubMixin):
"""add this mixin for object models that are AP serializable""" """add this mixin for object models that are AP serializable"""
def save(self, *args, created=None, **kwargs): def save(self, *args, created=None, software=None, priority=MEDIUM, **kwargs):
"""broadcast created/updated/deleted objects as appropriate""" """broadcast created/updated/deleted objects as appropriate"""
broadcast = kwargs.get("broadcast", True) broadcast = kwargs.get("broadcast", True)
# this bonus kwarg would cause an error in the base save method # this bonus kwarg would cause an error in the base save method
@ -219,15 +221,17 @@ class ObjectMixin(ActivitypubMixin):
return return
try: try:
software = None
# do we have a "pure" activitypub version of this for mastodon? # do we have a "pure" activitypub version of this for mastodon?
if hasattr(self, "pure_content"): if software != "bookwyrm" and hasattr(self, "pure_content"):
pure_activity = self.to_create_activity(user, pure=True) pure_activity = self.to_create_activity(user, pure=True)
self.broadcast(pure_activity, user, software="other") self.broadcast(
pure_activity, user, software="other", queue=priority
)
# set bookwyrm so that that type is also sent
software = "bookwyrm" software = "bookwyrm"
# sends to BW only if we just did a pure version for masto # sends to BW only if we just did a pure version for masto
activity = self.to_create_activity(user) activity = self.to_create_activity(user)
self.broadcast(activity, user, software=software) self.broadcast(activity, user, software=software, queue=priority)
except AttributeError: except AttributeError:
# janky as heck, this catches the mutliple inheritence chain # janky as heck, this catches the mutliple inheritence chain
# for boosts and ignores this auxilliary broadcast # for boosts and ignores this auxilliary broadcast
@ -241,8 +245,7 @@ class ObjectMixin(ActivitypubMixin):
if isinstance(self, user_model): if isinstance(self, user_model):
user = self user = self
# book data tracks last editor # book data tracks last editor
elif hasattr(self, "last_edited_by"): user = user or getattr(self, "last_edited_by", None)
user = self.last_edited_by
# again, if we don't know the user or they're remote, don't bother # again, if we don't know the user or they're remote, don't bother
if not user or not user.local: if not user or not user.local:
return return
@ -252,7 +255,7 @@ class ObjectMixin(ActivitypubMixin):
activity = self.to_delete_activity(user) activity = self.to_delete_activity(user)
else: else:
activity = self.to_update_activity(user) activity = self.to_update_activity(user)
self.broadcast(activity, user) self.broadcast(activity, user, queue=priority)
def to_create_activity(self, user, **kwargs): def to_create_activity(self, user, **kwargs):
"""returns the object wrapped in a Create activity""" """returns the object wrapped in a Create activity"""
@ -375,9 +378,9 @@ class CollectionItemMixin(ActivitypubMixin):
activity_serializer = activitypub.CollectionItem activity_serializer = activitypub.CollectionItem
def broadcast(self, activity, sender, software="bookwyrm"): def broadcast(self, activity, sender, software="bookwyrm", queue=MEDIUM):
"""only send book collection updates to other bookwyrm instances""" """only send book collection updates to other bookwyrm instances"""
super().broadcast(activity, sender, software=software) super().broadcast(activity, sender, software=software, queue=queue)
@property @property
def privacy(self): def privacy(self):
@ -396,7 +399,7 @@ class CollectionItemMixin(ActivitypubMixin):
return [] return []
return [collection_field.user] return [collection_field.user]
def save(self, *args, broadcast=True, **kwargs): def save(self, *args, broadcast=True, priority=MEDIUM, **kwargs):
"""broadcast updated""" """broadcast updated"""
# first off, we want to save normally no matter what # first off, we want to save normally no matter what
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -407,7 +410,7 @@ class CollectionItemMixin(ActivitypubMixin):
# adding an obj to the collection # adding an obj to the collection
activity = self.to_add_activity(self.user) activity = self.to_add_activity(self.user)
self.broadcast(activity, self.user) self.broadcast(activity, self.user, queue=priority)
def delete(self, *args, broadcast=True, **kwargs): def delete(self, *args, broadcast=True, **kwargs):
"""broadcast a remove activity""" """broadcast a remove activity"""
@ -440,12 +443,12 @@ class CollectionItemMixin(ActivitypubMixin):
class ActivityMixin(ActivitypubMixin): class ActivityMixin(ActivitypubMixin):
"""add this mixin for models that are AP serializable""" """add this mixin for models that are AP serializable"""
def save(self, *args, broadcast=True, **kwargs): def save(self, *args, broadcast=True, priority=MEDIUM, **kwargs):
"""broadcast activity""" """broadcast activity"""
super().save(*args, **kwargs) super().save(*args, **kwargs)
user = self.user if hasattr(self, "user") else self.user_subject user = self.user if hasattr(self, "user") else self.user_subject
if broadcast and user.local: if broadcast and user.local:
self.broadcast(self.to_activity(), user) self.broadcast(self.to_activity(), user, queue=priority)
def delete(self, *args, broadcast=True, **kwargs): def delete(self, *args, broadcast=True, **kwargs):
"""nevermind, undo that activity""" """nevermind, undo that activity"""
@ -502,7 +505,7 @@ def unfurl_related_field(related_field, sort_field=None):
return related_field.remote_id return related_field.remote_id
@app.task(queue="medium_priority") @app.task(queue=MEDIUM)
def broadcast_task(sender_id, activity, recipients): def broadcast_task(sender_id, activity, recipients):
"""the celery task for broadcast""" """the celery task for broadcast"""
user_model = apps.get_model("bookwyrm.User", require_ready=True) user_model = apps.get_model("bookwyrm.User", require_ready=True)

View file

@ -66,9 +66,10 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
self.remote_id = None self.remote_id = None
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
def broadcast(self, activity, sender, software="bookwyrm"): # pylint: disable=arguments-differ
def broadcast(self, activity, sender, software="bookwyrm", **kwargs):
"""only send book data updates to other bookwyrm instances""" """only send book data updates to other bookwyrm instances"""
super().broadcast(activity, sender, software=software) super().broadcast(activity, sender, software=software, **kwargs)
class Book(BookDataModel): class Book(BookDataModel):

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import imghdr import imghdr
import re import re
from uuid import uuid4 from uuid import uuid4
from urllib.parse import urljoin
import dateutil.parser import dateutil.parser
from dateutil.parser import ParserError from dateutil.parser import ParserError
@ -13,11 +14,12 @@ from django.db import models
from django.forms import ClearableFileInput, ImageField as DjangoImageField from django.forms import ClearableFileInput, ImageField as DjangoImageField
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.encoding import filepath_to_uri
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.connectors import get_image from bookwyrm.connectors import get_image
from bookwyrm.sanitize_html import InputHtmlParser from bookwyrm.sanitize_html import InputHtmlParser
from bookwyrm.settings import DOMAIN from bookwyrm.settings import MEDIA_FULL_URL
def validate_remote_id(value): def validate_remote_id(value):
@ -381,17 +383,6 @@ class CustomImageField(DjangoImageField):
widget = ClearableFileInputWithWarning widget = ClearableFileInputWithWarning
def image_serializer(value, alt):
"""helper for serializing images"""
if value and hasattr(value, "url"):
url = value.url
else:
return None
if not url[:4] == "http":
url = f"https://{DOMAIN}{url}"
return activitypub.Document(url=url, name=alt)
class ImageField(ActivitypubFieldMixin, models.ImageField): class ImageField(ActivitypubFieldMixin, models.ImageField):
"""activitypub-aware image field""" """activitypub-aware image field"""
@ -424,7 +415,12 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
activity[key] = formatted activity[key] = formatted
def field_to_activity(self, value, alt=None): def field_to_activity(self, value, alt=None):
return image_serializer(value, alt) url = get_absolute_url(value)
if not url:
return None
return activitypub.Document(url=url, name=alt)
def field_from_activity(self, value): def field_from_activity(self, value):
image_slug = value image_slug = value
@ -461,6 +457,20 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
) )
def get_absolute_url(value):
"""returns an absolute URL for the image"""
name = getattr(value, "name")
if not name:
return None
url = filepath_to_uri(name)
if url is not None:
url = url.lstrip("/")
url = urljoin(MEDIA_FULL_URL, url)
return url
class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
"""activitypub-aware datetime field""" """activitypub-aware datetime field"""

View file

@ -6,20 +6,14 @@ from django.db import models
from django.utils import timezone from django.utils import timezone
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from bookwyrm.models import ReadThrough, User, Book from bookwyrm.models import ReadThrough, User, Book, Edition
from .fields import PrivacyLevels from .fields import PrivacyLevels
# Mapping goodreads -> bookwyrm shelf titles.
GOODREADS_SHELVES = {
"read": "read",
"currently-reading": "reading",
"to-read": "to-read",
}
def unquote_string(text): def unquote_string(text):
"""resolve csv quote weirdness""" """resolve csv quote weirdness"""
if not text:
return None
match = re.match(r'="([^"]*)"', text) match = re.match(r'="([^"]*)"', text)
if match: if match:
return match.group(1) return match.group(1)
@ -41,14 +35,21 @@ class ImportJob(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
created_date = models.DateTimeField(default=timezone.now) created_date = models.DateTimeField(default=timezone.now)
task_id = models.CharField(max_length=100, null=True) updated_date = models.DateTimeField(default=timezone.now)
include_reviews = models.BooleanField(default=True) include_reviews = models.BooleanField(default=True)
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
) )
retry = models.BooleanField(default=False) retry = models.BooleanField(default=False)
@property
def pending_items(self):
"""items that haven't been processed yet"""
return self.items.filter(fail_reason__isnull=True, book__isnull=True)
class ImportItem(models.Model): class ImportItem(models.Model):
"""a single line of a csv being imported""" """a single line of a csv being imported"""
@ -56,6 +57,7 @@ class ImportItem(models.Model):
job = models.ForeignKey(ImportJob, on_delete=models.CASCADE, related_name="items") job = models.ForeignKey(ImportJob, on_delete=models.CASCADE, related_name="items")
index = models.IntegerField() index = models.IntegerField()
data = models.JSONField() data = models.JSONField()
normalized_data = models.JSONField()
book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True, blank=True) book = models.ForeignKey(Book, on_delete=models.SET_NULL, null=True, blank=True)
book_guess = models.ForeignKey( book_guess = models.ForeignKey(
Book, Book,
@ -65,9 +67,26 @@ class ImportItem(models.Model):
related_name="book_guess", related_name="book_guess",
) )
fail_reason = models.TextField(null=True) fail_reason = models.TextField(null=True)
linked_review = models.ForeignKey(
"Review", on_delete=models.SET_NULL, null=True, blank=True
)
def update_job(self):
"""let the job know when the items get work done"""
job = self.job
job.updated_date = timezone.now()
job.save()
if not job.pending_items.exists() and not job.complete:
job.complete = True
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"""
# 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:
@ -85,6 +104,10 @@ class ImportItem(models.Model):
self.isbn, min_confidence=0.999 self.isbn, min_confidence=0.999
) )
if search_result: if search_result:
# it's already in the right format
if isinstance(search_result, Edition):
return search_result
# it's just a search result, book needs to be created
# raises ConnectorException # raises ConnectorException
return search_result.connector.get_or_create_book(search_result.key) return search_result.connector.get_or_create_book(search_result.key)
return None return None
@ -96,6 +119,8 @@ class ImportItem(models.Model):
search_term, min_confidence=0.1 search_term, min_confidence=0.1
) )
if search_result: if search_result:
if isinstance(search_result, Edition):
return (search_result, 1)
# raises ConnectorException # raises ConnectorException
return ( return (
search_result.connector.get_or_create_book(search_result.key), search_result.connector.get_or_create_book(search_result.key),
@ -106,56 +131,62 @@ class ImportItem(models.Model):
@property @property
def title(self): def title(self):
"""get the book title""" """get the book title"""
return self.data["Title"] return self.normalized_data.get("title")
@property @property
def author(self): def author(self):
"""get the book title""" """get the book's authors"""
return self.data["Author"] return self.normalized_data.get("authors")
@property @property
def isbn(self): def isbn(self):
"""pulls out the isbn13 field from the csv line data""" """pulls out the isbn13 field from the csv line data"""
return unquote_string(self.data["ISBN13"]) return unquote_string(self.normalized_data.get("isbn_13")) or unquote_string(
self.normalized_data.get("isbn_10")
)
@property @property
def shelf(self): def shelf(self):
"""the goodreads shelf field""" """the goodreads shelf field"""
if self.data["Exclusive Shelf"]: return self.normalized_data.get("shelf")
return GOODREADS_SHELVES.get(self.data["Exclusive Shelf"])
return None
@property @property
def review(self): def review(self):
"""a user-written review, to be imported with the book data""" """a user-written review, to be imported with the book data"""
return self.data["My Review"] return self.normalized_data.get("review_body")
@property @property
def rating(self): def rating(self):
"""x/5 star rating for a book""" """x/5 star rating for a book"""
if self.data.get("My Rating", None): if self.normalized_data.get("rating"):
return int(self.data["My Rating"]) return float(self.normalized_data.get("rating"))
return None return None
@property @property
def date_added(self): def date_added(self):
"""when the book was added to this dataset""" """when the book was added to this dataset"""
if self.data["Date Added"]: if self.normalized_data.get("date_added"):
return timezone.make_aware(dateutil.parser.parse(self.data["Date Added"])) return timezone.make_aware(
dateutil.parser.parse(self.normalized_data.get("date_added"))
)
return None return None
@property @property
def date_started(self): def date_started(self):
"""when the book was started""" """when the book was started"""
if "Date Started" in self.data and self.data["Date Started"]: if self.normalized_data.get("date_started"):
return timezone.make_aware(dateutil.parser.parse(self.data["Date Started"])) return timezone.make_aware(
dateutil.parser.parse(self.normalized_data.get("date_started"))
)
return None return None
@property @property
def date_read(self): def date_read(self):
"""the date a book was completed""" """the date a book was completed"""
if self.data["Date Read"]: if self.normalized_data.get("date_finished"):
return timezone.make_aware(dateutil.parser.parse(self.data["Date Read"])) return timezone.make_aware(
dateutil.parser.parse(self.normalized_data.get("date_finished"))
)
return None return None
@property @property
@ -174,7 +205,9 @@ class ImportItem(models.Model):
if start_date and start_date is not None and not self.date_read: if start_date and start_date is not None and not self.date_read:
return [ReadThrough(start_date=start_date)] return [ReadThrough(start_date=start_date)]
if self.date_read: if self.date_read:
start_date = start_date if start_date < self.date_read else None start_date = (
start_date if start_date and start_date < self.date_read else None
)
return [ return [
ReadThrough( ReadThrough(
start_date=start_date, start_date=start_date,
@ -185,8 +218,10 @@ class ImportItem(models.Model):
def __repr__(self): def __repr__(self):
# pylint: disable=consider-using-f-string # pylint: disable=consider-using-f-string
return "<{!r}Item {!r}>".format(self.data["import_source"], self.data["Title"]) return "<{!r} Item {!r}>".format(self.index, self.normalized_data.get("title"))
def __str__(self): def __str__(self):
# pylint: disable=consider-using-f-string # pylint: disable=consider-using-f-string
return "{} by {}".format(self.data["Title"], self.data["Author"]) return "{} by {}".format(
self.normalized_data.get("title"), self.normalized_data.get("authors")
)

View file

@ -157,9 +157,12 @@ def notify_user_on_unboost(sender, instance, *args, **kwargs):
@receiver(models.signals.post_save, sender=ImportJob) @receiver(models.signals.post_save, sender=ImportJob)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def notify_user_on_import_complete(sender, instance, *args, **kwargs): def notify_user_on_import_complete(
sender, instance, *args, update_fields=None, **kwargs
):
"""we imported your books! aren't you proud of us""" """we imported your books! aren't you proud of us"""
if not instance.complete: update_fields = update_fields or []
if not instance.complete or "complete" not in update_fields:
return return
Notification.objects.create( Notification.objects.create(
user=instance.user, user=instance.user,

View file

@ -1,5 +1,6 @@
""" the particulars for this instance of BookWyrm """ """ the particulars for this instance of BookWyrm """
import datetime import datetime
from urllib.parse import urljoin
from django.db import models, IntegrityError from django.db import models, IntegrityError
from django.dispatch import receiver from django.dispatch import receiver
@ -7,9 +8,10 @@ from django.utils import timezone
from model_utils import FieldTracker from model_utils import FieldTracker
from bookwyrm.preview_images import generate_site_preview_image_task from bookwyrm.preview_images import generate_site_preview_image_task
from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL
from .base_model import BookWyrmModel, new_access_code from .base_model import BookWyrmModel, new_access_code
from .user import User from .user import User
from .fields import get_absolute_url
class SiteSettings(models.Model): class SiteSettings(models.Model):
@ -66,6 +68,28 @@ class SiteSettings(models.Model):
default_settings.save() default_settings.save()
return default_settings return default_settings
@property
def logo_url(self):
"""helper to build the logo url"""
return self.get_url("logo", "images/logo.png")
@property
def logo_small_url(self):
"""helper to build the logo url"""
return self.get_url("logo_small", "images/logo-small.png")
@property
def favicon_url(self):
"""helper to build the logo url"""
return self.get_url("favicon", "images/favicon.png")
def get_url(self, field, default_path):
"""get a media url or a default static path"""
uploaded = getattr(self, field, None)
if uploaded:
return get_absolute_url(uploaded)
return urljoin(STATIC_FULL_URL, default_path)
class SiteInvite(models.Model): class SiteInvite(models.Model):
"""gives someone access to create an account on the instance""" """gives someone access to create an account on the instance"""

View file

@ -19,7 +19,6 @@ from bookwyrm.settings import ENABLE_PREVIEW_IMAGES
from .activitypub_mixin import ActivitypubMixin, ActivityMixin from .activitypub_mixin import ActivitypubMixin, ActivityMixin
from .activitypub_mixin import OrderedCollectionPageMixin from .activitypub_mixin import OrderedCollectionPageMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
from .fields import image_serializer
from .readthrough import ProgressMode from .readthrough import ProgressMode
from . import fields from . import fields
@ -190,15 +189,26 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
if hasattr(activity, "name"): if hasattr(activity, "name"):
activity.name = self.pure_name activity.name = self.pure_name
activity.type = self.pure_type activity.type = self.pure_type
activity.attachment = [ book = getattr(self, "book", None)
image_serializer(b.cover, b.alt_text) books = [book] if book else []
for b in self.mention_books.all()[:4] books += list(self.mention_books.all())
if b.cover if len(books) == 1 and getattr(books[0], "preview_image", None):
] covers = [
if hasattr(self, "book") and self.book.cover: activitypub.Document(
activity.attachment.append( url=fields.get_absolute_url(books[0].preview_image),
image_serializer(self.book.cover, self.book.alt_text) name=books[0].alt_text,
) )
]
else:
covers = [
activitypub.Document(
url=fields.get_absolute_url(b.cover),
name=b.alt_text,
)
for b in books
if b and b.cover
]
activity.attachment = covers
return activity return activity
def to_activity(self, pure=False): # pylint: disable=arguments-differ def to_activity(self, pure=False): # pylint: disable=arguments-differ

View file

@ -165,7 +165,9 @@ LANGUAGES = [
("en-us", _("English")), ("en-us", _("English")),
("de-de", _("Deutsch (German)")), ("de-de", _("Deutsch (German)")),
("es-es", _("Español (Spanish)")), ("es-es", _("Español (Spanish)")),
("gl-es", _("Galego (Galician)")),
("fr-fr", _("Français (French)")), ("fr-fr", _("Français (French)")),
("lt-lt", _("Lietuvių (Lithuanian)")),
("pt-br", _("Português - Brasil (Brazilian Portuguese)")), ("pt-br", _("Português - Brasil (Brazilian Portuguese)")),
("zh-hans", _("简体中文 (Simplified Chinese)")), ("zh-hans", _("简体中文 (Simplified Chinese)")),
("zh-hant", _("繁體中文 (Traditional Chinese)")), ("zh-hant", _("繁體中文 (Traditional Chinese)")),

View file

@ -9,3 +9,8 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings")
app = Celery( app = Celery(
"tasks", broker=settings.CELERY_BROKER_URL, backend=settings.CELERY_RESULT_BACKEND "tasks", broker=settings.CELERY_BROKER_URL, backend=settings.CELERY_RESULT_BACKEND
) )
# priorities
LOW = "low_priority"
MEDIUM = "medium_priority"
HIGH = "high_priority"

View file

@ -1,10 +1,24 @@
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
{% with user_path=status.user.local_path username=status.user.display_name book_path=status.book.local_poth book_title=book|book_title %} {% with user_path=status.user.local_path username=status.user.display_name book_path=book.local_path book_title=book|book_title %}
{% if status.status_type == 'GeneratedNote' %} {% if status.status_type == 'GeneratedNote' %}
{{ status.content|safe }} {% if status.content == 'wants to read' %}
{% blocktrans trimmed %}
<a href="{{ user_path}}">{{ username }}</a> wants to read <a href="{{ book_path }}">{{ book_title }}</a>
{% endblocktrans %}
{% endif %}
{% if status.content == 'finished reading' %}
{% blocktrans trimmed %}
<a href="{{ user_path}}">{{ username }}</a> finished reading <a href="{{ book_path }}">{{ book_title }}</a>
{% endblocktrans %}
{% endif %}
{% if status.content == 'started reading' %}
{% blocktrans trimmed %}
<a href="{{ user_path}}">{{ username }}</a> started reading <a href="{{ book_path }}">{{ book_title }}</a>
{% endblocktrans %}
{% endif %}
{% elif status.status_type == 'Rating' %} {% elif status.status_type == 'Rating' %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
<a href="{{ user_path}}">{{ username }}</a> rated <a href="{{ book_path }}">{{ book_title }}</a> <a href="{{ user_path}}">{{ username }}</a> rated <a href="{{ book_path }}">{{ book_title }}</a>

View file

@ -2,7 +2,7 @@
<div style="font-family: BlinkMacSystemFont,-apple-system,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',Helvetica,Arial,sans-serif; border-radius: 6px; background-color: #efefef; max-width: 632px;"> <div style="font-family: BlinkMacSystemFont,-apple-system,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',Helvetica,Arial,sans-serif; border-radius: 6px; background-color: #efefef; max-width: 632px;">
<div style="padding: 1rem; overflow: auto;"> <div style="padding: 1rem; overflow: auto;">
<div style="float: left; margin-right: 1rem;"> <div style="float: left; margin-right: 1rem;">
<a style="color: #3273dc;" href="https://{{ domain }}" style="text-decoration: none;"><img src="https://{{ domain }}/{{ logo }}" alt="logo"></a> <a style="color: #3273dc;" href="https://{{ domain }}" style="text-decoration: none;"><img src="{{ logo }}" alt="logo"></a>
</div> </div>
<div> <div>
<a style="color: black; text-decoration: none" href="https://{{ domain }}" style="text-decoration: none;"><strong>{{ site_name }}</strong><br> <a style="color: black; text-decoration: none" href="https://{{ domain }}" style="text-decoration: none;"><strong>{{ site_name }}</strong><br>

View file

@ -0,0 +1,11 @@
{% extends 'email/html_layout.html' %}
{% load i18n %}
{% block content %}
<p>
{% blocktrans %}@{{ reporter }} has flagged behavior by @{{ reportee }} for moderation. {% endblocktrans %}
</p>
{% trans "View report" as text %}
{% include 'email/snippets/action.html' with path=report_link text=text %}
{% endblock %}

View file

@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}New report for {{ site_name }}{% endblocktrans %}

View file

@ -0,0 +1,9 @@
{% extends 'email/text_layout.html' %}
{% load i18n %}
{% block content %}
{% blocktrans %}@{{ reporter}} has flagged behavior by @{{ reportee }} for moderation. {% endblocktrans %}
{% trans "View report" %}
{{ report_link }}
{% endblock %}

View file

@ -1,4 +1,4 @@
<html lang="{% get_lang %}"> <html lang="en">
<body> <body>
<div> <div>
<strong>Subject:</strong> {% include subject_path %} <strong>Subject:</strong> {% include subject_path %}

View file

@ -9,7 +9,7 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="column is-one-third"> <div class="column is-one-third">
<section class="block"> <section class="block">
<h2 class="title is-4">{% trans "Your books" %}</h2> <h2 class="title is-4">{% trans "Your Books" %}</h2>
{% if not suggested_books %} {% if not suggested_books %}
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p> <p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
{% else %} {% else %}

View file

@ -46,10 +46,10 @@
</label> </label>
</div> </div>
<div class="field"> <div class="field">
<label> <label class="label" for="privacy_import">
<span class="label">{% trans "Privacy setting for imported reviews:" %}</span> {% trans "Privacy setting for imported reviews:" %}
{% include 'snippets/privacy_select.html' with no_label=True %}
</label> </label>
{% include 'snippets/privacy_select.html' with no_label=True privacy_uuid="import" %}
</div> </div>
</div> </div>
</div> </div>

View file

@ -6,129 +6,163 @@
{% block title %}{% trans "Import Status" %}{% endblock %} {% block title %}{% trans "Import Status" %}{% endblock %}
{% block content %}{% spaceless %} {% block content %}{% spaceless %}
<div class="block"> <header class="block">
<h1 class="title">{% trans "Import Status" %}</h1> <h1 class="title">
<a href="{% url 'import' %}" class="has-text-weight-normal help subtitle is-link">{% trans "Back to imports" %}</a> {% block page_title %}
{% if job.retry %}
<dl> {% trans "Retry Status" %}
<div class="is-flex"> {% else %}
<dt class="has-text-weight-medium">{% trans "Import started:" %}</dt> {% trans "Import Status" %}
<dd class="ml-2">{{ job.created_date | naturaltime }}</dd>
</div>
{% if job.complete %}
<div class="is-flex">
<dt class="has-text-weight-medium">{% trans "Import completed:" %}</dt>
<dd class="ml-2">{{ task.date_done | naturaltime }}</dd>
</div>
{% elif task.failed %}
<div class="notification is-danger">{% trans "TASK FAILED" %}</div>
{% endif %} {% endif %}
{% endblock %}
</h1>
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'import' %}">{% trans "Imports" %}</a></li>
{% url 'import-status' job.id as path %}
<li{% if request.path in path %} class="is-active"{% endif %}>
<a href="{{ path }}" {% if request.path in path %}aria-current="page"{% endif %}>
{% if job.retry %}
{% trans "Retry Status" %}
{% else %}
{% trans "Import Status" %}
{% endif %}
</a>
</li>
{% block breadcrumbs %}{% endblock %}
</ul>
</nav>
<div class="block">
<dl>
<dt class="is-pulled-left mr-5">{% trans "Import started:" %}</dt>
<dd>{{ job.created_date | naturaltime }}</dd>
</dl> </dl>
</div> </div>
<div class="block">
{% if not job.complete %} {% if not job.complete %}
<p> <div class="box is-processing">
{% trans "Import still in progress." %}
<br/>
{% trans "(Hit reload to update!)" %}
</p>
{% endif %}
</div>
{% if failed_items %}
<div class="block"> <div class="block">
<h2 class="title is-4">{% trans "Failed to load" %}</h2> <span class="icon icon-spinner is-pulled-left" aria-hidden="true"></span>
{% if not job.retry %} <span>{% trans "In progress" %}</span>
<form name="retry" action="/import/{{ job.id }}" method="post" class="box"> <span class="is-pulled-right">
{% csrf_token %} <a href="#" class="button is-small">{% trans "Refresh" %}</a>
</span>
</div>
<div class="is-flex">
<progress
class="progress is-success is-medium mr-2"
role="progressbar"
aria-min="0"
value="{{ complete_count }}"
aria-valuenow="{{ complete_count }}"
max="{{ item_count }}"
aria-valuemax="{{ item_count }}">
{{ percent }} %
</progress>
<span>{{ percent }}%</span>
</div>
</div>
{% endif %}
{% with failed_count=failed_items|length %} {% if manual_review_count and not legacy %}
{% if failed_count > 10 %} <div class="notification">
<p class="block"> {% blocktrans trimmed count counter=manual_review_count with display_counter=manual_review_count|intcomma %}
<a href="#select-all-failed-imports"> {{ display_counter }} item needs manual approval.
{% blocktrans %}Jump to the bottom of the list to select the {{ failed_count }} items which failed to import.{% endblocktrans %} {% plural %}
{{ display_counter }} items need manual approval.
{% endblocktrans %}
<a href="{% url 'import-review' job.id %}">{% trans "Review items" %}</a>
</div>
{% endif %}
{% if job.complete and fail_count and not job.retry and not legacy %}
<div class="notification is-warning">
{% blocktrans trimmed count counter=fail_count with display_counter=fail_count|intcomma %}
{{ display_counter }} item failed to import.
{% plural %}
{{ display_counter }} items failed to import.
{% endblocktrans %}
<a href="{% url 'import-troubleshoot' job.id %}">
{% trans "View and troubleshoot failed items" %}
</a> </a>
</p>
{% endif %}
{% endwith %}
<fieldset id="failed_imports">
<ul>
{% for item in failed_items %}
<li class="mb-2 is-flex is-align-items-start">
<input class="checkbox mt-1" type="checkbox" name="import_item" value="{{ item.id }}" id="import_item_{{ item.id }}">
<label class="ml-1" for="import-item-{{ item.id }}">
{% blocktrans with index=item.index title=item.data.Title author=item.data.Author %}Line {{ index }}: <strong>{{ title }}</strong> by {{ author }}{% endblocktrans %}
<br/>
{{ item.fail_reason }}.
</label>
</li>
{% endfor %}
</ul>
</fieldset>
<fieldset class="mt-3">
<a name="select-all-failed-imports"></a>
<label class="label is-inline">
<input
id="toggle-all-checkboxes-failed-imports"
class="checkbox"
type="checkbox"
data-action="toggle-all"
data-target="failed_imports"
/>
{% trans "Select all" %}
</label>
<button class="button is-block mt-3" type="submit">{% trans "Retry items" %}</button>
</fieldset>
</form>
<hr>
{% else %}
<ul>
{% for item in failed_items %}
<li class="pb-1">
<p>
Line {{ item.index }}:
<strong>{{ item.data.Title }}</strong> by
{{ item.data.Author }}
</p>
<p>
{{ item.fail_reason }}.
</p>
</li>
{% endfor %}
</ul>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</header>
<div class="block"> <div class="block">
{% if job.complete %} {% block actions %}{% endblock %}
<h2 class="title is-4">{% trans "Successfully imported" %}</h2> <div class="table-container">
{% else %} <table class="table is-striped">
<h2 class="title is-4">{% trans "Import Progress" %}</h2>
{% endif %}
<table class="table">
<tr> <tr>
<th> <th>
{% trans "Book" %} {% trans "Row" %}
</th> </th>
<th> <th>
{% trans "Title" %} {% trans "Title" %}
</th> </th>
<th>
{% trans "ISBN" %}
</th>
<th> <th>
{% trans "Author" %} {% trans "Author" %}
</th> </th>
<th> <th>
{% trans "Shelf" %}
</th> </th>
<th>
{% trans "Review" %}
</th>
{% block import_cols_headers %}
<th>
{% trans "Book" %}
</th>
<th>
{% trans "Status" %}
</th>
{% endblock %}
</tr> </tr>
{% if legacy %}
<tr>
<td colspan="8">
<p>
<em>{% trans "Import preview unavailable." %}</em>
</p>
</td>
</tr>
{% else %}
{% for item in items %} {% for item in items %}
<tr> <tr>
{% block index_col %}
<td>
{{ item.index }}
</td>
{% endblock %}
<td>
{{ item.normalized_data.title }}
</td>
<td>
{{ item.isbn|default:'' }}
</td>
<td>
{{ item.normalized_data.authors }}
</td>
<td>
{{ item.normalized_data.shelf }}
</td>
<td>
{% if item.rating %}
<p>{% include 'snippets/stars.html' with rating=item.rating %}</p>
{% endif %}
{% if item.review %}
<p>{{ item.review|truncatechars:100 }}</p>
{% endif %}
{% if item.linked_review %}
<a href="{{ item.linked_review.remote_id }}" target="_blank">{% trans "View imported review" %}</a>
{% endif %}
</td>
{% block import_cols %}
<td> <td>
{% if item.book %} {% if item.book %}
<a href="{{ item.book.local_path }}"> <a href="{{ item.book.local_path }}">
@ -136,23 +170,59 @@
</a> </a>
{% endif %} {% endif %}
</td> </td>
<td>
{{ item.data.Title }}
</td>
<td>
{{ item.data.Author }}
</td>
<td> <td>
{% if item.book %} {% if item.book %}
<span class="icon icon-check"> <span class="icon icon-check" aria-hidden="true"></span>
<span class="is-sr-only">{% trans "Imported" %}</span> <span class="is-sr-only-mobile">{% trans "Imported" %}</span>
{% elif item.fail_reason %}
<span class="icon icon-x has-text-danger" aria-hidden="true"></span>
<span class="is-sr-only-mobile">
{% if item.book_guess %}
{% trans "Needs manual review" %}
{% else %}
{{ item.fail_reason }}
{% endif %}
</span> </span>
{% else %}
<div class="is-flex">
<span class="icon icon-dots-three" aria-hidden="true"></span>
<span class="is-sr-only-mobile">{% trans "Pending" %}</span>
{# retry option if an item appears to be hanging #}
{% if job.created_date != job.updated_date and inactive_time > 24 %}
<form class="ml-2" method="POST" action="{% url 'import-item-retry' job.id item.id %}" name="retry-{{ item.id }}">
{% csrf_token %}
<button class="button is-danger is-outlined is-small">{% trans "Retry" %}</button>
</form>
{% endif %}
</div>
{% endif %} {% endif %}
</td> </td>
{% endblock %}
</tr> </tr>
{% block action_row %}{% endblock %}
{% endfor %} {% endfor %}
{% endif %}
</table> </table>
</div> </div>
{% if legacy %}
<div class="content">
<form name="update-import" method="POST" action="{% url 'import-status' job.id %}">
{% csrf_token %}
<p>
{% trans "This import is in an old format that is no longer supported. If you would like to troubleshoot missing items from this import, click the button below to update the import format." %}
</p>
<button class="button">{% trans "Update import" %}</button>
</form>
</div>
{% endif %}
</div>
{% if not legacy %}
<div>
{% include 'snippets/pagination.html' with page=items %}
</div>
{% endif %}
{% endspaceless %}{% endblock %} {% endspaceless %}{% endblock %}
{% block scripts %} {% block scripts %}

View file

@ -0,0 +1,75 @@
{% extends 'import/import_status.html' %}
{% load i18n %}
{% load utilities %}
{% block title %}{% trans "Import Troubleshooting" %}{% endblock %}
{% block page_title %}
{% trans "Review items" %}
{% endblock %}
{% block breadcrumbs %}
<li class="is-active">
<a href="#" aria-current="page">{% trans "Review" %}</a>
</li>
{% endblock %}
{% block actions %}
<div class="block">
<div class="notification content">
<p>
{% trans "Approving a suggestion will permanently add the suggested book to your shelves and associate your reading dates, reviews, and ratings with that book." %}
</p>
</div>
</div>
{% endblock %}
{% block import_cols_headers %}
{% endblock %}
{% block index_col %}
<td rowspan="2">
{{ item.index }}
</td>
{% endblock %}
{% block import_cols %}
{% endblock %}
{% block action_row %}
<tr>
<td colspan="5">
<div class="columns is-mobile">
{% with guess=item.book_guess %}
<div class="column is-narrow">
<a href="{{ item.book.local_path }}" target="_blank">
{% include 'snippets/book_cover.html' with book=guess cover_class='is-h-s' size='small' %}
</a>
</div>
<div class="column content">
<p>
{% include 'snippets/book_titleby.html' with book=guess %}
</p>
<div class="content is-flex">
<form class="pr-2" name="approve-{{ item.id }}" method="POST" action="{% url 'import-approve' job.id item.id %}">
{% csrf_token %}
<button type="submit" class="button is-success">
<span class="icon icon-check m-0-mobile" 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 m-0-mobile" aria-hidden="true"></span>
<span class="is-sr-only-mobile">{% trans "Reject" %}</span>
</button>
</form>
</div>
</div>
{% endwith %}
</div>
</td>
</tr>
{% endblock %}

View file

@ -0,0 +1,36 @@
{% extends 'import/import_status.html' %}
{% load i18n %}
{% block title %}{% trans "Import Troubleshooting" %}{% endblock %}
{% block page_title %}
{% trans "Failed items" %}
{% endblock %}
{% block breadcrumbs %}
<li class="is-active">
<a href="#" aria-current="page">{% trans "Troubleshooting" %}</a>
</li>
{% endblock %}
{% block actions %}
<div class="block">
<div class="notification content">
<p>
{% trans "Re-trying an import can fix missing items in cases such as:" %}
</p>
<ul>
<li>{% trans "The book has been added to the instance since this import" %}</li>
<li>{% trans "A transient error or timeout caused the external data source to be unavailable." %}</li>
<li>{% trans "BookWyrm has been updated since this import with a bug fix" %}</li>
</ul>
<p>
{% trans "Contact your admin or <a href='https://github.com/bookwyrm-social/bookwyrm/issues'>open an issue</a> if you are seeing unexpected failed items." %}
</p>
</div>
<form name="retry" method="post" action="{% url 'import-troubleshoot' job.id %}">
{% csrf_token %}
<button type="submit" class="button">Retry all</button>
</form>
</div>
{% endblock %}

View file

@ -5,7 +5,7 @@
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{% include 'user/books_header.html' %} {% include 'user/books_header.html' with shelf=shelf %}
{% endblock %} {% endblock %}
{% block opengraph_images %} {% block opengraph_images %}

View file

@ -5,11 +5,9 @@
{% if book.authors.exists %} {% if book.authors.exists %}
{% blocktrans trimmed with path=book.local_path title=book|book_title %} {% blocktrans trimmed with path=book.local_path title=book|book_title %}
<a href="{{ path }}">{{ title }}</a> by <a href="{{ path }}">{{ title }}</a> by
{% endblocktrans %} {% endblocktrans %}&nbsp;{% include 'snippets/authors.html' with book=book limit=3 %}
{% include 'snippets/authors.html' with book=book limit=3 %}
{% else %} {% else %}
<a href="{{ book.local_path }}">{{ book|book_title }}</a> <a href="{{ book.local_path }}">{{ book|book_title }}</a>
{% endif %} {% endif %}
{% endspaceless %} {% endspaceless %}

View file

@ -1,6 +1,16 @@
{% load i18n %} {% load i18n %}
{% if is_self %} {% if is_self %}
{% if shelf.identifier == 'to-read' %}
{% trans "To Read" %}
{% elif shelf.identifier == 'reading' %}
{% trans "Currently Reading" %}
{% elif shelf.identifier == 'read' %}
{% trans "Read" %}
{% elif shelf.identifier == 'all' %}
{% trans "Your books" %} {% trans "Your books" %}
{% else %} {% else %}
{{ shelf.name }}
{% endif %}
{% else %}
{% blocktrans with username=user.display_name %}{{ username }}'s books{% endblocktrans %} {% blocktrans with username=user.display_name %}{{ username }}'s books{% endblocktrans %}
{% endif %} {% endif %}

View file

@ -146,7 +146,7 @@ class BaseActivity(TestCase):
self.user.avatar.file # pylint: disable=pointless-statement self.user.avatar.file # pylint: disable=pointless-statement
# this would trigger a broadcast because it's a local user # this would trigger a broadcast because it's a local user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
activity.to_model(model=models.User, instance=self.user) activity.to_model(model=models.User, instance=self.user)
self.assertIsNotNone(self.user.avatar.file) self.assertIsNotNone(self.user.avatar.file)
self.assertEqual(self.user.name, "New Name") self.assertEqual(self.user.name, "New Name")
@ -154,7 +154,7 @@ class BaseActivity(TestCase):
def test_to_model_many_to_many(self, *_): def test_to_model_many_to_many(self, *_):
"""annoying that these all need special handling""" """annoying that these all need special handling"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
status = models.Status.objects.create( status = models.Status.objects.create(
content="test status", content="test status",
user=self.user, user=self.user,
@ -186,7 +186,7 @@ class BaseActivity(TestCase):
def test_to_model_one_to_many(self, *_): def test_to_model_one_to_many(self, *_):
"""these are reversed relationships, where the secondary object """these are reversed relationships, where the secondary object
keys the primary object but not vice versa""" keys the primary object but not vice versa"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
status = models.Status.objects.create( status = models.Status.objects.create(
content="test status", content="test status",
user=self.user, user=self.user,
@ -224,7 +224,7 @@ class BaseActivity(TestCase):
@responses.activate @responses.activate
def test_set_related_field(self, *_): def test_set_related_field(self, *_):
"""celery task to add back-references to created objects""" """celery task to add back-references to created objects"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
status = models.Status.objects.create( status = models.Status.objects.create(
content="test status", content="test status",
user=self.user, user=self.user,

View file

@ -4,7 +4,7 @@ from django.test import TestCase
from bookwyrm import activitystreams, models from bookwyrm import activitystreams, models
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay") @patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")

View file

@ -4,7 +4,7 @@ from django.test import TestCase
from bookwyrm import activitystreams, models from bookwyrm import activitystreams, models
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay") @patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")

View file

@ -4,7 +4,7 @@ from django.test import TestCase
from bookwyrm import activitystreams, models from bookwyrm import activitystreams, models
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay") @patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")

View file

@ -4,7 +4,7 @@ from django.test import TestCase
from bookwyrm import activitystreams, models from bookwyrm import activitystreams, models
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay") @patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")

View file

@ -4,7 +4,7 @@ from django.test import TestCase
from bookwyrm import activitystreams, models from bookwyrm import activitystreams, models
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
class ActivitystreamsSignals(TestCase): class ActivitystreamsSignals(TestCase):
"""using redis to build activity streams""" """using redis to build activity streams"""
@ -53,11 +53,12 @@ class ActivitystreamsSignals(TestCase):
status = models.Status.objects.create( status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public" user=self.remote_user, content="hi", privacy="public"
) )
with patch("bookwyrm.activitystreams.add_status_task.delay") as mock: with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
activitystreams.add_status_on_create_command(models.Status, status, False) activitystreams.add_status_on_create_command(models.Status, status, False)
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)
args = mock.call_args[0] args = mock.call_args[1]
self.assertEqual(args[0], status.id) self.assertEqual(args["args"][0], status.id)
self.assertEqual(args["queue"], "high_priority")
def test_populate_streams_on_account_create(self, _): def test_populate_streams_on_account_create(self, _):
"""create streams for a user""" """create streams for a user"""

View file

@ -34,7 +34,7 @@ class Activitystreams(TestCase):
) )
work = models.Work.objects.create(title="test work") work = models.Work.objects.create(title="test work")
self.book = models.Edition.objects.create(title="test book", parent_work=work) self.book = models.Edition.objects.create(title="test book", parent_work=work)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
self.status = models.Status.objects.create( self.status = models.Status.objects.create(
content="hi", user=self.local_user content="hi", user=self.local_user
) )
@ -133,7 +133,7 @@ class Activitystreams(TestCase):
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_boost_to_another_timeline(self, *_): def test_boost_to_another_timeline(self, *_):
"""boost from a non-follower doesn't remove original status from feed""" """boost from a non-follower doesn't remove original status from feed"""
status = models.Status.objects.create(user=self.local_user, content="hi") status = models.Status.objects.create(user=self.local_user, content="hi")
@ -155,7 +155,7 @@ class Activitystreams(TestCase):
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_boost_to_another_timeline_remote(self, *_): def test_boost_to_another_timeline_remote(self, *_):
"""boost from a remote non-follower doesn't remove original status from feed""" """boost from a remote non-follower doesn't remove original status from feed"""
status = models.Status.objects.create(user=self.local_user, content="hi") status = models.Status.objects.create(user=self.local_user, content="hi")
@ -177,7 +177,7 @@ class Activitystreams(TestCase):
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_boost_to_following_timeline(self, *_): def test_boost_to_following_timeline(self, *_):
"""add a boost and deduplicate the boosted status on the timeline""" """add a boost and deduplicate the boosted status on the timeline"""
self.local_user.following.add(self.another_user) self.local_user.following.add(self.another_user)
@ -199,7 +199,7 @@ class Activitystreams(TestCase):
@patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores")
@patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_boost_to_same_timeline(self, *_): def test_boost_to_same_timeline(self, *_):
"""add a boost and deduplicate the boosted status on the timeline""" """add a boost and deduplicate the boosted status on the timeline"""
status = models.Status.objects.create(user=self.local_user, content="hi") status = models.Status.objects.create(user=self.local_user, content="hi")

View file

@ -1,11 +1,14 @@
""" testing book data connectors """ """ testing book data connectors """
import json import json
import pathlib import pathlib
from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
import responses import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.connectors.inventaire import Connector, get_language_code from bookwyrm.connectors.inventaire import Connector, get_language_code
from bookwyrm.connectors.connector_manager import ConnectorException
class Inventaire(TestCase): class Inventaire(TestCase):
@ -48,6 +51,44 @@ class Inventaire(TestCase):
self.assertEqual(result["wdt:P31"], ["wd:Q3331189"]) self.assertEqual(result["wdt:P31"], ["wd:Q3331189"])
self.assertEqual(result["uri"], "isbn:9780375757853") self.assertEqual(result["uri"], "isbn:9780375757853")
@responses.activate
def test_get_book_data_invalid(self):
"""error if there isn't any entity data"""
responses.add(
responses.GET,
"https://test.url/ok",
json={
"entities": {},
"redirects": {},
},
)
with self.assertRaises(ConnectorException):
self.connector.get_book_data("https://test.url/ok")
@responses.activate
def test_search(self):
"""min confidence filtering"""
responses.add(
responses.GET,
"https://inventaire.io/search?q=hi",
json={
"results": [
{
"_score": 200,
"label": "hello",
},
{
"_score": 100,
"label": "hi",
},
],
},
)
results = self.connector.search("hi", min_confidence=0.5)
self.assertEqual(len(results), 1)
self.assertEqual(results[0].title, "hello")
def test_format_search_result(self): def test_format_search_result(self):
"""json to search result objs""" """json to search result objs"""
search_file = pathlib.Path(__file__).parent.joinpath( search_file = pathlib.Path(__file__).parent.joinpath(
@ -157,6 +198,88 @@ class Inventaire(TestCase):
"https://covers.inventaire.io/img/entities/12345", "https://covers.inventaire.io/img/entities/12345",
) )
def test_isbn_search_empty(self):
"""another search type"""
search_results = {}
results = self.connector.parse_isbn_search_data(search_results)
self.assertEqual(results, [])
def test_isbn_search_no_title(self):
"""another search type"""
search_file = pathlib.Path(__file__).parent.joinpath(
"../data/inventaire_isbn_search.json"
)
search_results = json.loads(search_file.read_bytes())
search_results["entities"]["isbn:9782290349229"]["claims"]["wdt:P1476"] = None
result = self.connector.format_isbn_search_result(
search_results.get("entities")
)
self.assertIsNone(result)
def test_is_work_data(self):
"""is it a work"""
work_file = pathlib.Path(__file__).parent.joinpath(
"../data/inventaire_work.json"
)
work_data = json.loads(work_file.read_bytes())
with patch("bookwyrm.connectors.inventaire.get_data") as get_data_mock:
get_data_mock.return_value = work_data
formatted = self.connector.get_book_data("hi")
self.assertTrue(self.connector.is_work_data(formatted))
edition_file = pathlib.Path(__file__).parent.joinpath(
"../data/inventaire_edition.json"
)
edition_data = json.loads(edition_file.read_bytes())
with patch("bookwyrm.connectors.inventaire.get_data") as get_data_mock:
get_data_mock.return_value = edition_data
formatted = self.connector.get_book_data("hi")
self.assertFalse(self.connector.is_work_data(formatted))
@responses.activate
def test_get_edition_from_work_data(self):
"""load edition"""
responses.add(
responses.GET,
"https://inventaire.io/?action=by-uris&uris=hello",
json={"entities": {}},
)
data = {"uri": "blah"}
with patch(
"bookwyrm.connectors.inventaire.Connector.load_edition_data"
) as loader_mock, patch(
"bookwyrm.connectors.inventaire.Connector.get_book_data"
) as getter_mock:
loader_mock.return_value = {"uris": ["hello"]}
self.connector.get_edition_from_work_data(data)
self.assertTrue(getter_mock.called)
with patch(
"bookwyrm.connectors.inventaire.Connector.load_edition_data"
) as loader_mock:
loader_mock.return_value = {"uris": []}
with self.assertRaises(ConnectorException):
self.connector.get_edition_from_work_data(data)
@responses.activate
def test_get_work_from_edition_data(self):
"""load work"""
responses.add(
responses.GET,
"https://inventaire.io/?action=by-uris&uris=hello",
)
data = {"wdt:P629": ["hello"]}
with patch("bookwyrm.connectors.inventaire.Connector.get_book_data") as mock:
self.connector.get_work_from_edition_data(data)
self.assertEqual(mock.call_count, 1)
args = mock.call_args[0]
self.assertEqual(args[0], "https://inventaire.io?action=by-uris&uris=hello")
data = {"wdt:P629": [None]}
with self.assertRaises(ConnectorException):
self.connector.get_work_from_edition_data(data)
def test_get_language_code(self): def test_get_language_code(self):
"""get english or whatever is in reach""" """get english or whatever is in reach"""
options = { options = {

View file

@ -0,0 +1,5 @@
id,title,author,ISBN13,rating,shelf,review,added,finished
38,Gideon the Ninth,Tamsyn Muir,"9781250313195",,read,,2021-11-10,2021-11-11
48,Harrow the Ninth,Tamsyn Muir,,3,read,,2021-11-10
23,Subcutanean,Aaron A. Reed,,,read,,2021-11-10
10,Patisserie at Home,Mélanie Dupuis,"9780062445315",2,read,"mixed feelings",2021-11-10,2021-11-11
Can't render this file because it has a wrong number of fields in line 3.

View file

@ -1,5 +0,0 @@
Book Id,Title,Author,Author l-f,Additional Authors,ISBN,ISBN13,My Rating,Average Rating,Publisher,Binding,Number of Pages,Year Published,Original Publication Year,Date Read,Date Added,Bookshelves,Bookshelves with positions,Exclusive Shelf,My Review,Spoiler,Private Notes,Read Count,Recommended For,Recommended By,Owned Copies,Original Purchase Date,Original Purchase Location,Condition,Condition Description,BCID
42036538,Gideon the Ninth (The Locked Tomb #1),Tamsyn Muir,"Muir, Tamsyn",,"=""1250313198""","=""9781250313195""",0,4.20,Tor,Hardcover,448,2019,2019,2020/10/25,2020/10/21,,,read,,,,1,,,0,,,,,
52691223,Subcutanean,Aaron A. Reed,"Reed, Aaron A.",,"=""""","=""""",0,4.45,,Paperback,232,2020,,2020/03/06,2020/03/05,,,read,,,,1,,,0,,,,,
28694510,Patisserie at Home,Mélanie Dupuis,"Dupuis, Mélanie",Anne Cazor,"=""0062445316""","=""9780062445315""",2,4.60,Harper Design,Hardcover,288,2016,,,2019/07/08,,,read,,,,2,,,0,,,,,
1 Book Id Title Author Author l-f Additional Authors ISBN ISBN13 My Rating Average Rating Publisher Binding Number of Pages Year Published Original Publication Year Date Read Date Added Bookshelves Bookshelves with positions Exclusive Shelf My Review Spoiler Private Notes Read Count Recommended For Recommended By Owned Copies Original Purchase Date Original Purchase Location Condition Condition Description BCID
2 42036538 Gideon the Ninth (The Locked Tomb #1) Tamsyn Muir Muir, Tamsyn ="1250313198" ="9781250313195" 0 4.20 Tor Hardcover 448 2019 2019 2020/10/25 2020/10/21 read 1 0
3 52691223 Subcutanean Aaron A. Reed Reed, Aaron A. ="" ="" 0 4.45 Paperback 232 2020 2020/03/06 2020/03/05 read 1 0
4 28694510 Patisserie at Home Mélanie Dupuis Dupuis, Mélanie Anne Cazor ="0062445316" ="9780062445315" 2 4.60 Harper Design Hardcover 288 2016 2019/07/08 read 2 0

View file

@ -1,4 +1,4 @@
Book Id,Title,Author,Author l-f,Additional Authors,ISBN,ISBN13,My Rating,Average Rating,Publisher,Binding,Number of Pages,Year Published,Original Publication Year,Date Read,Date Added,Bookshelves,Bookshelves with positions,Exclusive Shelf,My Review,Spoiler,Private Notes,Read Count,Recommended For,Recommended By,Owned Copies,Original Purchase Date,Original Purchase Location,Condition,Condition Description,BCID Book Id,Title,Author,Author l-f,Additional Authors,ISBN,ISBN13,My Rating,Average Rating,Publisher,Binding,Number of Pages,Year Published,Original Publication Year,Date Read,Date Added,Bookshelves,Bookshelves with positions,Exclusive Shelf,My Review,Spoiler,Private Notes,Read Count,Recommended For,Recommended By,Owned Copies,Original Purchase Date,Original Purchase Location,Condition,Condition Description,BCID
42036538,Gideon the Ninth (The Locked Tomb #1),Tamsyn Muir,"Muir, Tamsyn",,"=""1250313198""","=""9781250313195""",0,4.20,Tor,Hardcover,448,2019,2019,2020/10/25,2020/10/21,,,read,,,,1,,,0,,,,, 42036538,Gideon the Ninth (The Locked Tomb #1),Tamsyn Muir,"Muir, Tamsyn",,"=""1250313198""","=""9781250313195""",3,4.20,Tor,Hardcover,448,2019,2019,2020/10/25,2020/10/21,,,read,,,,1,,,0,,,,,
52691223,Subcutanean,Aaron A. Reed,"Reed, Aaron A.",,"=""""","=""""",0,4.45,,Paperback,232,2020,,2020/03/06,2020/03/05,,,read,,,,1,,,0,,,,, 52691223,Subcutanean,Aaron A. Reed,"Reed, Aaron A.",,"=""""","=""""",0,4.45,,Paperback,232,2020,,2020/03/06,2020/03/05,,,read,,,,1,,,0,,,,,
28694510,Patisserie at Home,Mélanie Dupuis,"Dupuis, Mélanie",Anne Cazor,"=""0062445316""","=""9780062445315""",2,4.60,Harper Design,Hardcover,288,2016,,,2019/07/08,,,read,"mixed feelings",,,2,,,0,,,,, 28694510,Patisserie at Home,Mélanie Dupuis,"Dupuis, Mélanie",Anne Cazor,"=""0062445316""","=""9780062445315""",2,4.60,Harper Design,Hardcover,288,2016,,,2019/07/08,,,read,"mixed feelings",,,2,,,0,,,,,

1 Book Id Title Author Author l-f Additional Authors ISBN ISBN13 My Rating Average Rating Publisher Binding Number of Pages Year Published Original Publication Year Date Read Date Added Bookshelves Bookshelves with positions Exclusive Shelf My Review Spoiler Private Notes Read Count Recommended For Recommended By Owned Copies Original Purchase Date Original Purchase Location Condition Condition Description BCID
2 42036538 Gideon the Ninth (The Locked Tomb #1) Tamsyn Muir Muir, Tamsyn ="1250313198" ="9781250313195" 0 3 4.20 Tor Hardcover 448 2019 2019 2020/10/25 2020/10/21 read 1 0
3 52691223 Subcutanean Aaron A. Reed Reed, Aaron A. ="" ="" 0 4.45 Paperback 232 2020 2020/03/06 2020/03/05 read 1 0
4 28694510 Patisserie at Home Mélanie Dupuis Dupuis, Mélanie Anne Cazor ="0062445316" ="9780062445315" 2 4.60 Harper Design Hardcover 288 2016 2019/07/08 read mixed feelings 2 0

View file

@ -1,4 +1,4 @@
Book Id Title Sort Character Primary Author Primary Author Role Secondary Author Secondary Author Roles Publication Date Review Rating Comment Private Comment Summary Media Physical Description Weight Height Thickness Length Dimensions Page Count LCCN Acquired Date Started Date Read Barcode BCID Tags Collections Languages Original Languages LC Classification ISBN ISBNs Subjects Dewey Decimal Dewey Wording Other Call Number Copies Source Entry Date From Where OCLC Work id Lending Patron Lending Status Lending Start Lending End Book Id Title Sort Character Primary Author Primary Author Role Secondary Author Secondary Author Roles Publication Date Review Rating Comment Private Comment Summary Media Physical Description Weight Height Thickness Length Dimensions Page Count LCCN Acquired Date Started Date Read Barcode BCID Tags Collections Languages Original Languages LC Classification ISBN ISBNs Subjects Dewey Decimal Dewey Wording Other Call Number Copies Source Entry Date From Where OCLC Work id Lending Patron Lending Status Lending Start Lending End
5498194 Marelle 1 Cortázar, Julio Gallimard (1979), Poche 1979 chef d'oeuvre 4.5 Marelle by Julio Cortázar (1979) Broché 590 p.; 7.24 inches 1.28 pounds 7.24 inches 1.26 inches 4.96 inches 7.24 x 4.96 x 1.26 inches 590 [2007-04-16] [2007-05-08] roman, espagnol, expérimental, bohème, philosophie Your library French Spanish PQ7797 .C7145 [2070291340] 2070291340, 9782070291342 Cortâazar, Julio. Rayuela 863 Literature > Spanish And Portuguese > Spanish fiction 1 Amazon.fr [2006-08-09] 57814 5498194 Marelle 1 Cortazar, Julio Gallimard (1979), Poche 1979 chef d'oeuvre 4.5 Marelle by Julio Cortázar (1979) Broché 590 p.; 7.24 inches 1.28 pounds 7.24 inches 1.26 inches 4.96 inches 7.24 x 4.96 x 1.26 inches 590 [2007-04-16] [2007-05-08] roman, espagnol, expérimental, bohème, philosophie Your library French Spanish PQ7797 .C7145 [2070291340] 2070291340, 9782070291342 Cortâazar, Julio. Rayuela 863 Literature > Spanish And Portuguese > Spanish fiction 1 Amazon.fr [2006-08-09] 57814
5015319 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) 1 Roubaud, Jacques Seuil (1989), Unknown Binding 1989 5 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) by Jacques Roubaud (1989) Broché 411 p.; 7.72 inches 0.88 pounds 7.72 inches 1.02 inches 5.43 inches 7.72 x 5.43 x 1.02 inches 411 Your library English PQ2678 .O77 [2020104725] 2020104725, 9782020104722 Autobiographical fiction|Roubaud, Jacques > Fiction 813 American And Canadian > Fiction > Literature 1 Amazon.com [2006-07-25] 478910 5015319 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) 1 Roubaud, Jacques Seuil (1989), Unknown Binding 1989 5 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) by Jacques Roubaud (1989) Broché 411 p.; 7.72 inches 0.88 pounds 7.72 inches 1.02 inches 5.43 inches 7.72 x 5.43 x 1.02 inches 411 Your library English PQ2678 .O77 [2020104725] 2020104725, 9782020104722 Autobiographical fiction|Roubaud, Jacques > Fiction 813 American And Canadian > Fiction > Literature 1 Amazon.com [2006-07-25] 478910
5015399 Le Maître et Marguerite 1 Boulgakov, Mikhaïl Pocket (1994), Poche 1994 Le Maître et Marguerite by Mikhaïl Boulgakov (1994) Broché 579 p.; 7.09 inches 0.66 pounds 7.09 inches 1.18 inches 4.33 inches 7.09 x 4.33 x 1.18 inches 579 Your library French PG3476 .B78 [2266062328] 2266062328, 9782266062329 Allegories|Bulgakov|Good and evil > Fiction|Humanities|Jerusalem > Fiction|Jesus Christ > Fiction|Literature|Mental illness > Fiction|Moscow (Russia) > Fiction|Novel|Pilate, Pontius, 1st cent. > Fiction|Political fiction|Russia > Fiction|Russian fiction|Russian publications (Form Entry)|Soviet Union > History > 1925-1953 > Fiction|literature 891.7342 1917-1945 > 1917-1991 (USSR) > Literature > Literature of other Indo-European languages > Other Languages > Russian > Russian Fiction 1 Amazon.fr [2006-07-25] 10151 5015399 Le Maître et Marguerite 1 Boulgakov, Mikhaïl Pocket (1994), Poche 1994 Le Maître et Marguerite by Mikhaïl Boulgakov (1994) Broché 579 p.; 7.09 inches 0.66 pounds 7.09 inches 1.18 inches 4.33 inches 7.09 x 4.33 x 1.18 inches 579 Your library French PG3476 .B78 [2266062328] 2266062328, 9782266062329 Allegories|Bulgakov|Good and evil > Fiction|Humanities|Jerusalem > Fiction|Jesus Christ > Fiction|Literature|Mental illness > Fiction|Moscow (Russia) > Fiction|Novel|Pilate, Pontius, 1st cent. > Fiction|Political fiction|Russia > Fiction|Russian fiction|Russian publications (Form Entry)|Soviet Union > History > 1925-1953 > Fiction|literature 891.7342 1917-1945 > 1917-1991 (USSR) > Literature > Literature of other Indo-European languages > Other Languages > Russian > Russian Fiction 1 Amazon.fr [2006-07-25] 10151

1 Book Id Title Sort Character Primary Author Primary Author Role Secondary Author Secondary Author Roles Publication Date Review Rating Comment Private Comment Summary Media Physical Description Weight Height Thickness Length Dimensions Page Count LCCN Acquired Date Started Date Read Barcode BCID Tags Collections Languages Original Languages LC Classification ISBN ISBNs Subjects Dewey Decimal Dewey Wording Other Call Number Copies Source Entry Date From Where OCLC Work id Lending Patron Lending Status Lending Start Lending End
2 5498194 Marelle 1 Cortázar, Julio Cortazar, Julio Gallimard (1979), Poche 1979 chef d'oeuvre 4.5 Marelle by Julio Cortázar (1979) Broché 590 p.; 7.24 inches 1.28 pounds 7.24 inches 1.26 inches 4.96 inches 7.24 x 4.96 x 1.26 inches 590 [2007-04-16] [2007-05-08] roman, espagnol, expérimental, bohème, philosophie Your library French Spanish PQ7797 .C7145 [2070291340] 2070291340, 9782070291342 Cortâazar, Julio. Rayuela 863 Literature > Spanish And Portuguese > Spanish fiction 1 Amazon.fr [2006-08-09] 57814
3 5015319 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) 1 Roubaud, Jacques Seuil (1989), Unknown Binding 1989 5 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) by Jacques Roubaud (1989) Broché 411 p.; 7.72 inches 0.88 pounds 7.72 inches 1.02 inches 5.43 inches 7.72 x 5.43 x 1.02 inches 411 Your library English PQ2678 .O77 [2020104725] 2020104725, 9782020104722 Autobiographical fiction|Roubaud, Jacques > Fiction 813 American And Canadian > Fiction > Literature 1 Amazon.com [2006-07-25] 478910
4 5015399 Le Maître et Marguerite 1 Boulgakov, Mikhaïl Pocket (1994), Poche 1994 Le Maître et Marguerite by Mikhaïl Boulgakov (1994) Broché 579 p.; 7.09 inches 0.66 pounds 7.09 inches 1.18 inches 4.33 inches 7.09 x 4.33 x 1.18 inches 579 Your library French PG3476 .B78 [2266062328] 2266062328, 9782266062329 Allegories|Bulgakov|Good and evil > Fiction|Humanities|Jerusalem > Fiction|Jesus Christ > Fiction|Literature|Mental illness > Fiction|Moscow (Russia) > Fiction|Novel|Pilate, Pontius, 1st cent. > Fiction|Political fiction|Russia > Fiction|Russian fiction|Russian publications (Form Entry)|Soviet Union > History > 1925-1953 > Fiction|literature 891.7342 1917-1945 > 1917-1991 (USSR) > Literature > Literature of other Indo-European languages > Other Languages > Russian > Russian Fiction 1 Amazon.fr [2006-07-25] 10151

View file

@ -0,0 +1,3 @@
Title,Authors,Contributors,ISBN,Format,Read Status,Date Added,Last Date Read,Dates Read,Read Count,Moods,Pace,Character- or Plot-Driven?,Strong Character Development?,Loveable Characters?,Diverse Characters?,Flawed Characters?,Star Rating,Review,Content Warnings,Content Warning Description,Tags,Owned?
Always Coming Home,"Ursula K. Le Guin, Todd Barton, Margaret Chodos-Irvine","",,,to-read,2021/05/10,"","",0,"",,,,,,,,,"",,"",No
Subprime Attention Crisis,Tim Hwang,"",,,read,2021/05/10,"","",1,informative,fast,,,,,,5.0,"","","","",No
1 Title Authors Contributors ISBN Format Read Status Date Added Last Date Read Dates Read Read Count Moods Pace Character- or Plot-Driven? Strong Character Development? Loveable Characters? Diverse Characters? Flawed Characters? Star Rating Review Content Warnings Content Warning Description Tags Owned?
2 Always Coming Home Ursula K. Le Guin, Todd Barton, Margaret Chodos-Irvine to-read 2021/05/10 0 No
3 Subprime Attention Crisis Tim Hwang read 2021/05/10 1 informative fast 5.0 No

View file

@ -1,17 +1,14 @@
""" testing import """ """ testing import """
from collections import namedtuple
import csv
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
import datetime import datetime
import pytz import pytz
from django.test import TestCase from django.test import TestCase
import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.importers import GoodreadsImporter from bookwyrm.importers import GoodreadsImporter
from bookwyrm.importers.importer import import_data, handle_imported_book from bookwyrm.importers.importer import handle_imported_book
def make_date(*args): def make_date(*args):
@ -34,7 +31,7 @@ class GoodreadsImport(TestCase):
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay" "bookwyrm.activitystreams.populate_stream_task.delay"
): ):
self.user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True "mouse", "mouse@mouse.mouse", "password", local=True
) )
@ -47,15 +44,17 @@ class GoodreadsImport(TestCase):
def test_create_job(self, *_): def test_create_job(self, *_):
"""creates the import job entry and checks csv""" """creates the import job entry and checks csv"""
import_job = self.importer.create_job(self.user, self.csv, False, "public") import_job = self.importer.create_job(
self.assertEqual(import_job.user, self.user) self.local_user, self.csv, False, "public"
self.assertEqual(import_job.include_reviews, False) )
self.assertEqual(import_job.privacy, "public")
import_items = models.ImportItem.objects.filter(job=import_job).all() import_items = models.ImportItem.objects.filter(job=import_job).all()
self.assertEqual(len(import_items), 3) self.assertEqual(len(import_items), 3)
self.assertEqual(import_items[0].index, 0) self.assertEqual(import_items[0].index, 0)
self.assertEqual(import_items[0].data["Book Id"], "42036538") self.assertEqual(import_items[0].data["Book Id"], "42036538")
self.assertEqual(import_items[0].normalized_data["isbn_13"], '="9781250313195"')
self.assertEqual(import_items[0].normalized_data["isbn_10"], '="1250313198"')
self.assertEqual(import_items[1].index, 1) self.assertEqual(import_items[1].index, 1)
self.assertEqual(import_items[1].data["Book Id"], "52691223") self.assertEqual(import_items[1].data["Book Id"], "52691223")
self.assertEqual(import_items[2].index, 2) self.assertEqual(import_items[2].index, 2)
@ -63,12 +62,16 @@ class GoodreadsImport(TestCase):
def test_create_retry_job(self, *_): def test_create_retry_job(self, *_):
"""trying again with items that didn't import""" """trying again with items that didn't import"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_job = self.importer.create_job(
self.local_user, self.csv, False, "unlisted"
)
import_items = models.ImportItem.objects.filter(job=import_job).all()[:2] import_items = models.ImportItem.objects.filter(job=import_job).all()[:2]
retry = self.importer.create_retry_job(self.user, import_job, import_items) retry = self.importer.create_retry_job(
self.local_user, import_job, import_items
)
self.assertNotEqual(import_job, retry) self.assertNotEqual(import_job, retry)
self.assertEqual(retry.user, self.user) self.assertEqual(retry.user, self.local_user)
self.assertEqual(retry.include_reviews, False) self.assertEqual(retry.include_reviews, False)
self.assertEqual(retry.privacy, "unlisted") self.assertEqual(retry.privacy, "unlisted")
@ -79,52 +82,20 @@ class GoodreadsImport(TestCase):
self.assertEqual(retry_items[1].index, 1) self.assertEqual(retry_items[1].index, 1)
self.assertEqual(retry_items[1].data["Book Id"], "52691223") self.assertEqual(retry_items[1].data["Book Id"], "52691223")
def test_start_import(self, *_):
"""begin loading books"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
MockTask = namedtuple("Task", ("id"))
mock_task = MockTask(7)
with patch("bookwyrm.importers.importer.import_data.delay") as start:
start.return_value = mock_task
self.importer.start_import(import_job)
import_job.refresh_from_db()
self.assertEqual(import_job.task_id, "7")
@responses.activate
def test_import_data(self, *_):
"""resolve entry"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
book = models.Edition.objects.create(title="Test Book")
with patch(
"bookwyrm.models.import_job.ImportItem.get_book_from_isbn"
) as resolve:
resolve.return_value = book
with patch("bookwyrm.importers.importer.handle_imported_book"):
import_data(self.importer.service, import_job.id)
import_item = models.ImportItem.objects.get(job=import_job, index=0)
self.assertEqual(import_item.book.id, book.id)
def test_handle_imported_book(self, *_): def test_handle_imported_book(self, *_):
"""goodreads import added a book, this adds related connections""" """goodreads import added a book, this adds related connections"""
shelf = self.user.shelf_set.filter(identifier="read").first() shelf = self.local_user.shelf_set.filter(identifier="read").first()
self.assertIsNone(shelf.books.first()) self.assertIsNone(shelf.books.first())
import_job = models.ImportJob.objects.create(user=self.user) import_job = self.importer.create_job(
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") self.local_user, self.csv, False, "public"
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
for index, entry in enumerate(list(csv.DictReader(csv_file))):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book
) )
break import_item = import_job.items.first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book( handle_imported_book(import_item)
self.importer.service, self.user, import_item, False, "public"
)
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
@ -132,77 +103,7 @@ class GoodreadsImport(TestCase):
shelf.shelfbook_set.first().shelved_date, make_date(2020, 10, 21) shelf.shelfbook_set.first().shelved_date, make_date(2020, 10, 21)
) )
readthrough = models.ReadThrough.objects.get(user=self.user) readthrough = models.ReadThrough.objects.get(user=self.local_user)
self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date, make_date(2020, 10, 21))
self.assertEqual(readthrough.finish_date, make_date(2020, 10, 25))
def test_handle_imported_book_already_shelved(self, *_):
"""goodreads import added a book, this adds related connections"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = self.user.shelf_set.filter(identifier="to-read").first()
models.ShelfBook.objects.create(
shelf=shelf,
user=self.user,
book=self.book,
shelved_date=make_date(2020, 2, 2),
)
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
for index, entry in enumerate(list(csv.DictReader(csv_file))):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book
)
break
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book(
self.importer.service, self.user, import_item, False, "public"
)
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
self.assertEqual(
shelf.shelfbook_set.first().shelved_date, make_date(2020, 2, 2)
)
self.assertIsNone(self.user.shelf_set.get(identifier="read").books.first())
readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date, make_date(2020, 10, 21))
self.assertEqual(readthrough.finish_date, make_date(2020, 10, 25))
def test_handle_import_twice(self, *_):
"""re-importing books"""
shelf = self.user.shelf_set.filter(identifier="read").first()
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
for index, entry in enumerate(list(csv.DictReader(csv_file))):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book
)
break
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book(
self.importer.service, self.user, import_item, False, "public"
)
handle_imported_book(
self.importer.service, self.user, import_item, False, "public"
)
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
self.assertEqual(
shelf.shelfbook_set.first().shelved_date, make_date(2020, 10, 21)
)
readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date, make_date(2020, 10, 21)) self.assertEqual(readthrough.start_date, make_date(2020, 10, 21))
self.assertEqual(readthrough.finish_date, make_date(2020, 10, 25)) self.assertEqual(readthrough.finish_date, make_date(2020, 10, 25))
@ -210,20 +111,17 @@ class GoodreadsImport(TestCase):
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_handle_imported_book_review(self, *_): def test_handle_imported_book_review(self, *_):
"""goodreads review import""" """goodreads review import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = self.importer.create_job(
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") self.local_user, self.csv, True, "unlisted"
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
entry = list(csv.DictReader(csv_file))[2]
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book
) )
import_item = import_job.items.get(index=2)
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book( handle_imported_book(import_item)
self.importer.service, self.user, import_item, True, "unlisted"
) review = models.Review.objects.get(book=self.book, user=self.local_user)
review = models.Review.objects.get(book=self.book, user=self.user)
self.assertEqual(review.content, "mixed feelings") self.assertEqual(review.content, "mixed feelings")
self.assertEqual(review.rating, 2) self.assertEqual(review.rating, 2)
self.assertEqual(review.published_date, make_date(2019, 7, 8)) self.assertEqual(review.published_date, make_date(2019, 7, 8))
@ -232,42 +130,18 @@ class GoodreadsImport(TestCase):
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_handle_imported_book_rating(self, *_): def test_handle_imported_book_rating(self, *_):
"""goodreads rating import""" """goodreads rating import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = self.importer.create_job(
datafile = pathlib.Path(__file__).parent.joinpath( self.local_user, self.csv, True, "unlisted"
"../data/goodreads-rating.csv"
)
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
entry = list(csv.DictReader(csv_file))[2]
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book
) )
import_item = import_job.items.filter(index=0).first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book( handle_imported_book(import_item)
self.importer.service, self.user, import_item, True, "unlisted"
) review = models.ReviewRating.objects.get(book=self.book, user=self.local_user)
review = models.ReviewRating.objects.get(book=self.book, user=self.user)
self.assertIsInstance(review, models.ReviewRating) self.assertIsInstance(review, models.ReviewRating)
self.assertEqual(review.rating, 2) self.assertEqual(review.rating, 3)
self.assertEqual(review.published_date, make_date(2019, 7, 8)) self.assertEqual(review.published_date, make_date(2020, 10, 25))
self.assertEqual(review.privacy, "unlisted") self.assertEqual(review.privacy, "unlisted")
def test_handle_imported_book_reviews_disabled(self, *_):
"""goodreads review import"""
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
csv_file = open(datafile, "r") # pylint: disable=unspecified-encoding
entry = list(csv.DictReader(csv_file))[2]
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book(
self.importer.service, self.user, import_item, False, "unlisted"
)
self.assertFalse(
models.Review.objects.filter(book=self.book, user=self.user).exists()
)

View file

@ -0,0 +1,346 @@
""" testing import """
import pathlib
from unittest.mock import patch
import datetime
import pytz
from django.test import TestCase
import responses
from bookwyrm import models
from bookwyrm.importers import Importer
from bookwyrm.importers.importer import start_import_task, import_item_task
from bookwyrm.importers.importer import handle_imported_book
def make_date(*args):
"""helper function to easily generate a date obj"""
return datetime.datetime(*args, tzinfo=pytz.UTC)
# pylint: disable=consider-using-with
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
class GenericImporter(TestCase):
"""importing from csv"""
def setUp(self):
"""use a test csv"""
self.importer = Importer()
datafile = pathlib.Path(__file__).parent.joinpath("../data/generic.csv")
self.csv = open(datafile, "r", encoding=self.importer.encoding)
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True
)
work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=work,
)
def test_create_job(self, *_):
"""creates the import job entry and checks csv"""
import_job = self.importer.create_job(
self.local_user, self.csv, False, "public"
)
self.assertEqual(import_job.user, self.local_user)
self.assertEqual(import_job.include_reviews, False)
self.assertEqual(import_job.privacy, "public")
import_items = models.ImportItem.objects.filter(job=import_job).all()
self.assertEqual(len(import_items), 4)
self.assertEqual(import_items[0].index, 0)
self.assertEqual(import_items[0].normalized_data["id"], "38")
self.assertEqual(import_items[0].normalized_data["title"], "Gideon the Ninth")
self.assertEqual(import_items[0].normalized_data["authors"], "Tamsyn Muir")
self.assertEqual(import_items[0].normalized_data["isbn_13"], "9781250313195")
self.assertIsNone(import_items[0].normalized_data["isbn_10"])
self.assertEqual(import_items[0].normalized_data["shelf"], "read")
self.assertEqual(import_items[1].index, 1)
self.assertEqual(import_items[1].normalized_data["id"], "48")
self.assertEqual(import_items[1].normalized_data["title"], "Harrow the Ninth")
self.assertEqual(import_items[2].index, 2)
self.assertEqual(import_items[2].normalized_data["id"], "23")
self.assertEqual(import_items[2].normalized_data["title"], "Subcutanean")
self.assertEqual(import_items[3].index, 3)
self.assertEqual(import_items[3].normalized_data["id"], "10")
self.assertEqual(import_items[3].normalized_data["title"], "Patisserie at Home")
def test_create_retry_job(self, *_):
"""trying again with items that didn't import"""
import_job = self.importer.create_job(
self.local_user, self.csv, False, "unlisted"
)
import_items = models.ImportItem.objects.filter(job=import_job).all()[:2]
retry = self.importer.create_retry_job(
self.local_user, import_job, import_items
)
self.assertNotEqual(import_job, retry)
self.assertEqual(retry.user, self.local_user)
self.assertEqual(retry.include_reviews, False)
self.assertEqual(retry.privacy, "unlisted")
retry_items = models.ImportItem.objects.filter(job=retry).all()
self.assertEqual(len(retry_items), 2)
self.assertEqual(retry_items[0].index, 0)
self.assertEqual(retry_items[0].normalized_data["id"], "38")
self.assertEqual(retry_items[1].index, 1)
self.assertEqual(retry_items[1].normalized_data["id"], "48")
def test_start_import(self, *_):
"""check that a task was created"""
import_job = self.importer.create_job(
self.local_user, self.csv, False, "unlisted"
)
with patch("bookwyrm.importers.importer.start_import_task.delay") as mock:
self.importer.start_import(import_job)
self.assertEqual(mock.call_count, 1)
@responses.activate
def test_start_import_task(self, *_):
"""resolve entry"""
import_job = self.importer.create_job(
self.local_user, self.csv, False, "unlisted"
)
with patch("bookwyrm.importers.importer.import_item_task.delay") as mock:
start_import_task(import_job.id)
self.assertEqual(mock.call_count, 4)
@responses.activate
def test_import_item_task(self, *_):
"""resolve entry"""
import_job = self.importer.create_job(
self.local_user, self.csv, False, "unlisted"
)
import_item = models.ImportItem.objects.get(job=import_job, index=0)
with patch(
"bookwyrm.models.import_job.ImportItem.get_book_from_isbn"
) as resolve:
resolve.return_value = self.book
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
import_item_task(import_item.id)
kwargs = mock.call_args.kwargs
self.assertEqual(kwargs["queue"], "low_priority")
import_item.refresh_from_db()
def test_complete_job(self, *_):
"""test notification"""
import_job = self.importer.create_job(
self.local_user, self.csv, False, "unlisted"
)
items = import_job.items.all()
for item in items[:3]:
item.fail_reason = "hello"
item.save()
item.update_job()
self.assertFalse(
models.Notification.objects.filter(
user=self.local_user,
related_import=import_job,
notification_type="IMPORT",
).exists()
)
item = items[3]
item.fail_reason = "hello"
item.save()
item.update_job()
import_job.refresh_from_db()
self.assertTrue(import_job.complete)
self.assertTrue(
models.Notification.objects.filter(
user=self.local_user,
related_import=import_job,
notification_type="IMPORT",
).exists()
)
def test_handle_imported_book(self, *_):
"""import added a book, this adds related connections"""
shelf = self.local_user.shelf_set.filter(identifier="read").first()
self.assertIsNone(shelf.books.first())
import_job = self.importer.create_job(
self.local_user, self.csv, False, "public"
)
import_item = import_job.items.first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
def test_handle_imported_book_already_shelved(self, *_):
"""import added a book, this adds related connections"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
shelf = self.local_user.shelf_set.filter(identifier="to-read").first()
models.ShelfBook.objects.create(
shelf=shelf,
user=self.local_user,
book=self.book,
shelved_date=make_date(2020, 2, 2),
)
import_job = self.importer.create_job(
self.local_user, self.csv, False, "public"
)
import_item = import_job.items.first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
self.assertEqual(
shelf.shelfbook_set.first().shelved_date, make_date(2020, 2, 2)
)
self.assertIsNone(
self.local_user.shelf_set.get(identifier="read").books.first()
)
def test_handle_import_twice(self, *_):
"""re-importing books"""
shelf = self.local_user.shelf_set.filter(identifier="read").first()
import_job = self.importer.create_job(
self.local_user, self.csv, False, "public"
)
import_item = import_job.items.first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
handle_imported_book(import_item)
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
self.assertEqual(models.ReadThrough.objects.count(), 1)
@patch("bookwyrm.activitystreams.add_status_task.delay")
def test_handle_imported_book_review(self, *_):
"""review import"""
import_job = self.importer.create_job(
self.local_user, self.csv, True, "unlisted"
)
import_item = import_job.items.filter(index=3).first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
with patch("bookwyrm.models.Status.broadcast") as broadcast_mock:
handle_imported_book(import_item)
kwargs = broadcast_mock.call_args.kwargs
self.assertEqual(kwargs["software"], "bookwyrm")
review = models.Review.objects.get(book=self.book, user=self.local_user)
self.assertEqual(review.content, "mixed feelings")
self.assertEqual(review.rating, 2.0)
self.assertEqual(review.privacy, "unlisted")
import_item.refresh_from_db()
self.assertEqual(import_item.linked_review, review)
@patch("bookwyrm.activitystreams.add_status_task.delay")
def test_handle_imported_book_rating(self, *_):
"""rating import"""
import_job = self.importer.create_job(
self.local_user, self.csv, True, "unlisted"
)
import_item = import_job.items.filter(index=1).first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
review = models.ReviewRating.objects.get(book=self.book, user=self.local_user)
self.assertIsInstance(review, models.ReviewRating)
self.assertEqual(review.rating, 3.0)
self.assertEqual(review.privacy, "unlisted")
import_item.refresh_from_db()
self.assertEqual(import_item.linked_review.id, review.id)
@patch("bookwyrm.activitystreams.add_status_task.delay")
def test_handle_imported_book_rating_duplicate_with_link(self, *_):
"""rating import twice"""
import_job = self.importer.create_job(
self.local_user, self.csv, True, "unlisted"
)
import_item = import_job.items.filter(index=1).first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
handle_imported_book(import_item)
review = models.ReviewRating.objects.get(book=self.book, user=self.local_user)
self.assertIsInstance(review, models.ReviewRating)
self.assertEqual(review.rating, 3.0)
self.assertEqual(review.privacy, "unlisted")
import_item.refresh_from_db()
self.assertEqual(import_item.linked_review.id, review.id)
@patch("bookwyrm.activitystreams.add_status_task.delay")
def test_handle_imported_book_rating_duplicate_without_link(self, *_):
"""rating import twice"""
import_job = self.importer.create_job(
self.local_user, self.csv, True, "unlisted"
)
import_item = import_job.items.filter(index=1).first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
import_item.refresh_from_db()
import_item.linked_review = None
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
review = models.ReviewRating.objects.get(book=self.book, user=self.local_user)
self.assertIsInstance(review, models.ReviewRating)
self.assertEqual(review.rating, 3.0)
self.assertEqual(review.privacy, "unlisted")
import_item.refresh_from_db()
self.assertEqual(import_item.linked_review.id, review.id)
def test_handle_imported_book_reviews_disabled(self, *_):
"""review import"""
import_job = self.importer.create_job(
self.local_user, self.csv, False, "unlisted"
)
import_item = import_job.items.filter(index=3).first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
self.assertFalse(
models.Review.objects.filter(book=self.book, user=self.local_user).exists()
)

View file

@ -1,16 +1,14 @@
""" testing import """ """ testing import """
import csv
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
import datetime import datetime
import pytz import pytz
from django.test import TestCase from django.test import TestCase
import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.importers import LibrarythingImporter from bookwyrm.importers import LibrarythingImporter
from bookwyrm.importers.importer import import_data, handle_imported_book from bookwyrm.importers.importer import handle_imported_book
def make_date(*args): def make_date(*args):
@ -35,7 +33,7 @@ class LibrarythingImport(TestCase):
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay" "bookwyrm.activitystreams.populate_stream_task.delay"
): ):
self.user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mmai", "mmai@mmai.mmai", "password", local=True "mmai", "mmai@mmai.mmai", "password", local=True
) )
work = models.Work.objects.create(title="Test Work") work = models.Work.objects.create(title="Test Work")
@ -47,8 +45,10 @@ class LibrarythingImport(TestCase):
def test_create_job(self, *_): def test_create_job(self, *_):
"""creates the import job entry and checks csv""" """creates the import job entry and checks csv"""
import_job = self.importer.create_job(self.user, self.csv, False, "public") import_job = self.importer.create_job(
self.assertEqual(import_job.user, self.user) self.local_user, self.csv, False, "public"
)
self.assertEqual(import_job.user, self.local_user)
self.assertEqual(import_job.include_reviews, False) self.assertEqual(import_job.include_reviews, False)
self.assertEqual(import_job.privacy, "public") self.assertEqual(import_job.privacy, "public")
@ -56,6 +56,14 @@ class LibrarythingImport(TestCase):
self.assertEqual(len(import_items), 3) self.assertEqual(len(import_items), 3)
self.assertEqual(import_items[0].index, 0) self.assertEqual(import_items[0].index, 0)
self.assertEqual(import_items[0].data["Book Id"], "5498194") self.assertEqual(import_items[0].data["Book Id"], "5498194")
self.assertEqual(import_items[0].normalized_data["isbn_13"], "9782070291342")
self.assertEqual(import_items[0].normalized_data["isbn_10"], "2070291340")
self.assertEqual(import_items[0].normalized_data["title"], "Marelle")
self.assertEqual(import_items[0].normalized_data["authors"], "Cortazar, Julio")
self.assertEqual(import_items[0].normalized_data["date_added"], "2006-08-09")
self.assertEqual(import_items[0].normalized_data["date_started"], "2007-04-16")
self.assertEqual(import_items[0].normalized_data["date_finished"], "2007-05-08")
self.assertEqual(import_items[1].index, 1) self.assertEqual(import_items[1].index, 1)
self.assertEqual(import_items[1].data["Book Id"], "5015319") self.assertEqual(import_items[1].data["Book Id"], "5015319")
self.assertEqual(import_items[2].index, 2) self.assertEqual(import_items[2].index, 2)
@ -63,12 +71,16 @@ class LibrarythingImport(TestCase):
def test_create_retry_job(self, *_): def test_create_retry_job(self, *_):
"""trying again with items that didn't import""" """trying again with items that didn't import"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_job = self.importer.create_job(
self.local_user, self.csv, False, "unlisted"
)
import_items = models.ImportItem.objects.filter(job=import_job).all()[:2] import_items = models.ImportItem.objects.filter(job=import_job).all()[:2]
retry = self.importer.create_retry_job(self.user, import_job, import_items) retry = self.importer.create_retry_job(
self.local_user, import_job, import_items
)
self.assertNotEqual(import_job, retry) self.assertNotEqual(import_job, retry)
self.assertEqual(retry.user, self.user) self.assertEqual(retry.user, self.local_user)
self.assertEqual(retry.include_reviews, False) self.assertEqual(retry.include_reviews, False)
self.assertEqual(retry.privacy, "unlisted") self.assertEqual(retry.privacy, "unlisted")
@ -79,111 +91,54 @@ class LibrarythingImport(TestCase):
self.assertEqual(retry_items[1].index, 1) self.assertEqual(retry_items[1].index, 1)
self.assertEqual(retry_items[1].data["Book Id"], "5015319") self.assertEqual(retry_items[1].data["Book Id"], "5015319")
@responses.activate
def test_import_data(self, *_):
"""resolve entry"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
book = models.Edition.objects.create(title="Test Book")
with patch(
"bookwyrm.models.import_job.ImportItem.get_book_from_isbn"
) as resolve:
resolve.return_value = book
with patch("bookwyrm.importers.importer.handle_imported_book"):
import_data(self.importer.service, import_job.id)
import_item = models.ImportItem.objects.get(job=import_job, index=0)
self.assertEqual(import_item.book.id, book.id)
def test_handle_imported_book(self, *_): def test_handle_imported_book(self, *_):
"""librarything import added a book, this adds related connections""" """librarything import added a book, this adds related connections"""
shelf = self.user.shelf_set.filter(identifier="read").first() shelf = self.local_user.shelf_set.filter(identifier="read").first()
self.assertIsNone(shelf.books.first()) self.assertIsNone(shelf.books.first())
import_job = models.ImportJob.objects.create(user=self.user) import_job = self.importer.create_job(
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") self.local_user, self.csv, False, "public"
csv_file = open(datafile, "r", encoding=self.importer.encoding)
for index, entry in enumerate(
list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))
):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book
) )
break import_item = import_job.items.first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book( handle_imported_book(import_item)
self.importer.service, self.user, import_item, False, "public"
)
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
readthrough = models.ReadThrough.objects.get(user=self.user) readthrough = models.ReadThrough.objects.get(user=self.local_user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date, make_date(2007, 4, 16)) self.assertEqual(readthrough.start_date, make_date(2007, 4, 16))
self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8)) self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8))
def test_handle_imported_book_already_shelved(self, *_): def test_handle_imported_book_already_shelved(self, *_):
"""librarything import added a book, this adds related connections""" """librarything import added a book, this adds related connections"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
shelf = self.user.shelf_set.filter(identifier="to-read").first() shelf = self.local_user.shelf_set.filter(identifier="to-read").first()
models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book) models.ShelfBook.objects.create(
shelf=shelf, user=self.local_user, book=self.book
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
csv_file = open(datafile, "r", encoding=self.importer.encoding)
for index, entry in enumerate(
list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))
):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book
) )
break
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): import_job = self.importer.create_job(
handle_imported_book( self.local_user, self.csv, False, "public"
self.importer.service, self.user, import_item, False, "public"
) )
import_item = import_job.items.first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
self.assertIsNone(self.user.shelf_set.get(identifier="read").books.first()) self.assertIsNone(
self.local_user.shelf_set.get(identifier="read").books.first()
readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date, make_date(2007, 4, 16))
self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8))
def test_handle_import_twice(self, *_):
"""re-importing books"""
shelf = self.user.shelf_set.filter(identifier="read").first()
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
csv_file = open(datafile, "r", encoding=self.importer.encoding)
for index, entry in enumerate(
list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))
):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book
)
break
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book(
self.importer.service, self.user, import_item, False, "public"
)
handle_imported_book(
self.importer.service, self.user, import_item, False, "public"
) )
shelf.refresh_from_db() readthrough = models.ReadThrough.objects.get(user=self.local_user)
self.assertEqual(shelf.books.first(), self.book)
readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date, make_date(2007, 4, 16)) self.assertEqual(readthrough.start_date, make_date(2007, 4, 16))
self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8)) self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8))
@ -191,40 +146,18 @@ class LibrarythingImport(TestCase):
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_handle_imported_book_review(self, *_): def test_handle_imported_book_review(self, *_):
"""librarything review import""" """librarything review import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = self.importer.create_job(
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") self.local_user, self.csv, True, "unlisted"
csv_file = open(datafile, "r", encoding=self.importer.encoding)
entry = list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))[0]
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book
) )
import_item = import_job.items.filter(index=0).first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book( handle_imported_book(import_item)
self.importer.service, self.user, import_item, True, "unlisted"
) review = models.Review.objects.get(book=self.book, user=self.local_user)
review = models.Review.objects.get(book=self.book, user=self.user)
self.assertEqual(review.content, "chef d'oeuvre") self.assertEqual(review.content, "chef d'oeuvre")
self.assertEqual(review.rating, 5) self.assertEqual(review.rating, 4.5)
self.assertEqual(review.published_date, make_date(2007, 5, 8)) self.assertEqual(review.published_date, make_date(2007, 5, 8))
self.assertEqual(review.privacy, "unlisted") self.assertEqual(review.privacy, "unlisted")
def test_handle_imported_book_reviews_disabled(self, *_):
"""librarything review import"""
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
csv_file = open(datafile, "r", encoding=self.importer.encoding)
entry = list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))[2]
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book(
self.importer.service, self.user, import_item, False, "unlisted"
)
self.assertFalse(
models.Review.objects.filter(book=self.book, user=self.user).exists()
)

View file

@ -0,0 +1,99 @@
""" testing import """
import pathlib
from unittest.mock import patch
import datetime
import pytz
from django.test import TestCase
from bookwyrm import models
from bookwyrm.importers import StorygraphImporter
from bookwyrm.importers.importer import handle_imported_book
def make_date(*args):
"""helper function to easily generate a date obj"""
return datetime.datetime(*args, tzinfo=pytz.UTC)
# pylint: disable=consider-using-with
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
class StorygraphImport(TestCase):
"""importing from storygraph csv"""
def setUp(self):
"""use a test csv"""
self.importer = StorygraphImporter()
datafile = pathlib.Path(__file__).parent.joinpath("../data/storygraph.csv")
self.csv = open(datafile, "r", encoding=self.importer.encoding)
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True
)
work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=work,
)
def test_create_job(self, *_):
"""creates the import job entry and checks csv"""
import_job = self.importer.create_job(
self.local_user, self.csv, False, "public"
)
import_items = models.ImportItem.objects.filter(job=import_job).all()
self.assertEqual(len(import_items), 2)
self.assertEqual(import_items[0].index, 0)
self.assertEqual(import_items[0].normalized_data["title"], "Always Coming Home")
self.assertEqual(import_items[1].index, 1)
self.assertEqual(
import_items[1].normalized_data["title"], "Subprime Attention Crisis"
)
self.assertEqual(import_items[1].normalized_data["rating"], "5.0")
def test_handle_imported_book(self, *_):
"""storygraph import added a book, this adds related connections"""
shelf = self.local_user.shelf_set.filter(identifier="to-read").first()
self.assertIsNone(shelf.books.first())
import_job = self.importer.create_job(
self.local_user, self.csv, False, "public"
)
import_item = import_job.items.first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
self.assertEqual(
shelf.shelfbook_set.first().shelved_date, make_date(2021, 5, 10)
)
@patch("bookwyrm.activitystreams.add_status_task.delay")
def test_handle_imported_book_rating(self, *_):
"""storygraph rating import"""
import_job = self.importer.create_job(
self.local_user, self.csv, True, "unlisted"
)
import_item = import_job.items.filter(index=1).first()
import_item.book = self.book
import_item.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
handle_imported_book(import_item)
review = models.ReviewRating.objects.get(book=self.book, user=self.local_user)
self.assertIsInstance(review, models.ReviewRating)
self.assertEqual(review.rating, 5.0)
self.assertEqual(review.published_date, make_date(2021, 5, 10))
self.assertEqual(review.privacy, "unlisted")

View file

@ -6,7 +6,7 @@ from bookwyrm import models
from bookwyrm.management.commands.populate_streams import populate_streams from bookwyrm.management.commands.populate_streams import populate_streams
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
class Activitystreams(TestCase): class Activitystreams(TestCase):
"""using redis to build activity streams""" """using redis to build activity streams"""

View file

@ -21,7 +21,7 @@ from bookwyrm.settings import PAGE_LENGTH
# pylint: disable=invalid-name # pylint: disable=invalid-name
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
class ActivitypubMixins(TestCase): class ActivitypubMixins(TestCase):
"""functionality shared across models""" """functionality shared across models"""

View file

@ -22,6 +22,7 @@ from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm.models import fields, User, Status from bookwyrm.models import fields, User, Status
from bookwyrm.models.base_model import BookWyrmModel from bookwyrm.models.base_model import BookWyrmModel
from bookwyrm.models.activitypub_mixin import ActivitypubMixin from bookwyrm.models.activitypub_mixin import ActivitypubMixin
from bookwyrm.settings import DOMAIN
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@ -424,21 +425,18 @@ class ModelFields(TestCase):
image.save(output, format=image.format) image.save(output, format=image.format)
user.avatar.save("test.jpg", ContentFile(output.getvalue())) user.avatar.save("test.jpg", ContentFile(output.getvalue()))
output = fields.image_serializer(user.avatar, alt="alt text") instance = fields.ImageField()
output = instance.field_to_activity(user.avatar)
self.assertIsNotNone( self.assertIsNotNone(
re.match( re.match(
r".*\.jpg", fr"https:\/\/{DOMAIN}\/.*\.jpg",
output.url, output.url,
) )
) )
self.assertEqual(output.name, "alt text") self.assertEqual(output.name, "")
self.assertEqual(output.type, "Document") self.assertEqual(output.type, "Document")
instance = fields.ImageField()
output = fields.image_serializer(user.avatar, alt=None)
self.assertEqual(instance.field_to_activity(user.avatar), output)
responses.add( responses.add(
responses.GET, responses.GET,
"http://www.example.com/image.jpg", "http://www.example.com/image.jpg",
@ -449,15 +447,6 @@ class ModelFields(TestCase):
self.assertIsInstance(loaded_image, list) self.assertIsInstance(loaded_image, list)
self.assertIsInstance(loaded_image[1], ContentFile) self.assertIsInstance(loaded_image[1], ContentFile)
def test_image_serialize(self, *_):
"""make sure we're creating sensible image paths"""
ValueMock = namedtuple("ValueMock", ("url"))
value_mock = ValueMock("/images/fish.jpg")
result = fields.image_serializer(value_mock, "hello")
self.assertEqual(result.type, "Document")
self.assertEqual(result.url, "https://your.domain.here/images/fish.jpg")
self.assertEqual(result.name, "hello")
def test_datetime_field(self, *_): def test_datetime_field(self, *_):
"""this one is pretty simple, it just has to use isoformat""" """this one is pretty simple, it just has to use isoformat"""
instance = fields.DateTimeField() instance = fields.DateTimeField()

View file

@ -5,7 +5,7 @@ from django.test import TestCase
from bookwyrm import models, settings from bookwyrm import models, settings
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
class Group(TestCase): class Group(TestCase):
"""some activitypub oddness ahead""" """some activitypub oddness ahead"""
@ -87,7 +87,7 @@ class Group(TestCase):
def test_group_members_can_see_followers_only_lists(self, _): def test_group_members_can_see_followers_only_lists(self, _):
"""follower-only group booklists should not be excluded from group booklist listing for group members who do not follower list owner""" """follower-only group booklists should not be excluded from group booklist listing for group members who do not follower list owner"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
followers_list = models.List.objects.create( followers_list = models.List.objects.create(
name="Followers List", name="Followers List",
curation="group", curation="group",
@ -107,7 +107,7 @@ class Group(TestCase):
def test_group_members_can_see_private_lists(self, _): def test_group_members_can_see_private_lists(self, _):
"""private group booklists should not be excluded from group booklist listing for group members""" """private group booklists should not be excluded from group booklist listing for group members"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
private_list = models.List.objects.create( private_list = models.List.objects.create(
name="Private List", name="Private List",

View file

@ -18,83 +18,68 @@ class ImportJob(TestCase):
def setUp(self): def setUp(self):
"""data is from a goodreads export of The Raven Tower""" """data is from a goodreads export of The Raven Tower"""
read_data = {
"Book Id": 39395857,
"Title": "The Raven Tower",
"Author": "Ann Leckie",
"Author l-f": "Leckie, Ann",
"Additional Authors": "",
"ISBN": '="0356506991"',
"ISBN13": '="9780356506999"',
"My Rating": 0,
"Average Rating": 4.06,
"Publisher": "Orbit",
"Binding": "Hardcover",
"Number of Pages": 416,
"Year Published": 2019,
"Original Publication Year": 2019,
"Date Read": "2019/04/12",
"Date Added": "2019/04/09",
"Bookshelves": "",
"Bookshelves with positions": "",
"Exclusive Shelf": "read",
"My Review": "",
"Spoiler": "",
"Private Notes": "",
"Read Count": 1,
"Recommended For": "",
"Recommended By": "",
"Owned Copies": 0,
"Original Purchase Date": "",
"Original Purchase Location": "",
"Condition": "",
"Condition Description": "",
"BCID": "",
}
currently_reading_data = read_data.copy()
currently_reading_data["Exclusive Shelf"] = "currently-reading"
currently_reading_data["Date Read"] = ""
unknown_read_data = currently_reading_data.copy()
unknown_read_data["Exclusive Shelf"] = "read"
unknown_read_data["Date Read"] = ""
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay" "bookwyrm.activitystreams.populate_stream_task.delay"
): ):
user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "password", local=True
)
job = models.ImportJob.objects.create(user=user)
self.item_1 = models.ImportItem.objects.create(
job=job, index=1, data=currently_reading_data
)
self.item_2 = models.ImportItem.objects.create(job=job, index=2, data=read_data)
self.item_3 = models.ImportItem.objects.create(
job=job, index=3, data=unknown_read_data
) )
self.job = models.ImportJob.objects.create(user=self.local_user, mappings={})
def test_isbn(self): def test_isbn(self):
"""it unquotes the isbn13 field from data""" """it unquotes the isbn13 field from data"""
expected = "9780356506999" item = models.ImportItem.objects.create(
item = models.ImportItem.objects.get(index=1) index=1,
self.assertEqual(item.isbn, expected) job=self.job,
data={},
normalized_data={
"isbn_13": '="9780356506999"',
},
)
self.assertEqual(item.isbn, "9780356506999")
def test_shelf(self): def test_shelf(self):
"""converts to the local shelf typology""" """converts to the local shelf typology"""
expected = "reading" item = models.ImportItem.objects.create(
self.assertEqual(self.item_1.shelf, expected) index=1,
job=self.job,
data={},
normalized_data={
"isbn_13": '="9780356506999"',
"shelf": "reading",
},
)
self.assertEqual(item.shelf, "reading")
def test_date_added(self): def test_date_added(self):
"""converts to the local shelf typology""" """converts to the local shelf typology"""
expected = datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc) expected = datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc)
item = models.ImportItem.objects.get(index=1) item = models.ImportItem.objects.create(
index=1,
job=self.job,
data={},
normalized_data={
"isbn_13": '="9780356506999"',
"shelf": "reading",
"date_added": "2019/04/09",
},
)
self.assertEqual(item.date_added, expected) self.assertEqual(item.date_added, expected)
def test_date_read(self): def test_date_read(self):
"""converts to the local shelf typology""" """converts to the local shelf typology"""
expected = datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc) expected = datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc)
item = models.ImportItem.objects.get(index=2) item = models.ImportItem.objects.create(
index=1,
job=self.job,
data={},
normalized_data={
"isbn_13": '="9780356506999"',
"shelf": "reading",
"date_added": "2019/04/09",
"date_finished": "2019/04/12",
},
)
self.assertEqual(item.date_read, expected) self.assertEqual(item.date_read, expected)
def test_currently_reading_reads(self): def test_currently_reading_reads(self):
@ -104,31 +89,66 @@ class ImportJob(TestCase):
start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc) start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc)
) )
] ]
actual = models.ImportItem.objects.get(index=1) item = models.ImportItem.objects.create(
self.assertEqual(actual.reads[0].start_date, expected[0].start_date) index=1,
self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date) job=self.job,
data={},
normalized_data={
"isbn_13": '="9780356506999"',
"shelf": "reading",
"date_added": "2019/04/09",
},
)
self.assertEqual(item.reads[0].start_date, expected[0].start_date)
self.assertIsNone(item.reads[0].finish_date)
def test_read_reads(self): def test_read_reads(self):
"""infer read dates where available""" """infer read dates where available"""
actual = self.item_2 item = models.ImportItem.objects.create(
index=1,
job=self.job,
data={},
normalized_data={
"isbn_13": '="9780356506999"',
"shelf": "reading",
"date_added": "2019/04/09",
"date_finished": "2019/04/12",
},
)
self.assertEqual( self.assertEqual(
actual.reads[0].start_date, item.reads[0].start_date,
datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc), datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc),
) )
self.assertEqual( self.assertEqual(
actual.reads[0].finish_date, item.reads[0].finish_date,
datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc), datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc),
) )
def test_unread_reads(self): def test_unread_reads(self):
"""handle books with no read dates""" """handle books with no read dates"""
expected = [] expected = []
actual = models.ImportItem.objects.get(index=3) item = models.ImportItem.objects.create(
self.assertEqual(actual.reads, expected) index=1,
job=self.job,
data={},
normalized_data={
"isbn_13": '="9780356506999"',
"shelf": "reading",
},
)
self.assertEqual(item.reads, expected)
@responses.activate @responses.activate
def test_get_book_from_isbn(self): def test_get_book_from_isbn(self):
"""search and load books by isbn (9780356506999)""" """search and load books by isbn (9780356506999)"""
item = models.ImportItem.objects.create(
index=1,
job=self.job,
data={},
normalized_data={
"isbn_13": '="9780356506999"',
},
)
connector_info = models.Connector.objects.create( connector_info = models.Connector.objects.create(
identifier="openlibrary.org", identifier="openlibrary.org",
name="OpenLibrary", name="OpenLibrary",
@ -177,6 +197,6 @@ class ImportJob(TestCase):
with patch( with patch(
"bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data" "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data"
): ):
book = self.item_1.get_book_from_isbn() book = item.get_book_from_isbn()
self.assertEqual(book.title, "Sabriel") self.assertEqual(book.title, "Sabriel")

View file

@ -5,7 +5,7 @@ from django.test import TestCase
from bookwyrm import models, settings from bookwyrm import models, settings
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
class List(TestCase): class List(TestCase):
"""some activitypub oddness ahead""" """some activitypub oddness ahead"""
@ -22,7 +22,7 @@ class List(TestCase):
def test_remote_id(self, _): def test_remote_id(self, _):
"""shelves use custom remote ids""" """shelves use custom remote ids"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
book_list = models.List.objects.create( book_list = models.List.objects.create(
name="Test List", user=self.local_user name="Test List", user=self.local_user
) )
@ -31,7 +31,7 @@ class List(TestCase):
def test_to_activity(self, _): def test_to_activity(self, _):
"""jsonify it""" """jsonify it"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
book_list = models.List.objects.create( book_list = models.List.objects.create(
name="Test List", user=self.local_user name="Test List", user=self.local_user
) )
@ -45,7 +45,7 @@ class List(TestCase):
def test_list_item(self, _): def test_list_item(self, _):
"""a list entry""" """a list entry"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
book_list = models.List.objects.create( book_list = models.List.objects.create(
name="Test List", user=self.local_user, privacy="unlisted" name="Test List", user=self.local_user, privacy="unlisted"
) )
@ -63,7 +63,7 @@ class List(TestCase):
def test_list_item_pending(self, _): def test_list_item_pending(self, _):
"""a list entry""" """a list entry"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
book_list = models.List.objects.create( book_list = models.List.objects.create(
name="Test List", user=self.local_user name="Test List", user=self.local_user
) )

View file

@ -33,11 +33,13 @@ class Relationship(TestCase):
def test_user_follows_from_request(self, _): def test_user_follows_from_request(self, _):
"""convert a follow request into a follow""" """convert a follow request into a follow"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
request = models.UserFollowRequest.objects.create( request = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user user_subject=self.local_user, user_object=self.remote_user
) )
activity = json.loads(mock.call_args[0][1]) activity = json.loads(mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Follow") self.assertEqual(activity["type"], "Follow")
self.assertEqual( self.assertEqual(
request.remote_id, "http://local.com/user/mouse#follows/%d" % request.id request.remote_id, "http://local.com/user/mouse#follows/%d" % request.id
@ -54,7 +56,7 @@ class Relationship(TestCase):
def test_user_follows_from_request_custom_remote_id(self, _): def test_user_follows_from_request_custom_remote_id(self, _):
"""store a specific remote id for a relationship provided by remote""" """store a specific remote id for a relationship provided by remote"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
request = models.UserFollowRequest.objects.create( request = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_subject=self.local_user,
user_object=self.remote_user, user_object=self.remote_user,
@ -69,19 +71,19 @@ class Relationship(TestCase):
self.assertEqual(rel.user_subject, self.local_user) self.assertEqual(rel.user_subject, self.local_user)
self.assertEqual(rel.user_object, self.remote_user) self.assertEqual(rel.user_object, self.remote_user)
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_follow_request_activity(self, broadcast_mock, _): def test_follow_request_activity(self, broadcast_mock, _):
"""accept a request and make it a relationship""" """accept a request and make it a relationship"""
models.UserFollowRequest.objects.create( models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_subject=self.local_user,
user_object=self.remote_user, user_object=self.remote_user,
) )
activity = json.loads(broadcast_mock.call_args[0][1]) activity = json.loads(broadcast_mock.call_args[1]["args"][1])
self.assertEqual(activity["actor"], self.local_user.remote_id) self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"], self.remote_user.remote_id) self.assertEqual(activity["object"], self.remote_user.remote_id)
self.assertEqual(activity["type"], "Follow") self.assertEqual(activity["type"], "Follow")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_follow_request_accept(self, broadcast_mock, _): def test_follow_request_accept(self, broadcast_mock, _):
"""accept a request and make it a relationship""" """accept a request and make it a relationship"""
self.local_user.manually_approves_followers = True self.local_user.manually_approves_followers = True
@ -96,7 +98,7 @@ class Relationship(TestCase):
) )
request.accept() request.accept()
activity = json.loads(broadcast_mock.call_args[0][1]) activity = json.loads(broadcast_mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Accept") self.assertEqual(activity["type"], "Accept")
self.assertEqual(activity["actor"], self.local_user.remote_id) self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["id"], "https://www.hi.com/") self.assertEqual(activity["object"]["id"], "https://www.hi.com/")
@ -107,7 +109,7 @@ class Relationship(TestCase):
self.assertEqual(rel.user_subject, self.remote_user) self.assertEqual(rel.user_subject, self.remote_user)
self.assertEqual(rel.user_object, self.local_user) self.assertEqual(rel.user_object, self.local_user)
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_follow_request_reject(self, broadcast_mock, _): def test_follow_request_reject(self, broadcast_mock, _):
"""accept a request and make it a relationship""" """accept a request and make it a relationship"""
self.local_user.manually_approves_followers = True self.local_user.manually_approves_followers = True
@ -120,7 +122,7 @@ class Relationship(TestCase):
) )
request.reject() request.reject()
activity = json.loads(broadcast_mock.call_args[0][1]) activity = json.loads(broadcast_mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Reject") self.assertEqual(activity["type"], "Reject")
self.assertEqual(activity["actor"], self.local_user.remote_id) self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["id"], request.remote_id) self.assertEqual(activity["object"]["id"], request.remote_id)

View file

@ -27,7 +27,7 @@ class Shelf(TestCase):
def test_remote_id(self, *_): def test_remote_id(self, *_):
"""shelves use custom remote ids""" """shelves use custom remote ids"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user name="Test Shelf", identifier="test-shelf", user=self.local_user
) )
@ -36,7 +36,7 @@ class Shelf(TestCase):
def test_to_activity(self, *_): def test_to_activity(self, *_):
"""jsonify it""" """jsonify it"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user name="Test Shelf", identifier="test-shelf", user=self.local_user
) )
@ -51,19 +51,23 @@ class Shelf(TestCase):
def test_create_update_shelf(self, *_): def test_create_update_shelf(self, *_):
"""create and broadcast shelf creation""" """create and broadcast shelf creation"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user name="Test Shelf", identifier="test-shelf", user=self.local_user
) )
activity = json.loads(mock.call_args[0][1]) activity = json.loads(mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Create") self.assertEqual(activity["type"], "Create")
self.assertEqual(activity["actor"], self.local_user.remote_id) self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["name"], "Test Shelf") self.assertEqual(activity["object"]["name"], "Test Shelf")
shelf.name = "arthur russel" shelf.name = "arthur russel"
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
shelf.save() shelf.save()
activity = json.loads(mock.call_args[0][1]) activity = json.loads(mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Update") self.assertEqual(activity["type"], "Update")
self.assertEqual(activity["actor"], self.local_user.remote_id) self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["name"], "arthur russel") self.assertEqual(activity["object"]["name"], "arthur russel")
@ -71,27 +75,31 @@ class Shelf(TestCase):
def test_shelve(self, *_): def test_shelve(self, *_):
"""create and broadcast shelf creation""" """create and broadcast shelf creation"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user name="Test Shelf", identifier="test-shelf", user=self.local_user
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
shelf_book = models.ShelfBook.objects.create( shelf_book = models.ShelfBook.objects.create(
shelf=shelf, user=self.local_user, book=self.book shelf=shelf, user=self.local_user, book=self.book
) )
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1]) activity = json.loads(mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Add") self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.local_user.remote_id) self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["id"], shelf_book.remote_id) self.assertEqual(activity["object"]["id"], shelf_book.remote_id)
self.assertEqual(activity["target"], shelf.remote_id) self.assertEqual(activity["target"], shelf.remote_id)
self.assertEqual(shelf.books.first(), self.book) self.assertEqual(shelf.books.first(), self.book)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
shelf_book.delete() shelf_book.delete()
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1]) activity = json.loads(mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Remove") self.assertEqual(activity["type"], "Remove")
self.assertEqual(activity["actor"], self.local_user.remote_id) self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["id"], shelf_book.remote_id) self.assertEqual(activity["object"]["id"], shelf_book.remote_id)

View file

@ -2,6 +2,7 @@
from unittest.mock import patch from unittest.mock import patch
from io import BytesIO from io import BytesIO
import pathlib import pathlib
import re
from django.http import Http404 from django.http import Http404
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
@ -190,9 +191,11 @@ class Status(TestCase):
self.assertEqual(activity["sensitive"], False) self.assertEqual(activity["sensitive"], False)
self.assertIsInstance(activity["attachment"], list) self.assertIsInstance(activity["attachment"], list)
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url, activity["attachment"][0].url,
f"https://{settings.DOMAIN}{self.book.cover.url}", )
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -220,9 +223,11 @@ class Status(TestCase):
f'test content<p>(comment on <a href="{self.book.remote_id}">"Test Edition"</a>)</p>', f'test content<p>(comment on <a href="{self.book.remote_id}">"Test Edition"</a>)</p>',
) )
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url, activity["attachment"][0].url,
f"https://{settings.DOMAIN}{self.book.cover.url}", )
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -257,9 +262,11 @@ class Status(TestCase):
f'a sickening sense <p>-- <a href="{self.book.remote_id}">"Test Edition"</a></p>test content', f'a sickening sense <p>-- <a href="{self.book.remote_id}">"Test Edition"</a></p>test content',
) )
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url, activity["attachment"][0].url,
f"https://{settings.DOMAIN}{self.book.cover.url}", )
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -298,9 +305,11 @@ class Status(TestCase):
) )
self.assertEqual(activity["content"], "test content") self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url, activity["attachment"][0].url,
f"https://{settings.DOMAIN}{self.book.cover.url}", )
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -320,9 +329,11 @@ class Status(TestCase):
) )
self.assertEqual(activity["content"], "test content") self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url, activity["attachment"][0].url,
f"https://{settings.DOMAIN}{self.book.cover.url}", )
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -341,27 +352,25 @@ class Status(TestCase):
f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars', f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars',
) )
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url, activity["attachment"][0].url,
f"https://{settings.DOMAIN}{self.book.cover.url}", )
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_favorite(self, *_): def test_favorite(self, *_):
"""fav a status""" """fav a status"""
real_broadcast = models.Favorite.broadcast
def fav_broadcast_mock(_, activity, user):
"""ok"""
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity["type"], "Like")
models.Favorite.broadcast = fav_broadcast_mock
status = models.Status.objects.create( status = models.Status.objects.create(
content="test content", user=self.local_user content="test content", user=self.local_user
) )
with patch("bookwyrm.models.Favorite.broadcast") as mock:
fav = models.Favorite.objects.create(status=status, user=self.local_user) fav = models.Favorite.objects.create(status=status, user=self.local_user)
args = mock.call_args[0]
self.assertEqual(args[1].remote_id, self.local_user.remote_id)
self.assertEqual(args[0]["type"], "Like")
# can't fav a status twice # can't fav a status twice
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
@ -371,7 +380,6 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Like") self.assertEqual(activity["type"], "Like")
self.assertEqual(activity["actor"], self.local_user.remote_id) self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"], status.remote_id) self.assertEqual(activity["object"], status.remote_id)
models.Favorite.broadcast = real_broadcast
def test_boost(self, *_): def test_boost(self, *_):
"""boosting, this one's a bit fussy""" """boosting, this one's a bit fussy"""

View file

@ -165,12 +165,12 @@ class User(TestCase):
"""deactivate a user""" """deactivate a user"""
self.assertTrue(self.user.is_active) self.assertTrue(self.user.is_active)
with patch( with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as broadcast_mock: ) as broadcast_mock:
self.user.delete() self.user.delete()
self.assertEqual(broadcast_mock.call_count, 1) self.assertEqual(broadcast_mock.call_count, 1)
activity = json.loads(broadcast_mock.call_args[0][1]) activity = json.loads(broadcast_mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Delete") self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["object"], self.user.remote_id) self.assertEqual(activity["object"], self.user.remote_id)
self.assertFalse(self.user.is_active) self.assertFalse(self.user.is_active)

View file

@ -57,12 +57,24 @@ class BookSearch(TestCase):
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertEqual(results[0], self.second_edition) self.assertEqual(results[0], self.second_edition)
def test_search_identifiers_return_first(self):
"""search by unique identifiers"""
result = book_search.search_identifiers("hello", return_first=True)
self.assertEqual(result, self.second_edition)
def test_search_title_author(self): def test_search_title_author(self):
"""search by unique identifiers""" """search by unique identifiers"""
results = book_search.search_title_author("Another", min_confidence=0) results = book_search.search_title_author("Another", min_confidence=0)
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertEqual(results[0], self.second_edition) self.assertEqual(results[0], self.second_edition)
def test_search_title_author_return_first(self):
"""search by unique identifiers"""
results = book_search.search_title_author(
"Another", min_confidence=0, return_first=True
)
self.assertEqual(results, self.second_edition)
def test_format_search_result(self): def test_format_search_result(self):
"""format a search result""" """format a search result"""
result = book_search.format_search_result(self.first_edition) result = book_search.format_search_result(self.first_edition)

View file

@ -5,7 +5,7 @@ from django.test import TestCase
from bookwyrm import models from bookwyrm import models
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
class PostgresTriggers(TestCase): class PostgresTriggers(TestCase):
"""special migrations, fancy stuff ya know""" """special migrations, fancy stuff ya know"""

View file

@ -9,7 +9,7 @@ from bookwyrm import models
from bookwyrm.suggested_users import suggested_users, get_annotated_users from bookwyrm.suggested_users import suggested_users, get_annotated_users
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.activitystreams.populate_stream_task.delay") @patch("bookwyrm.activitystreams.populate_stream_task.delay")
@ -168,7 +168,7 @@ class SuggestedUsers(TestCase):
remote_id="https://example.com/book/1", remote_id="https://example.com/book/1",
parent_work=work, parent_work=work,
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
# 1 shared follow # 1 shared follow
self.local_user.following.add(user_2) self.local_user.following.add(user_2)
user_1.followers.add(user_2) user_1.followers.add(user_2)
@ -213,7 +213,7 @@ class SuggestedUsers(TestCase):
user.following.add(user_1) user.following.add(user_1)
user.followers.add(self.local_user) user.followers.add(self.local_user)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
for i in range(3): for i in range(3):
book = models.Edition.objects.create( book = models.Edition.objects.create(
title=i, title=i,

View file

@ -44,7 +44,7 @@ class TemplateTags(TestCase):
def test_get_user_rating(self, *_): def test_get_user_rating(self, *_):
"""get a user's most recent rating of a book""" """get a user's most recent rating of a book"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
models.Review.objects.create(user=self.user, book=self.book, rating=3) models.Review.objects.create(user=self.user, book=self.book, rating=3)
self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 3) self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 3)
@ -63,7 +63,7 @@ class TemplateTags(TestCase):
utilities.get_user_identifier(self.remote_user), "rat@example.com" utilities.get_user_identifier(self.remote_user), "rat@example.com"
) )
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_get_replies(self, *_): def test_get_replies(self, *_):
"""direct replies to a status""" """direct replies to a status"""
parent = models.Review.objects.create( parent = models.Review.objects.create(
@ -90,7 +90,7 @@ class TemplateTags(TestCase):
def test_get_parent(self, *_): def test_get_parent(self, *_):
"""get the reply parent of a status""" """get the reply parent of a status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
parent = models.Review.objects.create( parent = models.Review.objects.create(
user=self.user, book=self.book, content="hi" user=self.user, book=self.book, content="hi"
) )
@ -107,7 +107,7 @@ class TemplateTags(TestCase):
status = models.Review.objects.create(user=self.remote_user, book=self.book) status = models.Review.objects.create(user=self.remote_user, book=self.book)
self.assertFalse(interaction.get_user_liked(self.user, status)) self.assertFalse(interaction.get_user_liked(self.user, status))
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
models.Favorite.objects.create(user=self.user, status=status) models.Favorite.objects.create(user=self.user, status=status)
self.assertTrue(interaction.get_user_liked(self.user, status)) self.assertTrue(interaction.get_user_liked(self.user, status))
@ -116,13 +116,13 @@ class TemplateTags(TestCase):
status = models.Review.objects.create(user=self.remote_user, book=self.book) status = models.Review.objects.create(user=self.remote_user, book=self.book)
self.assertFalse(interaction.get_user_boosted(self.user, status)) self.assertFalse(interaction.get_user_boosted(self.user, status))
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
models.Boost.objects.create(user=self.user, boosted_status=status) models.Boost.objects.create(user=self.user, boosted_status=status)
self.assertTrue(interaction.get_user_boosted(self.user, status)) self.assertTrue(interaction.get_user_boosted(self.user, status))
def test_get_boosted(self, *_): def test_get_boosted(self, *_):
"""load a boosted status""" """load a boosted status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
status = models.Review.objects.create(user=self.remote_user, book=self.book) status = models.Review.objects.create(user=self.remote_user, book=self.book)
boost = models.Boost.objects.create(user=self.user, boosted_status=status) boost = models.Boost.objects.create(user=self.user, boosted_status=status)
boosted = status_display.get_boosted(boost) boosted = status_display.get_boosted(boost)
@ -166,7 +166,7 @@ class TemplateTags(TestCase):
def test_related_status(self, *_): def test_related_status(self, *_):
"""gets the subclass model for a notification status""" """gets the subclass model for a notification status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
status = models.Status.objects.create(content="hi", user=self.user) status = models.Status.objects.create(content="hi", user=self.user)
notification = models.Notification.objects.create( notification = models.Notification.objects.create(
user=self.user, notification_type="MENTION", related_status=status user=self.user, notification_type="MENTION", related_status=status

View file

@ -151,10 +151,12 @@ class ReportViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
# de-activate # de-activate
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
views.moderator_delete_user(request, self.rat.id) views.moderator_delete_user(request, self.rat.id)
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1]) activity = json.loads(mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Delete") self.assertEqual(activity["type"], "Delete")
self.rat.refresh_from_db() self.rat.refresh_from_db()

View file

@ -67,7 +67,7 @@ class UserAdminViews(TestCase):
request.user = self.local_user request.user = self.local_user
request.user.is_superuser = True request.user.is_superuser = True
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
result = view(request, self.local_user.id) result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)

View file

@ -78,7 +78,7 @@ class BookViews(TestCase):
self.assertIsInstance(result, ActivitypubResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_book_page_statuses(self, *_): def test_book_page_statuses(self, *_):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
@ -169,7 +169,7 @@ class BookViews(TestCase):
request.user = self.local_user request.user = self.local_user
with patch( with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as delay_mock: ) as delay_mock:
views.upload_cover(request, self.book.id) views.upload_cover(request, self.book.id)
self.assertEqual(delay_mock.call_count, 1) self.assertEqual(delay_mock.call_count, 1)
@ -188,7 +188,7 @@ class BookViews(TestCase):
request.user = self.local_user request.user = self.local_user
with patch( with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as delay_mock: ) as delay_mock:
views.upload_cover(request, self.book.id) views.upload_cover(request, self.book.id)
self.assertEqual(delay_mock.call_count, 1) self.assertEqual(delay_mock.call_count, 1)
@ -202,7 +202,7 @@ class BookViews(TestCase):
request = self.factory.post("", {"description": "new description hi"}) request = self.factory.post("", {"description": "new description hi"})
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.add_description(request, self.book.id) views.add_description(request, self.book.id)
self.book.refresh_from_db() self.book.refresh_from_db()

View file

@ -79,7 +79,7 @@ class EditBookViews(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.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
view(request, self.book.id) view(request, self.book.id)
self.book.refresh_from_db() self.book.refresh_from_db()
@ -115,7 +115,7 @@ class EditBookViews(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.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
view(request, self.book.id) view(request, self.book.id)
self.book.refresh_from_db() self.book.refresh_from_db()
@ -136,7 +136,7 @@ class EditBookViews(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.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
view(request, self.book.id) view(request, self.book.id)
self.book.refresh_from_db() self.book.refresh_from_db()
self.assertEqual(self.book.title, "New Title") self.assertEqual(self.book.title, "New Title")
@ -207,7 +207,7 @@ class EditBookViews(TestCase):
request.user = self.local_user request.user = self.local_user
with patch( with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as delay_mock: ) as delay_mock:
views.upload_cover(request, self.book.id) views.upload_cover(request, self.book.id)
self.assertEqual(delay_mock.call_count, 1) self.assertEqual(delay_mock.call_count, 1)

View file

@ -111,7 +111,7 @@ class BookViews(TestCase):
work = models.Work.objects.create(title="test work") work = models.Work.objects.create(title="test work")
edition1 = models.Edition.objects.create(title="first ed", parent_work=work) edition1 = models.Edition.objects.create(title="first ed", parent_work=work)
edition2 = models.Edition.objects.create(title="second ed", parent_work=work) edition2 = models.Edition.objects.create(title="second ed", parent_work=work)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
shelf = models.Shelf.objects.create(name="Test Shelf", user=self.local_user) shelf = models.Shelf.objects.create(name="Test Shelf", user=self.local_user)
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=edition1, book=edition1,
@ -124,7 +124,7 @@ class BookViews(TestCase):
self.assertEqual(models.ReadThrough.objects.get().book, edition1) self.assertEqual(models.ReadThrough.objects.get().book, edition1)
request = self.factory.post("", {"edition": edition2.id}) request = self.factory.post("", {"edition": edition2.id})
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.switch_edition(request) views.switch_edition(request)
self.assertEqual(models.ShelfBook.objects.get().book, edition2) self.assertEqual(models.ShelfBook.objects.get().book, edition2)

View file

@ -0,0 +1 @@
from . import *

View file

@ -5,6 +5,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm.tests.validate_html import validate_html
from bookwyrm import forms, models, views from bookwyrm import forms, models, views
@ -34,32 +35,35 @@ class ImportViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_import_status(self): def test_import_status(self):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
view = views.ImportStatus.as_view() view = views.ImportStatus.as_view()
import_job = models.ImportJob.objects.create(user=self.local_user) import_job = models.ImportJob.objects.create(user=self.local_user, mappings={})
request = self.factory.get("") request = self.factory.get("")
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.tasks.app.AsyncResult") as async_result: with patch("bookwyrm.tasks.app.AsyncResult") as async_result:
async_result.return_value = [] async_result.return_value = []
result = view(request, import_job.id) result = view(request, import_job.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_start_import(self): def test_start_import(self):
"""retry failed items""" """retry failed items"""
view = views.Import.as_view() view = views.Import.as_view()
form = forms.ImportForm() form = forms.ImportForm()
form.data["source"] = "LibraryThing" form.data["source"] = "Goodreads"
form.data["privacy"] = "public" form.data["privacy"] = "public"
form.data["include_reviews"] = False form.data["include_reviews"] = False
csv_file = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") csv_file = pathlib.Path(__file__).parent.joinpath("../../data/goodreads.csv")
form.data["csv_file"] = SimpleUploadedFile( form.data["csv_file"] = SimpleUploadedFile(
csv_file, open(csv_file, "rb").read(), content_type="text/csv" # pylint: disable=consider-using-with
csv_file,
open(csv_file, "rb").read(),
content_type="text/csv",
) )
request = self.factory.post("", form.data) request = self.factory.post("", form.data)
@ -70,22 +74,3 @@ class ImportViews(TestCase):
job = models.ImportJob.objects.get() job = models.ImportJob.objects.get()
self.assertFalse(job.include_reviews) self.assertFalse(job.include_reviews)
self.assertEqual(job.privacy, "public") self.assertEqual(job.privacy, "public")
def test_retry_import(self):
"""retry failed items"""
view = views.ImportStatus.as_view()
import_job = models.ImportJob.objects.create(
user=self.local_user, privacy="unlisted"
)
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.importers.Importer.start_import"):
view(request, import_job.id)
self.assertEqual(models.ImportJob.objects.count(), 2)
retry_job = models.ImportJob.objects.last()
self.assertTrue(retry_job.retry)
self.assertEqual(retry_job.user, self.local_user)
self.assertEqual(retry_job.privacy, "unlisted")

View file

@ -0,0 +1,87 @@
""" test for app action functionality """
from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm.tests.validate_html import validate_html
from bookwyrm import models, views
class ImportManualReviewViews(TestCase):
"""goodreads import views"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.mouse",
"password",
local=True,
localname="mouse",
)
models.SiteSettings.objects.create()
self.job = models.ImportJob.objects.create(user=self.local_user, mappings={})
work = models.Work.objects.create(title="Test Work")
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
parent_work=work,
)
def test_import_troubleshoot_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.ImportManualReview.as_view()
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.tasks.app.AsyncResult") as async_result:
async_result.return_value = []
result = view(request, self.job.id)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_approve_item(self):
"""a guess is correct"""
import_item = models.ImportItem.objects.create(
index=0,
job=self.job,
book_guess=self.book,
fail_reason="no match",
data={},
normalized_data={},
)
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.importers.importer.import_item_task.delay") as mock:
views.approve_import_item(request, self.job.id, import_item.id)
self.assertEqual(mock.call_count, 1)
import_item.refresh_from_db()
self.assertIsNone(import_item.fail_reason)
self.assertIsNone(import_item.book_guess)
self.assertEqual(import_item.book.id, self.book.id)
def test_delete_item(self):
"""a guess is correct"""
import_item = models.ImportItem.objects.create(
index=0,
job=self.job,
book_guess=self.book,
fail_reason="no match",
data={},
normalized_data={},
)
request = self.factory.post("")
request.user = self.local_user
views.delete_import_item(request, self.job.id, import_item.id)
import_item.refresh_from_db()
self.assertEqual(import_item.fail_reason, "no match")
self.assertIsNone(import_item.book_guess)
self.assertIsNone(import_item.book)

View file

@ -0,0 +1,59 @@
""" test for app action functionality """
from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm.tests.validate_html import validate_html
from bookwyrm import models, views
class ImportTroubleshootViews(TestCase):
"""goodreads import views"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.mouse",
"password",
local=True,
localname="mouse",
)
models.SiteSettings.objects.create()
def test_import_troubleshoot_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.ImportTroubleshoot.as_view()
import_job = models.ImportJob.objects.create(user=self.local_user, mappings={})
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.tasks.app.AsyncResult") as async_result:
async_result.return_value = []
result = view(request, import_job.id)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_retry_import(self):
"""retry failed items"""
view = views.ImportTroubleshoot.as_view()
import_job = models.ImportJob.objects.create(
user=self.local_user, privacy="unlisted", mappings={}
)
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.importers.Importer.start_import"):
view(request, import_job.id)
self.assertEqual(models.ImportJob.objects.count(), 2)
retry_job = models.ImportJob.objects.last()
self.assertTrue(retry_job.retry)
self.assertEqual(retry_job.user, self.local_user)
self.assertEqual(retry_job.privacy, "unlisted")

View file

@ -36,7 +36,7 @@ class InboxActivities(TestCase):
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
with patch("bookwyrm.activitystreams.add_status_task.delay"): with patch("bookwyrm.activitystreams.add_status_task.delay"):
self.status = models.Status.objects.create( self.status = models.Status.objects.create(
user=self.local_user, user=self.local_user,

View file

@ -40,7 +40,7 @@ class InboxBlock(TestCase):
def test_handle_blocks(self): def test_handle_blocks(self):
"""create a "block" database entry from an activity""" """create a "block" database entry from an activity"""
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
models.UserFollowRequest.objects.create( models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user user_subject=self.local_user, user_object=self.remote_user
) )

View file

@ -10,7 +10,7 @@ from bookwyrm.activitypub import ActivitySerializerError
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay") @patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
class InboxCreate(TestCase): class InboxCreate(TestCase):
"""readthrough tests""" """readthrough tests"""

View file

@ -49,10 +49,12 @@ class InboxRelationships(TestCase):
} }
self.assertFalse(models.UserFollowRequest.objects.exists()) self.assertFalse(models.UserFollowRequest.objects.exists())
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)
response_activity = json.loads(mock.call_args[0][1]) response_activity = json.loads(mock.call_args[1]["args"][1])
self.assertEqual(response_activity["type"], "Accept") self.assertEqual(response_activity["type"], "Accept")
# notification created # notification created
@ -77,17 +79,19 @@ class InboxRelationships(TestCase):
"object": "https://example.com/user/mouse", "object": "https://example.com/user/mouse",
} }
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
# the follow relationship should exist # the follow relationship should exist
follow = models.UserFollows.objects.get(user_object=self.local_user) follow = models.UserFollows.objects.get(user_object=self.local_user)
self.assertEqual(follow.user_subject, self.remote_user) self.assertEqual(follow.user_subject, self.remote_user)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)
response_activity = json.loads(mock.call_args[0][1]) response_activity = json.loads(mock.call_args[1]["args"][1])
self.assertEqual(response_activity["type"], "Accept") self.assertEqual(response_activity["type"], "Accept")
# the follow relationship should STILL exist # the follow relationship should STILL exist
@ -109,7 +113,7 @@ class InboxRelationships(TestCase):
broadcast=False, update_fields=["manually_approves_followers"] broadcast=False, update_fields=["manually_approves_followers"]
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
# notification created # notification created
@ -132,7 +136,7 @@ class InboxRelationships(TestCase):
self.local_user.save( self.local_user.save(
broadcast=False, update_fields=["manually_approves_followers"] broadcast=False, update_fields=["manually_approves_followers"]
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
request = models.UserFollowRequest.objects.create( request = models.UserFollowRequest.objects.create(
user_subject=self.remote_user, user_object=self.local_user user_subject=self.remote_user, user_object=self.local_user
) )
@ -160,7 +164,7 @@ class InboxRelationships(TestCase):
def test_unfollow(self): def test_unfollow(self):
"""remove a relationship""" """remove a relationship"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
rel = models.UserFollows.objects.create( rel = models.UserFollows.objects.create(
user_subject=self.remote_user, user_object=self.local_user user_subject=self.remote_user, user_object=self.local_user
) )
@ -186,7 +190,7 @@ class InboxRelationships(TestCase):
@patch("bookwyrm.activitystreams.add_user_statuses_task.delay") @patch("bookwyrm.activitystreams.add_user_statuses_task.delay")
def test_follow_accept(self, _): def test_follow_accept(self, _):
"""a remote user approved a follow request from local""" """a remote user approved a follow request from local"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
rel = models.UserFollowRequest.objects.create( rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user user_subject=self.local_user, user_object=self.remote_user
) )
@ -217,7 +221,7 @@ class InboxRelationships(TestCase):
def test_follow_reject(self): def test_follow_reject(self):
"""turn down a follow request""" """turn down a follow request"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
rel = models.UserFollowRequest.objects.create( rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user user_subject=self.local_user, user_object=self.remote_user
) )

View file

@ -35,7 +35,7 @@ class InboxActivities(TestCase):
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
with patch("bookwyrm.activitystreams.add_status_task.delay"): with patch("bookwyrm.activitystreams.add_status_task.delay"):
self.status = models.Status.objects.create( self.status = models.Status.objects.create(
user=self.local_user, user=self.local_user,

View file

@ -75,7 +75,7 @@ class InboxRemove(TestCase):
def test_handle_remove_book_from_list(self): def test_handle_remove_book_from_list(self):
"""listing a book""" """listing a book"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
booklist = models.List.objects.create( booklist = models.List.objects.create(
name="test list", name="test list",
user=self.local_user, user=self.local_user,

View file

@ -50,7 +50,7 @@ class InboxUpdate(TestCase):
def test_update_list(self): def test_update_list(self):
"""a new list""" """a new list"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
book_list = models.List.objects.create( book_list = models.List.objects.create(
name="hi", remote_id="https://example.com/list/22", user=self.local_user name="hi", remote_id="https://example.com/list/22", user=self.local_user
) )
@ -171,7 +171,7 @@ class InboxUpdate(TestCase):
book = models.Work.objects.get(id=book.id) book = models.Work.objects.get(id=book.id)
self.assertEqual(book.title, "Piranesi") self.assertEqual(book.title, "Piranesi")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_update_status(self, *_): def test_update_status(self, *_):
"""edit a status""" """edit a status"""

View file

@ -9,7 +9,7 @@ from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
class BlockViews(TestCase): class BlockViews(TestCase):
"""view user and edit profile""" """view user and edit profile"""

View file

@ -35,9 +35,9 @@ class DeleteUserViews(TestCase):
self.book = models.Edition.objects.create( self.book = models.Edition.objects.create(
title="test", parent_work=models.Work.objects.create(title="test work") title="test", parent_work=models.Work.objects.create(title="test work")
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"), patch( with patch(
"bookwyrm.activitystreams.add_book_statuses_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
): ), patch("bookwyrm.activitystreams.add_book_statuses_task.delay"):
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=self.book, book=self.book,
user=self.local_user, user=self.local_user,
@ -70,11 +70,11 @@ class DeleteUserViews(TestCase):
self.assertIsNone(self.local_user.name) self.assertIsNone(self.local_user.name)
with patch( with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as delay_mock: ) as delay_mock:
view(request) view(request)
self.assertEqual(delay_mock.call_count, 1) self.assertEqual(delay_mock.call_count, 1)
activity = json.loads(delay_mock.call_args[0][1]) activity = json.loads(delay_mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Delete") self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["actor"], self.local_user.remote_id) self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual( self.assertEqual(

View file

@ -38,9 +38,9 @@ class EditUserViews(TestCase):
self.book = models.Edition.objects.create( self.book = models.Edition.objects.create(
title="test", parent_work=models.Work.objects.create(title="test work") title="test", parent_work=models.Work.objects.create(title="test work")
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"), patch( with patch(
"bookwyrm.activitystreams.add_book_statuses_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
): ), patch("bookwyrm.activitystreams.add_book_statuses_task.delay"):
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=self.book, book=self.book,
user=self.local_user, user=self.local_user,
@ -74,7 +74,7 @@ class EditUserViews(TestCase):
self.assertIsNone(self.local_user.name) self.assertIsNone(self.local_user.name)
with patch( with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as delay_mock: ) as delay_mock:
view(request) view(request)
self.assertEqual(delay_mock.call_count, 1) self.assertEqual(delay_mock.call_count, 1)
@ -100,7 +100,7 @@ class EditUserViews(TestCase):
request.user = self.local_user request.user = self.local_user
with patch( with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as delay_mock: ) as delay_mock:
view(request) view(request)
self.assertEqual(delay_mock.call_count, 1) self.assertEqual(delay_mock.call_count, 1)

View file

@ -11,7 +11,7 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.tests.validate_html import validate_html from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.activitystreams.populate_stream_task.delay") @patch("bookwyrm.activitystreams.populate_stream_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay") @patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
@ -39,7 +39,7 @@ class ShelfViews(TestCase):
remote_id="https://example.com/book/1", remote_id="https://example.com/book/1",
parent_work=self.work, parent_work=self.work,
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
self.shelf = models.Shelf.objects.create( self.shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user name="Test Shelf", identifier="test-shelf", user=self.local_user
) )
@ -142,7 +142,7 @@ class ShelfViews(TestCase):
"", {"privacy": "public", "user": self.local_user.id, "name": "cool name"} "", {"privacy": "public", "user": self.local_user.id, "name": "cool name"}
) )
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
view(request, request.user.username, shelf.identifier) view(request, request.user.username, shelf.identifier)
shelf.refresh_from_db() shelf.refresh_from_db()
@ -159,7 +159,7 @@ class ShelfViews(TestCase):
"", {"privacy": "public", "user": self.local_user.id, "name": "cool name"} "", {"privacy": "public", "user": self.local_user.id, "name": "cool name"}
) )
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
view(request, request.user.username, shelf.identifier) view(request, request.user.username, shelf.identifier)
self.assertEqual(shelf.name, "To Read") self.assertEqual(shelf.name, "To Read")

View file

@ -9,7 +9,7 @@ from django.test.client import RequestFactory
from bookwyrm import forms, models, views from bookwyrm import forms, models, views
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.activitystreams.populate_stream_task.delay") @patch("bookwyrm.activitystreams.populate_stream_task.delay")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay") @patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
@ -37,7 +37,7 @@ class ShelfActionViews(TestCase):
remote_id="https://example.com/book/1", remote_id="https://example.com/book/1",
parent_work=self.work, parent_work=self.work,
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
self.shelf = models.Shelf.objects.create( self.shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user name="Test Shelf", identifier="test-shelf", user=self.local_user
) )
@ -49,11 +49,13 @@ class ShelfActionViews(TestCase):
"", {"book": self.book.id, "shelf": self.shelf.identifier} "", {"book": self.book.id, "shelf": self.shelf.identifier}
) )
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
views.shelve(request) views.shelve(request)
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args[0][1]) activity = json.loads(mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Add") self.assertEqual(activity["type"], "Add")
item = models.ShelfBook.objects.get() item = models.ShelfBook.objects.get()
@ -69,7 +71,7 @@ class ShelfActionViews(TestCase):
) )
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.shelve(request) views.shelve(request)
# make sure the book is on the shelf # make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book) self.assertEqual(shelf.books.get(), self.book)
@ -82,7 +84,7 @@ class ShelfActionViews(TestCase):
) )
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.shelve(request) views.shelve(request)
# make sure the book is on the shelf # make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book) self.assertEqual(shelf.books.get(), self.book)
@ -95,7 +97,7 @@ class ShelfActionViews(TestCase):
) )
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.shelve(request) views.shelve(request)
# make sure the book is on the shelf # make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book) self.assertEqual(shelf.books.get(), self.book)
@ -118,7 +120,7 @@ class ShelfActionViews(TestCase):
) )
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.shelve(request) views.shelve(request)
# make sure the book is on the shelf # make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book) self.assertEqual(shelf.books.get(), self.book)
@ -126,7 +128,7 @@ class ShelfActionViews(TestCase):
def test_unshelve(self, *_): def test_unshelve(self, *_):
"""remove a book from a shelf""" """remove a book from a shelf"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=self.book, user=self.local_user, shelf=self.shelf book=self.book, user=self.local_user, shelf=self.shelf
) )
@ -136,9 +138,11 @@ class ShelfActionViews(TestCase):
self.assertEqual(self.shelf.books.count(), 1) self.assertEqual(self.shelf.books.count(), 1)
request = self.factory.post("", {"book": self.book.id, "shelf": self.shelf.id}) request = self.factory.post("", {"book": self.book.id, "shelf": self.shelf.id})
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
views.unshelve(request) views.unshelve(request)
activity = json.loads(mock.call_args[0][1]) activity = json.loads(mock.call_args[1]["args"][1])
self.assertEqual(activity["type"], "Remove") self.assertEqual(activity["type"], "Remove")
self.assertEqual(activity["object"]["id"], item.remote_id) self.assertEqual(activity["object"]["id"], item.remote_id)
self.assertEqual(self.shelf.books.count(), 0) self.assertEqual(self.shelf.books.count(), 0)
@ -192,7 +196,7 @@ class ShelfActionViews(TestCase):
def test_delete_shelf_has_book(self, *_): def test_delete_shelf_has_book(self, *_):
"""delete a brand new custom shelf""" """delete a brand new custom shelf"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=self.book, user=self.local_user, shelf=self.shelf book=self.book, user=self.local_user, shelf=self.shelf
) )

View file

@ -111,7 +111,7 @@ class AuthorViews(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.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
view(request, author.id) view(request, author.id)
author.refresh_from_db() author.refresh_from_db()
self.assertEqual(author.name, "New Name") self.assertEqual(author.name, "New Name")

View file

@ -41,7 +41,7 @@ class DiscoverViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
result.render() result.render()
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_discover_page(self, *_): def test_discover_page(self, *_):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""

View file

@ -57,7 +57,7 @@ class FeedViews(TestCase):
def test_status_page(self, *_): def test_status_page(self, *_):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
view = views.Status.as_view() view = views.Status.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
status = models.Status.objects.create(content="hi", user=self.local_user) status = models.Status.objects.create(content="hi", user=self.local_user)
request = self.factory.get("") request = self.factory.get("")
request.user = self.local_user request.user = self.local_user
@ -95,7 +95,7 @@ class FeedViews(TestCase):
local=True, local=True,
localname="rat", localname="rat",
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
status = models.Status.objects.create(content="hi", user=another_user) status = models.Status.objects.create(content="hi", user=another_user)
request = self.factory.get("") request = self.factory.get("")
@ -115,7 +115,7 @@ class FeedViews(TestCase):
image = Image.open(image_file) image = Image.open(image_file)
output = BytesIO() output = BytesIO()
image.save(output, format=image.format) image.save(output, format=image.format)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
status = models.Review.objects.create( status = models.Review.objects.create(
content="hi", content="hi",
user=self.local_user, user=self.local_user,
@ -144,7 +144,7 @@ class FeedViews(TestCase):
def test_replies_page(self, *_): def test_replies_page(self, *_):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
view = views.Replies.as_view() view = views.Replies.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
status = models.Status.objects.create(content="hi", user=self.local_user) status = models.Status.objects.create(content="hi", user=self.local_user)
request = self.factory.get("") request = self.factory.get("")
request.user = self.local_user request.user = self.local_user
@ -171,7 +171,7 @@ class FeedViews(TestCase):
result.render() result.render()
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay") @patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
def test_get_suggested_book(self, *_): def test_get_suggested_book(self, *_):
"""gets books the ~*~ algorithm ~*~ thinks you want to post about""" """gets books the ~*~ algorithm ~*~ thinks you want to post about"""

View file

@ -59,7 +59,7 @@ class FollowViews(TestCase):
request.user = self.local_user request.user = self.local_user
self.assertEqual(models.UserFollowRequest.objects.count(), 0) self.assertEqual(models.UserFollowRequest.objects.count(), 0)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.follow(request) views.follow(request)
rel = models.UserFollowRequest.objects.get() rel = models.UserFollowRequest.objects.get()
@ -86,7 +86,7 @@ class FollowViews(TestCase):
request.user = self.local_user request.user = self.local_user
self.assertEqual(models.UserFollowRequest.objects.count(), 0) self.assertEqual(models.UserFollowRequest.objects.count(), 0)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.follow(request) views.follow(request)
rel = models.UserFollowRequest.objects.get() rel = models.UserFollowRequest.objects.get()
@ -111,7 +111,7 @@ class FollowViews(TestCase):
request.user = self.local_user request.user = self.local_user
self.assertEqual(models.UserFollowRequest.objects.count(), 0) self.assertEqual(models.UserFollowRequest.objects.count(), 0)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.follow(request) views.follow(request)
rel = models.UserFollows.objects.get() rel = models.UserFollows.objects.get()
@ -127,10 +127,12 @@ class FollowViews(TestCase):
request.user = self.local_user request.user = self.local_user
self.remote_user.followers.add(self.local_user) self.remote_user.followers.add(self.local_user)
self.assertEqual(self.remote_user.followers.count(), 1) self.assertEqual(self.remote_user.followers.count(), 1)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as mock:
views.unfollow(request) views.unfollow(request)
self.assertEqual(mock.call_count, 1) self.assertEqual(mock.call_count, 1)
activity = json.loads(mock.call_args_list[0][0][1]) activity = json.loads(mock.call_args_list[0][1]["args"][1])
self.assertEqual(activity["type"], "Undo") self.assertEqual(activity["type"], "Undo")
self.assertEqual(self.remote_user.followers.count(), 0) self.assertEqual(self.remote_user.followers.count(), 0)
@ -147,7 +149,7 @@ class FollowViews(TestCase):
user_subject=self.remote_user, user_object=self.local_user user_subject=self.remote_user, user_object=self.local_user
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.accept_follow_request(request) views.accept_follow_request(request)
# request should be deleted # request should be deleted
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0) self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
@ -166,7 +168,7 @@ class FollowViews(TestCase):
user_subject=self.remote_user, user_object=self.local_user user_subject=self.remote_user, user_object=self.local_user
) )
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.delete_follow_request(request) views.delete_follow_request(request)
# request should be deleted # request should be deleted
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0) self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)

View file

@ -56,7 +56,7 @@ class GetStartedViews(TestCase):
self.assertIsNone(self.local_user.name) self.assertIsNone(self.local_user.name)
with patch( with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as delay_mock: ) as delay_mock:
view(request) view(request)
self.assertEqual(delay_mock.call_count, 1) self.assertEqual(delay_mock.call_count, 1)
@ -98,7 +98,7 @@ class GetStartedViews(TestCase):
self.assertFalse(self.local_user.shelfbook_set.exists()) self.assertFalse(self.local_user.shelfbook_set.exists())
with patch( with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay" "bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as delay_mock: ) as delay_mock:
view(request) view(request)
self.assertEqual(delay_mock.call_count, 1) self.assertEqual(delay_mock.call_count, 1)

View file

@ -123,7 +123,7 @@ class GoalViews(TestCase):
}, },
) )
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
view(request, self.local_user.localname, self.year) view(request, self.local_user.localname, self.year)
goal = models.AnnualGoal.objects.get() goal = models.AnnualGoal.objects.get()

View file

@ -10,7 +10,7 @@ from bookwyrm import models, views, forms
from bookwyrm.tests.validate_html import validate_html from bookwyrm.tests.validate_html import validate_html
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
class GroupViews(TestCase): class GroupViews(TestCase):
"""view group and edit details""" """view group and edit details"""

View file

@ -55,7 +55,7 @@ class ViewsHelpers(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
self.userdata = json.loads(datafile.read_bytes()) self.userdata = json.loads(datafile.read_bytes())
del self.userdata["icon"] del self.userdata["icon"]
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
self.shelf = models.Shelf.objects.create( self.shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user name="Test Shelf", identifier="test-shelf", user=self.local_user
) )
@ -166,7 +166,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_to_read(self, *_): def test_handle_reading_status_to_read(self, *_):
"""posts shelve activities""" """posts shelve activities"""
shelf = self.local_user.shelf_set.get(identifier="to-read") shelf = self.local_user.shelf_set.get(identifier="to-read")
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.helpers.handle_reading_status( views.helpers.handle_reading_status(
self.local_user, shelf, self.book, "public" self.local_user, shelf, self.book, "public"
) )
@ -178,7 +178,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_reading(self, *_): def test_handle_reading_status_reading(self, *_):
"""posts shelve activities""" """posts shelve activities"""
shelf = self.local_user.shelf_set.get(identifier="reading") shelf = self.local_user.shelf_set.get(identifier="reading")
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.helpers.handle_reading_status( views.helpers.handle_reading_status(
self.local_user, shelf, self.book, "public" self.local_user, shelf, self.book, "public"
) )
@ -190,7 +190,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_read(self, *_): def test_handle_reading_status_read(self, *_):
"""posts shelve activities""" """posts shelve activities"""
shelf = self.local_user.shelf_set.get(identifier="read") shelf = self.local_user.shelf_set.get(identifier="read")
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.helpers.handle_reading_status( views.helpers.handle_reading_status(
self.local_user, shelf, self.book, "public" self.local_user, shelf, self.book, "public"
) )
@ -201,7 +201,7 @@ class ViewsHelpers(TestCase):
def test_handle_reading_status_other(self, *_): def test_handle_reading_status_other(self, *_):
"""posts shelve activities""" """posts shelve activities"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
views.helpers.handle_reading_status( views.helpers.handle_reading_status(
self.local_user, self.shelf, self.book, "public" self.local_user, self.shelf, self.book, "public"
) )

Some files were not shown because too many files have changed in this diff Show more