Merge branch 'main' into book-format-choices

This commit is contained in:
Mouse Reeve 2021-08-04 14:13:55 -07:00
commit fd0f9324d3
166 changed files with 7987 additions and 4136 deletions

View file

@ -3,6 +3,7 @@ SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG=true DEBUG=true
USE_HTTPS=false
DOMAIN=your.domain.here DOMAIN=your.domain.here
#EMAIL=your@email.here #EMAIL=your@email.here
@ -42,6 +43,21 @@ EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true EMAIL_USE_TLS=true
EMAIL_USE_SSL=false EMAIL_USE_SSL=false
# S3 configuration
USE_S3=false
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
# Commented are example values if you use a non-AWS, S3-compatible service
# AWS S3 should work with only AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME
# non-AWS S3-compatible services will need AWS_STORAGE_BUCKET_NAME,
# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL
# AWS_STORAGE_BUCKET_NAME= # "example-bucket-name"
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud"
# AWS_S3_REGION_NAME=None # "fr-par"
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
# Preview image generation can be computing and storage intensive # Preview image generation can be computing and storage intensive
# ENABLE_PREVIEW_IMAGES=True # ENABLE_PREVIEW_IMAGES=True

View file

@ -3,6 +3,7 @@ SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG=false DEBUG=false
USE_HTTPS=true
DOMAIN=your.domain.here DOMAIN=your.domain.here
EMAIL=your@email.here EMAIL=your@email.here
@ -42,6 +43,21 @@ EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true EMAIL_USE_TLS=true
EMAIL_USE_SSL=false EMAIL_USE_SSL=false
# S3 configuration
USE_S3=false
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
# Commented are example values if you use a non-AWS, S3-compatible service
# AWS S3 should work with only AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME
# non-AWS S3-compatible services will need AWS_STORAGE_BUCKET_NAME,
# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL
# AWS_STORAGE_BUCKET_NAME= # "example-bucket-name"
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud"
# AWS_S3_REGION_NAME=None # "fr-par"
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
# Preview image generation can be computing and storage intensive # Preview image generation can be computing and storage intensive
# ENABLE_PREVIEW_IMAGES=True # ENABLE_PREVIEW_IMAGES=True

View file

@ -2,7 +2,7 @@
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: '' labels: 'bug'
assignees: '' assignees: ''
--- ---
@ -23,6 +23,14 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Instance**
On which BookWyrm instance did you encounter this problem.
**Additional context**
Add any other context about the problem here.
---
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. iOS] - OS: [e.g. iOS]
- Browser [e.g. chrome, safari] - Browser [e.g. chrome, safari]
@ -33,6 +41,3 @@ If applicable, add screenshots to help explain your problem.
- OS: [e.g. iOS8.1] - OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari] - Browser [e.g. stock browser, safari]
- Version [e.g. 22] - Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -1,6 +1,10 @@
name: Python Formatting (run ./bw-dev black to fix) name: Python Formatting (run ./bw-dev black to fix)
on: [push, pull_request] on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs: jobs:
lint: lint:

View file

@ -9,18 +9,9 @@ jobs:
build: build:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy:
max-parallel: 4
matrix:
db: [postgres]
python-version: [3.9]
include:
- db: postgres
db_port: 5432
services: services:
postgres: postgres:
image: postgres:10 image: postgres:13
env: env:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: hunter2 POSTGRES_PASSWORD: hunter2
@ -33,22 +24,18 @@ jobs:
- 5432:5432 - 5432:5432
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: 3.9
- name: Install Dependencies - name: Install Dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
- name: Run Tests - name: Run Tests
env: env:
DB: ${{ matrix.db }}
DB_HOST: 127.0.0.1
DB_PORT: ${{ matrix.db_port }}
DB_PASSWORD: hunter2
SECRET_KEY: beepbeep SECRET_KEY: beepbeep
DEBUG: true DEBUG: false
DOMAIN: your.domain.here DOMAIN: your.domain.here
BOOKWYRM_DATABASE_BACKEND: postgres BOOKWYRM_DATABASE_BACKEND: postgres
MEDIA_ROOT: images/ MEDIA_ROOT: images/
@ -64,6 +51,6 @@ jobs:
EMAIL_HOST_USER: "" EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: "" EMAIL_HOST_PASSWORD: ""
EMAIL_USE_TLS: true EMAIL_USE_TLS: true
ENABLE_PREVIEW_IMAGES: true ENABLE_PREVIEW_IMAGES: false
run: | run: |
python manage.py test pytest -n 3

View file

@ -1,6 +1,10 @@
name: Pylint name: Pylint
on: [push, pull_request] on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs: jobs:
build: build:

View file

@ -106,6 +106,7 @@ class ActivityObject:
value = field.default value = field.default
setattr(self, field.name, value) setattr(self, field.name, value)
# pylint: disable=too-many-locals,too-many-branches
def to_model(self, model=None, instance=None, allow_create=True, save=True): def to_model(self, model=None, instance=None, allow_create=True, save=True):
"""convert from an activity to a model instance""" """convert from an activity to a model instance"""
model = model or get_model_from_type(self.type) model = model or get_model_from_type(self.type)
@ -126,27 +127,36 @@ class ActivityObject:
return None return None
instance = instance or model() instance = instance or model()
# keep track of what we've changed
update_fields = []
for field in instance.simple_fields: for field in instance.simple_fields:
try: try:
field.set_field_from_activity(instance, self) changed = field.set_field_from_activity(instance, self)
if changed:
update_fields.append(field.name)
except AttributeError as e: except AttributeError as e:
raise ActivitySerializerError(e) raise ActivitySerializerError(e)
# image fields have to be set after other fields because they can save # image fields have to be set after other fields because they can save
# too early and jank up users # too early and jank up users
for field in instance.image_fields: for field in instance.image_fields:
field.set_field_from_activity(instance, self, save=save) changed = field.set_field_from_activity(instance, self, save=save)
if changed:
update_fields.append(field.name)
if not save: if not save:
return instance return instance
with transaction.atomic(): with transaction.atomic():
# can't force an update on fields unless the object already exists in the db
if not instance.id:
update_fields = None
# we can't set many to many and reverse fields on an unsaved object # we can't set many to many and reverse fields on an unsaved object
try: try:
try: try:
instance.save(broadcast=False) instance.save(broadcast=False, update_fields=update_fields)
except TypeError: except TypeError:
instance.save() instance.save(update_fields=update_fields)
except IntegrityError as e: except IntegrityError as e:
raise ActivitySerializerError(e) raise ActivitySerializerError(e)

View file

@ -2,11 +2,10 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
import logging import logging
from urllib3.exceptions import RequestError
from django.db import transaction from django.db import transaction
import requests import requests
from requests.exceptions import SSLError from requests.exceptions import RequestException
from bookwyrm import activitypub, models, settings from bookwyrm import activitypub, models, settings
from .connector_manager import load_more_data, ConnectorException from .connector_manager import load_more_data, ConnectorException
@ -237,7 +236,7 @@ def get_data(url, params=None, timeout=10):
}, },
timeout=timeout, timeout=timeout,
) )
except (RequestError, SSLError, ConnectionError) as err: except RequestException as err:
logger.exception(err) logger.exception(err)
raise ConnectorException() raise ConnectorException()
@ -262,7 +261,7 @@ def get_image(url, timeout=10):
}, },
timeout=timeout, timeout=timeout,
) )
except (RequestError, SSLError) as err: except RequestException as err:
logger.exception(err) logger.exception(err)
return None return None
if not resp.ok: if not resp.ok:

View file

@ -2,7 +2,7 @@
from functools import reduce from functools import reduce
import operator import operator
from django.contrib.postgres.search import SearchRank, SearchVector from django.contrib.postgres.search import SearchRank, SearchQuery
from django.db.models import OuterRef, Subquery, F, Q from django.db.models import OuterRef, Subquery, F, Q
from bookwyrm import models from bookwyrm import models
@ -13,7 +13,7 @@ class Connector(AbstractConnector):
"""instantiate a connector""" """instantiate a connector"""
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
def search(self, query, min_confidence=0.1, raw=False, filters=None): def search(self, query, min_confidence=0, raw=False, filters=None):
"""search your local database""" """search your local database"""
filters = filters or [] filters = filters or []
if not query: if not query:
@ -141,16 +141,11 @@ def search_identifiers(query, *filters):
def search_title_author(query, min_confidence, *filters): def search_title_author(query, min_confidence, *filters):
"""searches for title and author""" """searches for title and author"""
vector = ( query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
SearchVector("title", weight="A")
+ SearchVector("subtitle", weight="B")
+ SearchVector("authors__name", weight="C")
+ SearchVector("series", weight="D")
)
results = ( results = (
models.Edition.objects.annotate(rank=SearchRank(vector, query)) models.Edition.objects.filter(*filters, search_vector=query)
.filter(*filters, rank__gt=min_confidence) .annotate(rank=SearchRank(F("search_vector"), query))
.filter(rank__gt=min_confidence)
.order_by("-rank") .order_by("-rank")
) )

View file

@ -11,10 +11,7 @@ def site_settings(request): # pylint: disable=unused-argument
return { return {
"site": models.SiteSettings.objects.get(), "site": models.SiteSettings.objects.get(),
"active_announcements": models.Announcement.active_announcements(), "active_announcements": models.Announcement.active_announcements(),
"static_url": settings.STATIC_URL, "media_full_url": settings.MEDIA_FULL_URL,
"media_url": settings.MEDIA_URL,
"static_path": settings.STATIC_PATH,
"media_path": settings.MEDIA_PATH,
"preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES, "preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES,
"request_protocol": request_protocol, "request_protocol": request_protocol,
} }

View file

@ -132,6 +132,7 @@ class EditUserForm(CustomForm):
"summary", "summary",
"show_goal", "show_goal",
"manually_approves_followers", "manually_approves_followers",
"default_post_privacy",
"discoverable", "discoverable",
"preferred_timezone", "preferred_timezone",
] ]
@ -183,6 +184,7 @@ class EditionForm(CustomForm):
"parent_work", "parent_work",
"shelves", "shelves",
"connector", "connector",
"search_vector",
] ]
@ -194,6 +196,7 @@ class AuthorForm(CustomForm):
"origin_id", "origin_id",
"created_date", "created_date",
"updated_date", "updated_date",
"search_vector",
] ]

View file

@ -2,6 +2,8 @@
import csv import csv
import logging import logging
from django.utils import timezone
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
@ -100,7 +102,10 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
# shelve the book if it hasn't been shelved already # shelve the book if it hasn't been shelved already
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)
models.ShelfBook.objects.create(book=item.book, shelf=desired_shelf, user=user) shelved_date = item.date_added or timezone.now()
models.ShelfBook.objects.create(
book=item.book, shelf=desired_shelf, user=user, shelved_date=shelved_date
)
for read in item.reads: for read in item.reads:
# check for an existing readthrough with the same dates # check for an existing readthrough with the same dates

View file

@ -1,12 +1,6 @@
""" Re-create user streams """ """ Re-create user streams """
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
import redis from bookwyrm import activitystreams, models
from bookwyrm import activitystreams, models, settings
r = redis.Redis(
host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0
)
def populate_streams(): def populate_streams():

View file

@ -0,0 +1,25 @@
""" Populate suggested users """
from django.core.management.base import BaseCommand
from bookwyrm import models
from bookwyrm.suggested_users import rerank_suggestions_task
def populate_suggestions():
"""build all the streams for all the users"""
users = models.User.objects.filter(
local=True,
is_active=True,
).values_list("id", flat=True)
for user in users:
rerank_suggestions_task.delay(user)
class Command(BaseCommand):
"""start all over with user suggestions"""
help = "Populate suggested users for all users"
# pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options):
"""run builder"""
populate_suggestions()

View file

@ -0,0 +1,27 @@
# Generated by Django 3.0.7 on 2021-02-14 00:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0045_auto_20210210_2114"),
]
operations = [
migrations.AddField(
model_name="user",
name="default_post_privacy",
field=models.CharField(
choices=[
("public", "Public"),
("unlisted", "Unlisted"),
("followers", "Followers"),
("direct", "Direct"),
],
default="public",
max_length=255,
),
),
]

View file

@ -0,0 +1,126 @@
# Generated by Django 3.2.4 on 2021-06-23 21:55
import django.contrib.postgres.indexes
import django.contrib.postgres.search
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0076_preview_images"),
]
operations = [
migrations.AddField(
model_name="author",
name="search_vector",
field=django.contrib.postgres.search.SearchVectorField(null=True),
),
migrations.AddField(
model_name="book",
name="search_vector",
field=django.contrib.postgres.search.SearchVectorField(null=True),
),
migrations.AddIndex(
model_name="author",
index=django.contrib.postgres.indexes.GinIndex(
fields=["search_vector"], name="bookwyrm_au_search__b050a8_gin"
),
),
migrations.AddIndex(
model_name="book",
index=django.contrib.postgres.indexes.GinIndex(
fields=["search_vector"], name="bookwyrm_bo_search__51beb3_gin"
),
),
migrations.RunSQL(
sql="""
CREATE FUNCTION book_trigger() RETURNS trigger AS $$
begin
new.search_vector :=
coalesce(
NULLIF(setweight(to_tsvector('english', coalesce(new.title, '')), 'A'), ''),
setweight(to_tsvector('simple', coalesce(new.title, '')), 'A')
) ||
setweight(to_tsvector('english', coalesce(new.subtitle, '')), 'B') ||
(SELECT setweight(to_tsvector('simple', coalesce(array_to_string(array_agg(bookwyrm_author.name), ' '), '')), 'C')
FROM bookwyrm_book
LEFT OUTER JOIN bookwyrm_book_authors
ON bookwyrm_book.id = bookwyrm_book_authors.book_id
LEFT OUTER JOIN bookwyrm_author
ON bookwyrm_book_authors.author_id = bookwyrm_author.id
WHERE bookwyrm_book.id = new.id
) ||
setweight(to_tsvector('english', coalesce(new.series, '')), 'D');
return new;
end
$$ LANGUAGE plpgsql;
CREATE TRIGGER search_vector_trigger
BEFORE INSERT OR UPDATE OF title, subtitle, series, search_vector
ON bookwyrm_book
FOR EACH ROW EXECUTE FUNCTION book_trigger();
UPDATE bookwyrm_book SET search_vector = NULL;
""",
reverse_sql="""
DROP TRIGGER IF EXISTS search_vector_trigger
ON bookwyrm_book;
DROP FUNCTION IF EXISTS book_trigger;
""",
),
# when an author is edited
migrations.RunSQL(
sql="""
CREATE FUNCTION author_trigger() RETURNS trigger AS $$
begin
WITH book AS (
SELECT bookwyrm_book.id as row_id
FROM bookwyrm_author
LEFT OUTER JOIN bookwyrm_book_authors
ON bookwyrm_book_authors.id = new.id
LEFT OUTER JOIN bookwyrm_book
ON bookwyrm_book.id = bookwyrm_book_authors.book_id
)
UPDATE bookwyrm_book SET search_vector = ''
FROM book
WHERE id = book.row_id;
return new;
end
$$ LANGUAGE plpgsql;
CREATE TRIGGER author_search_vector_trigger
AFTER UPDATE OF name
ON bookwyrm_author
FOR EACH ROW EXECUTE FUNCTION author_trigger();
""",
reverse_sql="""
DROP TRIGGER IF EXISTS author_search_vector_trigger
ON bookwyrm_author;
DROP FUNCTION IF EXISTS author_trigger;
""",
),
# when an author is added to or removed from a book
migrations.RunSQL(
sql="""
CREATE FUNCTION book_authors_trigger() RETURNS trigger AS $$
begin
UPDATE bookwyrm_book SET search_vector = ''
WHERE id = coalesce(new.book_id, old.book_id);
return new;
end
$$ LANGUAGE plpgsql;
CREATE TRIGGER book_authors_search_vector_trigger
AFTER INSERT OR DELETE
ON bookwyrm_book_authors
FOR EACH ROW EXECUTE FUNCTION book_authors_trigger();
""",
reverse_sql="""
DROP TRIGGER IF EXISTS book_authors_search_vector_trigger
ON bookwyrm_book_authors;
DROP FUNCTION IF EXISTS book_authors_trigger;
""",
),
]

View file

@ -0,0 +1,34 @@
# Generated by Django 3.2.4 on 2021-07-03 08:25
from django.db import migrations, models
import django.utils.timezone
def copy_created_date(app_registry, schema_editor):
db_alias = schema_editor.connection.alias
ShelfBook = app_registry.get_model("bookwyrm", "ShelfBook")
ShelfBook.objects.all().update(shelved_date=models.F("created_date"))
def do_nothing(app_registry, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0077_auto_20210623_2155"),
]
operations = [
migrations.AlterModelOptions(
name="shelfbook",
options={"ordering": ("-shelved_date",)},
),
migrations.AddField(
model_name="shelfbook",
name="shelved_date",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.RunPython(copy_created_date, reverse_code=do_nothing),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.4 on 2021-08-04 17:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0046_user_default_post_privacy"),
("bookwyrm", "0078_add_shelved_date"),
]
operations = []

View file

@ -30,6 +30,7 @@ logger = logging.getLogger(__name__)
PropertyField = namedtuple("PropertyField", ("set_activity_from_field")) PropertyField = namedtuple("PropertyField", ("set_activity_from_field"))
# 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"""
activity[field[1]] = getattr(obj, field[0]) activity[field[1]] = getattr(obj, field[0])
@ -318,7 +319,9 @@ class OrderedCollectionPageMixin(ObjectMixin):
remote_id = remote_id or self.remote_id remote_id = remote_id or self.remote_id
if page: if page:
return to_ordered_collection_page(queryset, remote_id, **kwargs) if isinstance(page, list) and len(page) > 0:
page = page[0]
return to_ordered_collection_page(queryset, remote_id, page=page, **kwargs)
if collection_only or not hasattr(self, "activity_serializer"): if collection_only or not hasattr(self, "activity_serializer"):
serializer = activitypub.OrderedCollection serializer = activitypub.OrderedCollection

View file

@ -1,4 +1,5 @@
""" database schema for info about authors """ """ database schema for info about authors """
from django.contrib.postgres.indexes import GinIndex
from django.db import models from django.db import models
from bookwyrm import activitypub from bookwyrm import activitypub
@ -37,3 +38,8 @@ class Author(BookDataModel):
return "https://%s/author/%s" % (DOMAIN, self.id) return "https://%s/author/%s" % (DOMAIN, self.id)
activity_serializer = activitypub.Author activity_serializer = activitypub.Author
class Meta:
"""sets up postgres GIN index field"""
indexes = (GinIndex(fields=["search_vector"]),)

View file

@ -1,6 +1,8 @@
""" database schema for books and shelves """ """ database schema for books and shelves """
import re import re
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from model_utils import FieldTracker from model_utils import FieldTracker
@ -34,6 +36,7 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
bnf_id = fields.CharField( # Bibliothèque nationale de France bnf_id = fields.CharField( # Bibliothèque nationale de France
max_length=255, blank=True, null=True, deduplication_field=True max_length=255, blank=True, null=True, deduplication_field=True
) )
search_vector = SearchVectorField(null=True)
last_edited_by = fields.ForeignKey( last_edited_by = fields.ForeignKey(
"User", "User",
@ -142,6 +145,11 @@ class Book(BookDataModel):
self.title, self.title,
) )
class Meta:
"""sets up postgres GIN index field"""
indexes = (GinIndex(fields=["search_vector"]),)
class Work(OrderedCollectionPageMixin, Book): class Work(OrderedCollectionPageMixin, Book):
"""a work (an abstract concept of a book that manifests in an edition)""" """a work (an abstract concept of a book that manifests in an edition)"""

View file

@ -30,7 +30,7 @@ class Favorite(ActivityMixin, BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""update user active time""" """update user active time"""
self.user.last_active_date = timezone.now() self.user.last_active_date = timezone.now()
self.user.save(broadcast=False) self.user.save(broadcast=False, update_fields=["last_active_date"])
super().save(*args, **kwargs) super().save(*args, **kwargs)
if self.status.user.local and self.status.user != self.user: if self.status.user.local and self.status.user != self.user:

View file

@ -13,6 +13,7 @@ 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 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
@ -66,7 +67,7 @@ class ActivitypubFieldMixin:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data): def set_field_from_activity(self, instance, data):
"""helper function for assinging a value to the field""" """helper function for assinging a value to the field. Returns if changed"""
try: try:
value = getattr(data, self.get_activitypub_field()) value = getattr(data, self.get_activitypub_field())
except AttributeError: except AttributeError:
@ -76,8 +77,14 @@ class ActivitypubFieldMixin:
value = getattr(data, "actor") value = getattr(data, "actor")
formatted = self.field_from_activity(value) formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING or formatted == {}: if formatted is None or formatted is MISSING or formatted == {}:
return return False
# the field is unchanged
if hasattr(instance, self.name) and getattr(instance, self.name) == formatted:
return False
setattr(instance, self.name, formatted) setattr(instance, self.name, formatted)
return True
def set_activity_from_field(self, activity, instance): def set_activity_from_field(self, activity, instance):
"""update the json object""" """update the json object"""
@ -204,6 +211,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
# pylint: disable=invalid-name # pylint: disable=invalid-name
def set_field_from_activity(self, instance, data): def set_field_from_activity(self, instance, data):
original = getattr(instance, self.name)
to = data.to to = data.to
cc = data.cc cc = data.cc
if to == [self.public]: if to == [self.public]:
@ -214,6 +222,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
setattr(instance, self.name, "unlisted") setattr(instance, self.name, "unlisted")
else: else:
setattr(instance, self.name, "followers") setattr(instance, self.name, "followers")
return original == getattr(instance, self.name)
def set_activity_from_field(self, activity, instance): def set_activity_from_field(self, activity, instance):
# explicitly to anyone mentioned (statuses only) # explicitly to anyone mentioned (statuses only)
@ -269,9 +278,10 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
value = getattr(data, self.get_activitypub_field()) value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value) formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING: if formatted is None or formatted is MISSING:
return return False
getattr(instance, self.name).set(formatted) getattr(instance, self.name).set(formatted)
instance.save(broadcast=False) instance.save(broadcast=False)
return True
def field_to_activity(self, value): def field_to_activity(self, value):
if self.link_only: if self.link_only:
@ -354,7 +364,8 @@ def image_serializer(value, alt):
url = value.url url = value.url
else: else:
return None return None
url = "https://%s%s" % (DOMAIN, url) if not url[:4] == "http":
url = "https://{:s}{:s}".format(DOMAIN, url)
return activitypub.Document(url=url, name=alt) return activitypub.Document(url=url, name=alt)
@ -371,8 +382,10 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
value = getattr(data, self.get_activitypub_field()) value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value) formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING: if formatted is None or formatted is MISSING:
return return False
getattr(instance, self.name).save(*formatted, save=save) getattr(instance, self.name).save(*formatted, save=save)
return True
def set_activity_from_field(self, activity, instance): def set_activity_from_field(self, activity, instance):
value = getattr(instance, self.name) value = getattr(instance, self.name)
@ -408,7 +421,8 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
return None return None
image_content = ContentFile(response.content) image_content = ContentFile(response.content)
image_name = str(uuid4()) + "." + imghdr.what(None, image_content.read()) extension = imghdr.what(None, image_content.read()) or ""
image_name = "{:s}.{:s}".format(str(uuid4()), extension)
return [image_name, image_content] return [image_name, image_content]
def formfield(self, **kwargs): def formfield(self, **kwargs):

View file

@ -30,7 +30,7 @@ class ReadThrough(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""update user active time""" """update user active time"""
self.user.last_active_date = timezone.now() self.user.last_active_date = timezone.now()
self.user.save(broadcast=False) self.user.save(broadcast=False, update_fields=["last_active_date"])
super().save(*args, **kwargs) super().save(*args, **kwargs)
def create_update(self): def create_update(self):
@ -55,5 +55,5 @@ class ProgressUpdate(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""update user active time""" """update user active time"""
self.user.last_active_date = timezone.now() self.user.last_active_date = timezone.now()
self.user.save(broadcast=False) self.user.save(broadcast=False, update_fields=["last_active_date"])
super().save(*args, **kwargs) super().save(*args, **kwargs)

View file

@ -1,6 +1,7 @@
""" puttin' books on shelves """ """ puttin' books on shelves """
import re import re
from django.db import models from django.db import models
from django.utils import timezone
from bookwyrm import activitypub from bookwyrm import activitypub
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
@ -69,6 +70,7 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
"Edition", on_delete=models.PROTECT, activitypub_field="book" "Edition", on_delete=models.PROTECT, activitypub_field="book"
) )
shelf = models.ForeignKey("Shelf", on_delete=models.PROTECT) shelf = models.ForeignKey("Shelf", on_delete=models.PROTECT)
shelved_date = models.DateTimeField(default=timezone.now)
user = fields.ForeignKey( user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor" "User", on_delete=models.PROTECT, activitypub_field="actor"
) )
@ -86,4 +88,4 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
you can't put a book on shelf twice""" you can't put a book on shelf twice"""
unique_together = ("book", "shelf") unique_together = ("book", "shelf")
ordering = ("-created_date",) ordering = ("-shelved_date", "-created_date", "-updated_date")

View file

@ -7,7 +7,7 @@ from django.contrib.auth.models import AbstractUser, Group
from django.contrib.postgres.fields import CICharField from django.contrib.postgres.fields import CICharField
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.dispatch import receiver from django.dispatch import receiver
from django.db import models from django.db import models, transaction
from django.utils import timezone from django.utils import timezone
from model_utils import FieldTracker from model_utils import FieldTracker
import pytz import pytz
@ -105,6 +105,9 @@ class User(OrderedCollectionPageMixin, AbstractUser):
through_fields=("user", "status"), through_fields=("user", "status"),
related_name="favorite_statuses", related_name="favorite_statuses",
) )
default_post_privacy = models.CharField(
max_length=255, default="public", choices=fields.PrivacyLevels.choices
)
remote_id = fields.RemoteIdField(null=True, unique=True, activitypub_field="id") remote_id = fields.RemoteIdField(null=True, unique=True, activitypub_field="id")
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
@ -243,7 +246,6 @@ class User(OrderedCollectionPageMixin, AbstractUser):
# generate a username that uses the domain (webfinger format) # generate a username that uses the domain (webfinger format)
actor_parts = urlparse(self.remote_id) actor_parts = urlparse(self.remote_id)
self.username = "%s@%s" % (self.username, actor_parts.netloc) self.username = "%s@%s" % (self.username, actor_parts.netloc)
super().save(*args, **kwargs)
# this user already exists, no need to populate fields # this user already exists, no need to populate fields
if not created: if not created:
@ -253,7 +255,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
# this is a new remote user, we need to set their remote server field # this is a new remote user, we need to set their remote server field
if not self.local: if not self.local:
super().save(*args, **kwargs) super().save(*args, **kwargs)
set_remote_server.delay(self.id) transaction.on_commit(lambda: set_remote_server.delay(self.id))
return return
# populate fields for local users # populate fields for local users
@ -276,7 +278,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
self.key_pair = KeyPair.objects.create( self.key_pair = KeyPair.objects.create(
remote_id="%s/#main-key" % self.remote_id remote_id="%s/#main-key" % self.remote_id
) )
self.save(broadcast=False) self.save(broadcast=False, update_fields=["key_pair"])
shelves = [ shelves = [
{ {
@ -406,7 +408,7 @@ def set_remote_server(user_id):
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
actor_parts = urlparse(user.remote_id) actor_parts = urlparse(user.remote_id)
user.federated_server = get_or_create_remote_server(actor_parts.netloc) user.federated_server = get_or_create_remote_server(actor_parts.netloc)
user.save(broadcast=False) user.save(broadcast=False, update_fields=["federated_server"])
if user.bookwyrm_user and user.outbox: if user.bookwyrm_user and user.outbox:
get_remote_reviews.delay(user.outbox) get_remote_reviews.delay(user.outbox)

View file

@ -11,6 +11,7 @@ from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageColor
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.files.storage import default_storage
from django.db.models import Avg from django.db.models import Avg
from bookwyrm import models, settings from bookwyrm import models, settings
@ -319,9 +320,9 @@ def save_and_cleanup(image, instance=None):
try: try:
try: try:
old_path = instance.preview_image.path old_path = instance.preview_image.name
except ValueError: except ValueError:
old_path = "" old_path = None
# Save # Save
image.save(image_buffer, format="jpeg", quality=75) image.save(image_buffer, format="jpeg", quality=75)
@ -337,13 +338,13 @@ def save_and_cleanup(image, instance=None):
save_without_broadcast = isinstance(instance, (models.Book, models.User)) save_without_broadcast = isinstance(instance, (models.Book, models.User))
if save_without_broadcast: if save_without_broadcast:
instance.save(broadcast=False) instance.save(broadcast=False, update_fields=["preview_image"])
else: else:
instance.save() instance.save(update_fields=["preview_image"])
# Clean up old file after saving # Clean up old file after saving
if os.path.exists(old_path): if old_path and default_storage.exists(old_path):
os.remove(old_path) default_storage.delete(old_path)
finally: finally:
image_buffer.close() image_buffer.close()

View file

@ -50,15 +50,15 @@ class RedisStore(ABC):
pipeline.execute() pipeline.execute()
def bulk_remove_objects_from_store(self, objs, store): def bulk_remove_objects_from_store(self, objs, store):
"""remoev a list of objects from a given store""" """remove a list of objects from a given store"""
pipeline = r.pipeline() pipeline = r.pipeline()
for obj in objs[: self.max_length]: for obj in objs[: self.max_length]:
pipeline.zrem(store, -1, obj.id) pipeline.zrem(store, -1, obj.id)
pipeline.execute() pipeline.execute()
def get_store(self, store): # pylint: disable=no-self-use def get_store(self, store, **kwargs): # pylint: disable=no-self-use
"""load the values in a store""" """load the values in a store"""
return r.zrevrange(store, 0, -1) return r.zrevrange(store, 0, -1, **kwargs)
def populate_store(self, store): def populate_store(self, store):
"""go from zero to a store""" """go from zero to a store"""

View file

@ -58,6 +58,7 @@ SECRET_KEY = env("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env.bool("DEBUG", True) DEBUG = env.bool("DEBUG", True)
USE_HTTPS = env.bool("USE_HTTPS", False)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"]) ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"])
@ -74,6 +75,7 @@ INSTALLED_APPS = [
"django_rename_app", "django_rename_app",
"bookwyrm", "bookwyrm",
"celery", "celery",
"storages",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -166,6 +168,7 @@ LANGUAGES = [
("es", _("Spanish")), ("es", _("Spanish")),
("fr-fr", _("French")), ("fr-fr", _("French")),
("zh-hans", _("Simplified Chinese")), ("zh-hans", _("Simplified Chinese")),
("zh-hant", _("Traditional Chinese")),
] ]
@ -178,19 +181,51 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_URL = "/static/"
STATIC_PATH = "%s/%s" % (DOMAIN, env("STATIC_ROOT", "static"))
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_URL = "/images/"
MEDIA_PATH = "%s/%s" % (DOMAIN, env("MEDIA_ROOT", "images"))
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % ( USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % (
requests.utils.default_user_agent(), requests.utils.default_user_agent(),
VERSION, VERSION,
DOMAIN, DOMAIN,
) )
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
# Storage
PROTOCOL = "http"
if USE_HTTPS:
PROTOCOL = "https"
USE_S3 = env.bool("USE_S3", False)
if USE_S3:
# AWS settings
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
AWS_S3_CUSTOM_DOMAIN = env("AWS_S3_CUSTOM_DOMAIN")
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", "")
AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL")
AWS_DEFAULT_ACL = "public-read"
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
# S3 Static settings
STATIC_LOCATION = "static"
STATIC_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, STATIC_LOCATION)
STATICFILES_STORAGE = "bookwyrm.storage_backends.StaticStorage"
# S3 Media settings
MEDIA_LOCATION = "images"
MEDIA_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, MEDIA_LOCATION)
MEDIA_FULL_URL = MEDIA_URL
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
# I don't know if it's used, but the site crashes without it
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
else:
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
MEDIA_URL = "/images/"
MEDIA_FULL_URL = "%s://%s%s" % (PROTOCOL, DOMAIN, MEDIA_URL)
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))

View file

@ -72,6 +72,14 @@ body {
flex-grow: 1; flex-grow: 1;
} }
.preserve-whitespace p {
white-space: pre-wrap !important;
}
.display-inline p {
display: inline !important;
}
/** Shelving /** Shelving
******************************************************************************/ ******************************************************************************/

View file

@ -0,0 +1,17 @@
"""Handles backends for storages"""
from storages.backends.s3boto3 import S3Boto3Storage
class StaticStorage(S3Boto3Storage): # pylint: disable=abstract-method
"""Storage class for Static contents"""
location = "static"
default_acl = "public-read"
class ImagesStorage(S3Boto3Storage): # pylint: disable=abstract-method
"""Storage class for Image files"""
location = "images"
default_acl = "public-read"
file_overwrite = False

221
bookwyrm/suggested_users.py Normal file
View file

@ -0,0 +1,221 @@
""" store recommended follows in redis """
import math
import logging
from django.dispatch import receiver
from django.db.models import signals, Count, Q
from bookwyrm import models
from bookwyrm.redis_store import RedisStore, r
from bookwyrm.tasks import app
logger = logging.getLogger(__name__)
class SuggestedUsers(RedisStore):
"""suggested users for a user"""
max_length = 30
def get_rank(self, obj):
"""get computed rank"""
return obj.mutuals + (1.0 - (1.0 / (obj.shared_books + 1)))
def store_id(self, user): # pylint: disable=no-self-use
"""the key used to store this user's recs"""
if isinstance(user, int):
return "{:d}-suggestions".format(user)
return "{:d}-suggestions".format(user.id)
def get_counts_from_rank(self, rank): # pylint: disable=no-self-use
"""calculate mutuals count and shared books count from rank"""
return {
"mutuals": math.floor(rank),
"shared_books": int(1 / (-1 * (rank % 1 - 1))) - 1,
}
def get_objects_for_store(self, store):
"""a list of potential follows for a user"""
user = models.User.objects.get(id=store.split("-")[0])
return get_annotated_users(
user,
~Q(id=user.id),
~Q(followers=user),
~Q(follower_requests=user),
bookwyrm_user=True,
)
def get_stores_for_object(self, obj):
return [self.store_id(u) for u in self.get_users_for_object(obj)]
def get_users_for_object(self, obj): # pylint: disable=no-self-use
"""given a user, who might want to follow them"""
return models.User.objects.filter(local=True,).exclude(
Q(id=obj.id) | Q(followers=obj) | Q(id__in=obj.blocks.all()) | Q(blocks=obj)
)
def rerank_obj(self, obj, update_only=True):
"""update all the instances of this user with new ranks"""
pipeline = r.pipeline()
for store_user in self.get_users_for_object(obj):
annotated_user = get_annotated_users(
store_user,
id=obj.id,
).first()
if not annotated_user:
continue
pipeline.zadd(
self.store_id(store_user),
self.get_value(annotated_user),
xx=update_only,
)
pipeline.execute()
def rerank_user_suggestions(self, user):
"""update the ranks of the follows suggested to a user"""
self.populate_store(self.store_id(user))
def remove_suggestion(self, user, suggested_user):
"""take a user out of someone's suggestions"""
self.bulk_remove_objects_from_store([suggested_user], self.store_id(user))
def get_suggestions(self, user):
"""get suggestions"""
values = self.get_store(self.store_id(user), withscores=True)
results = []
# annotate users with mutuals and shared book counts
for user_id, rank in values[:5]:
counts = self.get_counts_from_rank(rank)
try:
user = models.User.objects.get(id=user_id)
except models.User.DoesNotExist as err:
# if this happens, the suggestions are janked way up
logger.exception(err)
continue
user.mutuals = counts["mutuals"]
user.shared_books = counts["shared_books"]
results.append(user)
return results
def get_annotated_users(viewer, *args, **kwargs):
"""Users, annotated with things they have in common"""
return (
models.User.objects.filter(discoverable=True, is_active=True, *args, **kwargs)
.exclude(Q(id__in=viewer.blocks.all()) | Q(blocks=viewer))
.annotate(
mutuals=Count(
"followers",
filter=Q(
~Q(id=viewer.id),
~Q(id__in=viewer.following.all()),
followers__in=viewer.following.all(),
),
distinct=True,
),
shared_books=Count(
"shelfbook",
filter=Q(
~Q(id=viewer.id),
shelfbook__book__parent_work__in=[
s.book.parent_work for s in viewer.shelfbook_set.all()
],
),
distinct=True,
),
)
)
suggested_users = SuggestedUsers()
@receiver(signals.post_save, sender=models.UserFollows)
# pylint: disable=unused-argument
def update_suggestions_on_follow(sender, instance, created, *args, **kwargs):
"""remove a follow from the recs and update the ranks"""
if not created or not instance.user_object.discoverable:
return
if instance.user_subject.local:
remove_suggestion_task.delay(instance.user_subject.id, instance.user_object.id)
rerank_user_task.delay(instance.user_object.id, update_only=False)
@receiver(signals.post_save, sender=models.UserBlocks)
# pylint: disable=unused-argument
def update_suggestions_on_block(sender, instance, *args, **kwargs):
"""remove blocked users from recs"""
if instance.user_subject.local and instance.user_object.discoverable:
remove_suggestion_task.delay(instance.user_subject.id, instance.user_object.id)
if instance.user_object.local and instance.user_subject.discoverable:
remove_suggestion_task.delay(instance.user_object.id, instance.user_subject.id)
@receiver(signals.post_delete, sender=models.UserFollows)
# pylint: disable=unused-argument
def update_suggestions_on_unfollow(sender, instance, **kwargs):
"""update rankings, but don't re-suggest because it was probably intentional"""
if instance.user_object.discoverable:
rerank_user_task.delay(instance.user_object.id, update_only=False)
@receiver(signals.post_save, sender=models.ShelfBook)
@receiver(signals.post_delete, sender=models.ShelfBook)
# pylint: disable=unused-argument
def update_rank_on_shelving(sender, instance, *args, **kwargs):
"""when a user shelves or unshelves a book, re-compute their rank"""
# if it's a local user, re-calculate who is rec'ed to them
if instance.user.local:
rerank_suggestions_task.delay(instance.user.id)
# if the user is discoverable, update their rankings
if instance.user.discoverable:
rerank_user_task.delay(instance.user.id)
@receiver(signals.post_save, sender=models.User)
# pylint: disable=unused-argument, too-many-arguments
def add_new_user(sender, instance, created, update_fields=None, **kwargs):
"""a new user, wow how cool"""
# a new user is found, create suggestions for them
if created and instance.local:
rerank_suggestions_task.delay(instance.id)
if update_fields and not "discoverable" in update_fields:
return
# this happens on every save, not just when discoverability changes, annoyingly
if instance.discoverable:
rerank_user_task.delay(instance.id, update_only=False)
elif not created:
remove_user_task.delay(instance.id)
@app.task
def rerank_suggestions_task(user_id):
"""do the hard work in celery"""
suggested_users.rerank_user_suggestions(user_id)
@app.task
def rerank_user_task(user_id, update_only=False):
"""do the hard work in celery"""
user = models.User.objects.get(id=user_id)
suggested_users.rerank_obj(user, update_only=update_only)
@app.task
def remove_user_task(user_id):
"""do the hard work in celery"""
user = models.User.objects.get(id=user_id)
suggested_users.remove_object_from_related_stores(user)
@app.task
def remove_suggestion_task(user_id, suggested_user_id):
"""remove a specific user from a specific user's suggestions"""
suggested_user = models.User.objects.get(id=suggested_user_id)
suggested_users.remove_suggestion(user_id, suggested_user)

View file

@ -7,6 +7,5 @@ from bookwyrm import settings
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "celerywyrm.settings")
app = Celery( app = Celery(
"tasks", "tasks", broker=settings.CELERY_BROKER, backend=settings.CELERY_RESULT_BACKEND
broker=settings.CELERY_BROKER,
) )

View file

@ -1,5 +1,10 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %}{% load layout %} {% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% load utilities %}
{% load static %}
{% load layout %}
{% block title %}{{ book|book_title }}{% endblock %} {% block title %}{{ book|book_title }}{% endblock %}
@ -47,7 +52,7 @@
{% if user_authenticated and can_edit_book %} {% if user_authenticated and can_edit_book %}
<div class="column is-narrow"> <div class="column is-narrow">
<a href="{{ book.id }}/edit"> <a href="{% url 'edit-book' book.id %}">
<span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span> <span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
<span class="is-hidden-mobile">{% trans "Edit Book" %}</span> <span class="is-hidden-mobile">{% trans "Edit Book" %}</span>
</a> </a>
@ -209,24 +214,24 @@
<ul> <ul>
{% url 'book' book.id as tab_url %} {% url 'book' book.id as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}> <li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Reviews" %} ({{ review_count }})</a> <a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a>
</li> </li>
{% if user_statuses.review_count %} {% if user_statuses.review_count %}
{% url 'book-user-statuses' book.id 'review' as tab_url %} {% url 'book-user-statuses' book.id 'review' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}> <li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Your reviews" %} ({{ user_statuses.review_count }})</a> <a href="{{ tab_url }}#reviews">{% trans "Your reviews" %} ({{ user_statuses.review_count }})</a>
</li> </li>
{% endif %} {% endif %}
{% if user_statuses.comment_count %} {% if user_statuses.comment_count %}
{% url 'book-user-statuses' book.id 'comment' as tab_url %} {% url 'book-user-statuses' book.id 'comment' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}> <li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Your comments" %} ({{ user_statuses.comment_count }})</a> <a href="{{ tab_url }}#reviews">{% trans "Your comments" %} ({{ user_statuses.comment_count }})</a>
</li> </li>
{% endif %} {% endif %}
{% if user_statuses.quotation_count %} {% if user_statuses.quotation_count %}
{% url 'book-user-statuses' book.id 'quote' as tab_url %} {% url 'book-user-statuses' book.id 'quote' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}> <li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Your quotes" %} ({{ user_statuses.quotation_count }})</a> <a href="{{ tab_url }}#reviews">{% trans "Your quotes" %} ({{ user_statuses.quotation_count }})</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
@ -321,5 +326,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/vendor/tabs.js"></script> <script src="{% static "js/vendor/tabs.js" %}"></script>
{% endblock %} {% endblock %}

View file

@ -11,14 +11,21 @@
</a> </a>
<div class="media-content"> <div class="media-content">
<a href="{{ user.local_path }}" class="is-block mb-2"> <a href="{{ user.local_path }}" class="is-block mb-2">
<span class="title is-4 is-block">{{ user.display_name }}</span> <span class="title is-4 is-block">
{{ user.display_name }}
{% if user.manually_approves_followers %}
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
<span class="is-sr-only">{% trans "Locked account" %}</span>
</span>
{% endif %}
</span>
<span class="subtitle is-7 is-block">@{{ user|username }}</span> <span class="subtitle is-7 is-block">@{{ user|username }}</span>
</a> </a>
{% include 'snippets/follow_button.html' with user=user %} {% include 'snippets/follow_button.html' with user=user %}
</div> </div>
</div> </div>
<div> <div class="display-inline">
{% if user.summary %} {% if user.summary %}
{{ user.summary|to_markdown|safe|truncatechars_html:40 }} {{ user.summary|to_markdown|safe|truncatechars_html:40 }}
{% else %}&nbsp;{% endif %} {% else %}&nbsp;{% endif %}

View file

@ -1,4 +1,4 @@
{% extends 'feed/feed_layout.html' %} {% extends 'feed/layout.html' %}
{% load i18n %} {% load i18n %}
{% block panel %} {% block panel %}

View file

@ -1,4 +1,4 @@
{% extends 'feed/feed_layout.html' %} {% extends 'feed/layout.html' %}
{% load i18n %} {% load i18n %}
{% block panel %} {% block panel %}
@ -44,18 +44,22 @@
{# activity feed #} {# activity feed #}
{% if not activities %} {% if not activities %}
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p> <div class="block content">
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p>
{% if suggested_users %}
{# suggested users for when things are very lonely #}
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
</div>
{% endif %}
{% endif %} {% endif %}
{% for activity in activities %} {% for activity in activities %}
{% if not activities.number > 1 and forloop.counter0 == 2 and suggested_users %} {% if not activities.number > 1 and forloop.counter0 == 2 and suggested_users %}
{# suggested users on the first page, two statuses down #} {# suggested users on the first page, two statuses down #}
<section class="block"> {% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
<h2 class="title is-5">{% trans "Who to follow" %}</h2>
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
<a class="help" href="{% url 'directory' %}">View directory <span class="icon icon-arrow-right"></a>
</section>
{% endif %} {% endif %}
<div class="block"> <div class="block">
{% include 'snippets/status/status.html' with status=activity %} {% include 'snippets/status/status.html' with status=activity %}

View file

@ -1,107 +0,0 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block title %}{% trans "Updates" %}{% endblock %}
{% block content %}
<div class="columns">
{% if user.is_authenticated %}
<div class="column is-one-third">
<h2 class="title is-5">{% trans "Your books" %}</h2>
{% if not suggested_books %}
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
{% else %}
{% with active_book=request.GET.book %}
<div class="tab-group">
<div class="tabs is-small">
<ul role="tablist">
{% for shelf in suggested_books %}
{% if shelf.books %}
{% with shelf_counter=forloop.counter %}
<li>
<p>
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
{% else %}{{ shelf.name }}{% endif %}
</p>
<div class="tabs is-small is-toggle">
<ul>
{% for book in shelf.books %}
<li class="{% if active_book == book.id|stringformat:'d' %}is-active{% elif not active_book and shelf_counter == 1 and forloop.first %}is-active{% endif %}">
<a
href="{{ request.path }}?book={{ book.id }}"
id="tab-book-{{ book.id }}"
role="tab"
aria-label="{{ book.title }}"
aria-selected="{% if active_book == book.id|stringformat:'d' %}true{% elif shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}"
aria-controls="book-{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' %}
</a>
</li>
{% endfor %}
</ul>
</div>
</li>
{% endwith %}
{% endif %}
{% endfor %}
</ul>
</div>
{% for shelf in suggested_books %}
{% with shelf_counter=forloop.counter %}
{% for book in shelf.books %}
<div
class="suggested-tabs card"
role="tabpanel"
id="book-{{ book.id }}"
{% if active_book and active_book == book.id|stringformat:'d' %}{% elif not active_book and shelf_counter == 1 and forloop.first %}{% else %} hidden{% endif %}
aria-labelledby="tab-book-{{ book.id }}">
<div class="card-header">
<div class="card-header-title">
<div>
<p class="mb-2">{% include 'snippets/book_titleby.html' with book=book %}</p>
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
</div>
<div class="card-header-icon is-hidden-tablet">
{% trans "Close" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with label=button_text controls_text="book" controls_uid=book.id class="delete" nonbutton=True pressed=True %}
</div>
</div>
<div class="card-content">
{% include 'snippets/create_status.html' with book=book %}
</div>
</div>
{% endfor %}
{% endwith %}
{% endfor %}
</div>
{% endwith %}
{% endif %}
{% if goal %}
<section class="section">
<div class="block">
<h3 class="title is-4">{% blocktrans with yar=goal.year %}{{ year }} Reading Goal{% endblocktrans %}</h3>
{% include 'snippets/goal_progress.html' with goal=goal %}
</div>
</section>
{% endif %}
</div>
{% endif %}
<div class="column is-two-thirds" id="feed">
{% block panel %}{% endblock %}
{% if activities %}
{% include 'snippets/pagination.html' with page=activities path=path anchor="#feed" %}
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/vendor/tabs.js"></script>
{% endblock %}

View file

@ -0,0 +1,110 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Updates" %}{% endblock %}
{% block content %}
<div class="columns">
{% if user.is_authenticated %}
<div class="column is-one-third">
<section class="block">
<h2 class="title is-4">{% trans "Your books" %}</h2>
{% if not suggested_books %}
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
{% else %}
{% with active_book=request.GET.book %}
<div class="tab-group">
<div class="tabs is-small">
<ul role="tablist">
{% for shelf in suggested_books %}
{% if shelf.books %}
{% with shelf_counter=forloop.counter %}
<li>
<p>
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
{% else %}{{ shelf.name }}{% endif %}
</p>
<div class="tabs is-small is-toggle">
<ul>
{% for book in shelf.books %}
<li class="{% if active_book == book.id|stringformat:'d' %}is-active{% elif not active_book and shelf_counter == 1 and forloop.first %}is-active{% endif %}">
<a
href="{{ request.path }}?book={{ book.id }}"
id="tab-book-{{ book.id }}"
role="tab"
aria-label="{{ book.title }}"
aria-selected="{% if active_book == book.id|stringformat:'d' %}true{% elif shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}"
aria-controls="book-{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' %}
</a>
</li>
{% endfor %}
</ul>
</div>
</li>
{% endwith %}
{% endif %}
{% endfor %}
</ul>
</div>
{% for shelf in suggested_books %}
{% with shelf_counter=forloop.counter %}
{% for book in shelf.books %}
<div
class="suggested-tabs card"
role="tabpanel"
id="book-{{ book.id }}"
{% if active_book and active_book == book.id|stringformat:'d' %}{% elif not active_book and shelf_counter == 1 and forloop.first %}{% else %} hidden{% endif %}
aria-labelledby="tab-book-{{ book.id }}">
<div class="card-header">
<div class="card-header-title">
<div>
<p class="mb-2">{% include 'snippets/book_titleby.html' with book=book %}</p>
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
</div>
<div class="card-header-icon is-hidden-tablet">
{% trans "Close" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with label=button_text controls_text="book" controls_uid=book.id class="delete" nonbutton=True pressed=True %}
</div>
</div>
<div class="card-content">
{% include 'snippets/create_status.html' with book=book %}
</div>
</div>
{% endfor %}
{% endwith %}
{% endfor %}
</div>
{% endwith %}
{% endif %}
</section>
{% if goal %}
<section class="block">
<div class="block">
<h3 class="title is-4">{% blocktrans with yar=goal.year %}{{ year }} Reading Goal{% endblocktrans %}</h3>
{% include 'snippets/goal_progress.html' with goal=goal %}
</div>
</section>
{% endif %}
</div>
{% endif %}
<div class="column is-two-thirds" id="feed">
{% block panel %}{% endblock %}
{% if activities %}
{% include 'snippets/pagination.html' with page=activities path=path anchor="#feed" %}
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{% static "js/vendor/tabs.js" %}"></script>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'feed/feed_layout.html' %} {% extends 'feed/layout.html' %}
{% load i18n %} {% load i18n %}
{% block panel %} {% block panel %}

View file

@ -1,25 +1,6 @@
{% load i18n %} {% load i18n %}
{% load utilities %} <section class="block">
{% load humanize %} <h2 class="title is-5">{% trans "Who to follow" %}</h2>
<div class="columns is-mobile scroll-x mb-0"> {% include 'snippets/suggested_users.html' with suggested_users=suggested_users %}
{% for user in suggested_users %} <a class="help" href="{% url 'directory' %}">View directory <span class="icon icon-arrow-right"></a>
<div class="column is-flex is-flex-grow-0"> </section>
<div class="box has-text-centered is-shadowless has-background-white-bis m-0">
<a href="{{ user.local_path }}" class="has-text-black">
{% include 'snippets/avatar.html' with user=user large=True %}
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
</a>
{% include 'snippets/follow_button.html' with user=user minimal=True %}
{% if user.mutuals %}
<p class="help">
{% blocktrans with mutuals=user.mutuals|intcomma count counter=user.mutuals %}{{ mutuals }} follower you follow{% plural %}{{ mutuals }} followers you follow{% endblocktrans %}
</p>
{% elif user.shared_books %}
<p class="help">{% blocktrans with shared_books=user.shared_books|intcomma count counter=user.shared_books %}{{ shared_books }} book on your shelves{% plural %}{{ shared_books }} books on your shelves{% endblocktrans %}</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>

View file

@ -1,5 +1,6 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block title %}{% trans "Welcome" %}{% endblock %} {% block title %}{% trans "Welcome" %}{% endblock %}
@ -9,7 +10,7 @@
<div class="modal-background"></div> <div class="modal-background"></div>
<div class="modal-card is-fullwidth"> <div class="modal-card is-fullwidth">
<header class="modal-card-head"> <header class="modal-card-head">
<img class="image logo mr-2" src="{% if site.logo_small %}/images/{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" aria-hidden="true"> <img class="image logo mr-2" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" aria-hidden="true">
<h1 class="modal-card-title" id="get-started-header"> <h1 class="modal-card-title" id="get-started-header">
{% blocktrans %}Welcome to {{ site_name }}!{% endblocktrans %} {% blocktrans %}Welcome to {{ site_name }}!{% endblocktrans %}
<span class="subtitle is-block"> <span class="subtitle is-block">

View file

@ -9,7 +9,7 @@
<form class="field has-addons" method="get" action="{% url 'get-started-users' %}"> <form class="field has-addons" method="get" action="{% url 'get-started-users' %}">
<div class="control"> <div class="control">
<input type="text" name="query" value="{{ request.GET.query }}" class="input" placeholder="{% trans 'Search for a user' %}" aria-label="{% trans 'Search for a user' %}"> <input type="text" name="query" value="{{ request.GET.query }}" class="input" placeholder="{% trans 'Search for a user' %}" aria-label="{% trans 'Search for a user' %}">
{% if request.GET.query and not user_results %} {% if request.GET.query and no_results %}
<p class="help">{% blocktrans with query=request.GET.query %}No users found for "{{ query }}"{% endblocktrans %}</p> <p class="help">{% blocktrans with query=request.GET.query %}No users found for "{{ query }}"{% endblocktrans %}</p>
{% endif %} {% endif %}
</div> </div>
@ -22,7 +22,7 @@
</div> </div>
</form> </form>
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %} {% include 'snippets/suggested_users.html' with suggested_users=suggested_users %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -17,9 +17,9 @@
{% block panel %} {% block panel %}
<section class="block"> <section class="block">
{% if user == request.user %} {% now 'Y' as current_year %}
{% if user == request.user and year == current_year %}
<div class="block"> <div class="block">
{% now 'Y' as year %}
<section class="card {% if goal %}is-hidden{% endif %}" id="show-edit-goal"> <section class="card {% if goal %}is-hidden{% endif %}" id="show-edit-goal">
<header class="card-header"> <header class="card-header">
<h2 class="card-header-title has-background-primary has-text-white" tabindex="0" id="edit-form-header"> <h2 class="card-header-title has-background-primary has-text-white" tabindex="0" id="edit-form-header">

View file

@ -1,6 +1,7 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% load static %}
{% block title %}{% trans "Import Status" %}{% endblock %} {% block title %}{% trans "Import Status" %}{% endblock %}
@ -156,5 +157,5 @@
{% endspaceless %}{% endblock %} {% endspaceless %}{% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/check_all.js"></script> <script src="{% static "js/check_all.js" %}"></script>
{% endblock %} {% endblock %}

View file

@ -1,14 +1,16 @@
{% load layout %}{% load i18n %} {% load layout %}
{% load i18n %}
{% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{% get_lang %}"> <html lang="{% get_lang %}">
<head> <head>
<title>{% block title %}BookWyrm{% endblock %} | {{ site.name }}</title> <title>{% block title %}BookWyrm{% endblock %} | {{ site.name }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/css/vendor/bulma.min.css"> <link rel="stylesheet" href="{% static "css/vendor/bulma.min.css" %}">
<link rel="stylesheet" href="/static/css/vendor/icons.css"> <link rel="stylesheet" href="{% static "css/vendor/icons.css" %}">
<link rel="stylesheet" href="/static/css/bookwyrm.css"> <link rel="stylesheet" href="{% static "css/bookwyrm.css" %}">
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{{ media_url }}{{ site.favicon }}{% else %}/static/images/favicon.ico{% endif %}"> <link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}">
{% if preview_images_enabled is True %} {% if preview_images_enabled is True %}
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
@ -30,7 +32,7 @@
<div class="container"> <div class="container">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="/"> <a class="navbar-item" href="/">
<img class="image logo" src="{% if site.logo_small %}{{ media_url }}{{ site.logo_small }}{% else %}/static/images/logo-small.png{% endif %}" alt="Home page"> <img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="Home page">
</a> </a>
<form class="navbar-item column" action="/search/"> <form class="navbar-item column" action="/search/">
<div class="field has-addons"> <div class="field has-addons">
@ -149,7 +151,7 @@
{% if request.path != '/login' and request.path != '/login/' %} {% if request.path != '/login' and request.path != '/login/' %}
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<form name="login" method="post" action="/login"> <form name="login" method="post" action="{% url 'login' %}?next={{ request.path }}">
{% csrf_token %} {% csrf_token %}
<div class="columns is-variable is-1"> <div class="columns is-variable is-1">
<div class="column"> <div class="column">
@ -242,8 +244,8 @@
<script> <script>
var csrf_token = '{{ csrf_token }}'; var csrf_token = '{{ csrf_token }}';
</script> </script>
<script src="/static/js/bookwyrm.js"></script> <script src="{% static "js/bookwyrm.js" %}"></script>
<script src="/static/js/localstorage.js"></script> <script src="{% static "js/localstorage.js" %}"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View file

@ -53,6 +53,14 @@
{{ form.manually_approves_followers }} {{ form.manually_approves_followers }}
</label> </label>
</div> </div>
<div class="block">
<label class="label" for="id_default_post_privacy">
{% trans "Default post privacy:" %}
</label>
<div class="select">
{{ form.default_post_privacy }}
</div>
</div>
<div class="block"> <div class="block">
<label class="checkbox label" for="id_discoverable"> <label class="checkbox label" for="id_discoverable">
{% trans "Show this account in suggested users:" %} {% trans "Show this account in suggested users:" %}

View file

@ -1,7 +1,9 @@
{% load static %}
<div class="columns"> <div class="columns">
<div class="column is-narrow is-hidden-mobile"> <div class="column is-narrow is-hidden-mobile">
<figure class="block"> <figure class="block is-w-xl">
<img src="{% if site.logo %}/images/{{ site.logo }}{% else %}/static/images/logo.png{% endif %}" alt="BookWyrm logo"> <img src="{% if site.logo %}/images/{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}" alt="BookWyrm logo">
</figure> </figure>
</div> </div>
<div class="content"> <div class="content">

View file

@ -1,9 +1,16 @@
{% spaceless %} {% spaceless %}
{% load i18n %}
{% load humanize %}
{% comment %} {% comment %}
@todo The author property needs to be an Organization or a Person. Well be using Thing which is the more generic ancestor. @todo The author property needs to be an Organization or a Person. Well be using Thing which is the more generic ancestor.
@see https://schema.org/Author @see https://schema.org/Author
{% endcomment %} {% endcomment %}
{% for author in book.authors.all %} {% firstof limit None as limit %}
{% with subtraction_value='-'|add:limit %}
{% with remainder_count=book.authors.count|add:subtraction_value %}
{% with remainder_count_display=remainder_count|intcomma %}
{% for author in book.authors.all|slice:limit %}
<a <a
href="{{ author.local_path }}" href="{{ author.local_path }}"
class="author" class="author"
@ -12,6 +19,14 @@
itemtype="https://schema.org/Thing" itemtype="https://schema.org/Thing"
><span ><span
itemprop="name" itemprop="name"
>{{ author.name }}<span></a>{% if not forloop.last %}, {% endif %} >{{ author.name }}<span></a>{% if not forloop.last %}, {% elif remainder_count > 0 %}, {% blocktrans trimmed count counter=remainder_count %}
and {{ remainder_count_display }} other
{% plural %}
and {{ remainder_count_display }} others
{% endblocktrans %}{% endif %}
{% endfor %} {% endfor %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endspaceless %} {% endspaceless %}

View file

@ -1,2 +1,4 @@
<img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}"> {% load static %}
<img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}{% get_media_prefix %}{{ user.avatar }}{% else %}{% static "images/default_avi.jpg" %}{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}">

View file

@ -1,6 +1,7 @@
{% spaceless %} {% spaceless %}
{% load i18n %} {% load i18n %}
{% load static %}
<figure <figure
class=" class="
@ -20,14 +21,14 @@
class="book-cover" class="book-cover"
{% if book.cover %} {% if book.cover %}
src="{% if img_path is None %}/images/{% else %}{{ img_path }}{% endif %}{{ book.cover }}" src="{% if img_path is None %}{% get_media_prefix %}{% else %}{{ img_path }}{% endif %}{{ book.cover }}"
itemprop="thumbnailUrl" itemprop="thumbnailUrl"
{% if book.alt_text %} {% if book.alt_text %}
alt="{{ book.alt_text }}" alt="{{ book.alt_text }}"
{% endif %} {% endif %}
{% else %} {% else %}
src="/static/images/no_cover.jpg" src="{% static "images/no_cover.jpg" %}"
alt="{% trans "No cover" %}" alt="{% trans "No cover" %}"
{% endif %} {% endif %}
> >

View file

@ -1,8 +1,15 @@
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
{% spaceless %}
{% if book.authors %} {% if book.authors %}
{% blocktrans with path=book.local_path title=book|book_title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %} {% blocktrans trimmed with path=book.local_path title=book|book_title %}
<a href="{{ path }}">{{ title }}</a> by
{% endblocktrans %}
{% 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 %}

View file

@ -1,12 +1,14 @@
{% load static %}
{% if preview_images_enabled is True %} {% if preview_images_enabled is True %}
{% if image %} {% if image %}
<meta name="twitter:image" content="{{ request.scheme }}://{{ media_path }}{{ image }}"> <meta name="twitter:image" content="{{ media_full_url }}{{ image }}">
<meta name="og:image" content="{{ request.scheme }}://{{ media_path }}{{ image }}"> <meta name="og:image" content="{{ media_full_url }}{{ image }}">
{% else %} {% else %}
<meta name="twitter:image" content="{{ request.scheme }}://{{ media_path }}{{ site.preview_image }}"> <meta name="twitter:image" content="{{ media_full_url }}{{ site.preview_image }}">
<meta name="og:image" content="{{ request.scheme }}://{{ media_path }}{{ site.preview_image }}"> <meta name="og:image" content="{{ media_full_url }}{{ site.preview_image }}">
{% endif %} {% endif %}
{% else %} {% else %}
<meta name="twitter:image" content="{{ request.scheme }}://{% if site.logo %}{{ media_path }}{{ site.logo }}{% else %}{{ static_path }}/images/logo.png{% endif %}"> <meta name="twitter:image" content="{% if site.logo %}{{ media_full_url }}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}">
<meta name="og:image" content="{{ request.scheme }}://{% if site.logo %}{{ media_path }}{{ site.logo }}{% else %}{{ static_path }}/images/logo.png{% endif %}"> <meta name="og:image" content="{% if site.logo %}{{ media_full_url }}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}">
{% endif %} {% endif %}

View file

@ -5,17 +5,18 @@
{% if not no_label %} {% if not no_label %}
<label class="is-sr-only" for="privacy-{{ uuid }}">{% trans "Post privacy" %}</label> <label class="is-sr-only" for="privacy-{{ uuid }}">{% trans "Post privacy" %}</label>
{% endif %} {% endif %}
{% firstof current user.default_post_privacy "public" as privacy %}
<select name="privacy" id="privacy-{{ uuid }}"> <select name="privacy" id="privacy-{{ uuid }}">
<option value="public" {% if not current or current == 'public' %}selected{% endif %}> <option value="public" {% if privacy == 'public' %}selected{% endif %}>
{% trans "Public" %} {% trans "Public" %}
</option> </option>
<option value="unlisted" {% if current == 'unlisted' %}selected{% endif %}> <option value="unlisted" {% if privacy == 'unlisted' %}selected{% endif %}>
{% trans "Unlisted" %} {% trans "Unlisted" %}
</option> </option>
<option value="followers" {% if current == 'followers' %}selected{% endif %}> <option value="followers" {% if privacy == 'followers' %}selected{% endif %}>
{% trans "Followers" %} {% trans "Followers" %}
</option> </option>
<option value="direct" {% if current == 'direct' %}selected{% endif %}> <option value="direct" {% if privacy == 'direct' %}selected{% endif %}>
{% trans "Private" %} {% trans "Private" %}
</option> </option>
</select> </select>

View file

@ -1,6 +1,7 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load markdown %} {% load markdown %}
{% load i18n %} {% load i18n %}
{% load static %}
{% with status_type=status.status_type %} {% with status_type=status.status_type %}
<div <div
@ -111,12 +112,12 @@
<div class="column is-narrow"> <div class="column is-narrow">
<figure class="image is-128x128"> <figure class="image is-128x128">
<a <a
href="/images/{{ attachment.image }}" href="{% get_media_prefix %}{{ attachment.image }}"
target="_blank" target="_blank"
aria-label="{% trans 'Open image in new window' %}" aria-label="{% trans 'Open image in new window' %}"
> >
<img <img
src="/images/{{ attachment.image }}" src="{% get_media_prefix %}{{ attachment.image }}"
{% if attachment.caption %} {% if attachment.caption %}
alt="{{ attachment.caption }}" alt="{{ attachment.caption }}"

View file

@ -2,6 +2,7 @@
{% load status_display %} {% load status_display %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% load static %}
<div class="media"> <div class="media">
<figure class="media-left" aria-hidden="true"> <figure class="media-left" aria-hidden="true">
@ -18,7 +19,7 @@
itemtype="https://schema.org/Person" itemtype="https://schema.org/Person"
> >
{% if status.user.avatar %} {% if status.user.avatar %}
<meta itemprop="image" content="/images/{{ status.user.avatar }}"> <meta itemprop="image" content="{% get_media_prefix %}{{ status.user.avatar }}">
{% endif %} {% endif %}
<a <a

View file

@ -0,0 +1,38 @@
{% load i18n %}
{% load utilities %}
{% load humanize %}
<div class="columns is-mobile scroll-x mb-0">
{% for user in suggested_users %}
<div class="column is-flex is-flex-grow-0">
<div class="box has-text-centered is-shadowless has-background-white-bis m-0">
<a href="{{ user.local_path }}" class="has-text-black">
{% include 'snippets/avatar.html' with user=user large=True %}
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
</a>
{% include 'snippets/follow_button.html' with user=user minimal=True %}
{% if user.mutuals %}
<p class="help">
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
{{ mutuals }} follower you follow
{% plural %}
{{ mutuals }} followers you follow{% endblocktrans %}
</p>
{% elif user.shared_books %}
<p class="help">
{% blocktrans trimmed with shared_books=user.shared_books|intcomma count counter=user.shared_books %}
{{ shared_books }} book on your shelves
{% plural %}
{{ shared_books }} books on your shelves
{% endblocktrans %}
</p>
{% elif request.user in user.following.all %}
<p class="help">
{% trans "Follows you" %}
</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>

View file

@ -28,8 +28,10 @@
</div> </div>
{% if user.summary %} {% if user.summary %}
<div class="column box has-background-white-bis content"> {% spaceless %}
<div class="column box has-background-white-bis content preserve-whitespace">
{{ user.summary|to_markdown|safe }} {{ user.summary|to_markdown|safe }}
{% endspaceless %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -12,3 +12,4 @@
{% blocktrans with username=user.display_name %}{{ username }} isn't following any users{% endblocktrans %} {% blocktrans with username=user.display_name %}{{ username }} isn't following any users{% endblocktrans %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -103,7 +103,7 @@
{% include 'snippets/authors.html' %} {% include 'snippets/authors.html' %}
</td> </td>
<td data-title="{% trans "Shelved" %}"> <td data-title="{% trans "Shelved" %}">
{{ book.created_date|naturalday }} {{ book.shelved_date|naturalday }}
</td> </td>
{% latest_read_through book user as read_through %} {% latest_read_through book user as read_through %}
<td data-title="{% trans "Started" %}"> <td data-title="{% trans "Started" %}">

View file

@ -10,7 +10,14 @@
</a> </a>
</div> </div>
<div class="media-content"> <div class="media-content">
<p>{% if user.name %}{{ user.name }}{% else %}{{ user.localname }}{% endif %}</p> <p>
{% if user.name %}{{ user.name }}{% else %}{{ user.localname }}{% endif %}
{% if user.manually_approves_followers %}
<span class="icon icon-lock is-size-7" title="{% trans 'Locked account' %}">
<span class="is-sr-only">{% trans "Locked account" %}</span>
</span>
{% endif %}
</p>
<p><a href="{{ user.remote_id }}">{{ user.username }}</a></p> <p><a href="{{ user.remote_id }}">{{ user.username }}</a></p>
<p>{% blocktrans with date=user.created_date|naturaltime %}Joined {{ date }}{% endblocktrans %}</p> <p>{% blocktrans with date=user.created_date|naturaltime %}Joined {{ date }}{% endblocktrans %}</p>
<p> <p>
@ -23,7 +30,13 @@
{% mutuals_count user as mutuals %} {% mutuals_count user as mutuals %}
<a href="{% url 'user-followers' user|username %}"> <a href="{% url 'user-followers' user|username %}">
{% if mutuals %}
{% blocktrans with mutuals_display=mutuals|intcomma count counter=mutuals %}{{ mutuals_display }} follower you follow{% plural %}{{ mutuals_display }} followers you follow{% endblocktrans %} {% blocktrans with mutuals_display=mutuals|intcomma count counter=mutuals %}{{ mutuals_display }} follower you follow{% plural %}{{ mutuals_display }} followers you follow{% endblocktrans %}
{% elif request.user in user.following.all %}
{% trans "Follows you" %}
{% else %}
{% trans "No followers you follow" %}
{% endif %}
</a> </a>
{% endif %} {% endif %}

View file

@ -1,17 +1,14 @@
import datetime """test author serializer"""
from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from bookwyrm import models from bookwyrm import models
class Author(TestCase): class Author(TestCase):
def setUp(self): def setUp(self):
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): self.book = models.Edition.objects.create(
self.book = models.Edition.objects.create( title="Example Edition",
title="Example Edition", remote_id="https://example.com/book/1",
remote_id="https://example.com/book/1", )
)
self.author = models.Author.objects.create( self.author = models.Author.objects.create(
name="Author fullname", name="Author fullname",
aliases=["One", "Two"], aliases=["One", "Two"],

View file

@ -20,49 +20,52 @@ from bookwyrm import models
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
@patch("bookwyrm.suggested_users.rerank_user_task.delay")
@patch("bookwyrm.suggested_users.remove_user_task.delay")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
class BaseActivity(TestCase): class BaseActivity(TestCase):
"""the super class for model-linked activitypub dataclasses""" """the super class for model-linked activitypub dataclasses"""
def setUp(self): def setUp(self):
"""we're probably going to re-use this so why copy/paste""" """we're probably going to re-use this so why copy/paste"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
self.user.remote_id = "http://example.com/a/b" self.user.remote_id = "http://example.com/a/b"
self.user.save(broadcast=False) self.user.save(broadcast=False, update_fields=["remote_id"])
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())
# don't try to load the user icon # don't try to load the user icon
del self.userdata["icon"] del self.userdata["icon"]
image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/default_avi.jpg" "../../static/images/default_avi.jpg"
) )
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)
self.image_data = output.getvalue() self.image_data = output.getvalue()
def test_init(self, _): def test_init(self, *_):
"""simple successfuly init""" """simple successfuly init"""
instance = ActivityObject(id="a", type="b") instance = ActivityObject(id="a", type="b")
self.assertTrue(hasattr(instance, "id")) self.assertTrue(hasattr(instance, "id"))
self.assertTrue(hasattr(instance, "type")) self.assertTrue(hasattr(instance, "type"))
def test_init_missing(self, _): def test_init_missing(self, *_):
"""init with missing required params""" """init with missing required params"""
with self.assertRaises(ActivitySerializerError): with self.assertRaises(ActivitySerializerError):
ActivityObject() ActivityObject()
def test_init_extra_fields(self, _): def test_init_extra_fields(self, *_):
"""init ignoring additional fields""" """init ignoring additional fields"""
instance = ActivityObject(id="a", type="b", fish="c") instance = ActivityObject(id="a", type="b", fish="c")
self.assertTrue(hasattr(instance, "id")) self.assertTrue(hasattr(instance, "id"))
self.assertTrue(hasattr(instance, "type")) self.assertTrue(hasattr(instance, "type"))
def test_init_default_field(self, _): def test_init_default_field(self, *_):
"""replace an existing required field with a default field""" """replace an existing required field with a default field"""
@dataclass(init=False) @dataclass(init=False)
@ -75,7 +78,7 @@ class BaseActivity(TestCase):
self.assertEqual(instance.id, "a") self.assertEqual(instance.id, "a")
self.assertEqual(instance.type, "TestObject") self.assertEqual(instance.type, "TestObject")
def test_serialize(self, _): def test_serialize(self, *_):
"""simple function for converting dataclass to dict""" """simple function for converting dataclass to dict"""
instance = ActivityObject(id="a", type="b") instance = ActivityObject(id="a", type="b")
serialized = instance.serialize() serialized = instance.serialize()
@ -84,7 +87,7 @@ class BaseActivity(TestCase):
self.assertEqual(serialized["type"], "b") self.assertEqual(serialized["type"], "b")
@responses.activate @responses.activate
def test_resolve_remote_id(self, _): def test_resolve_remote_id(self, *_):
"""look up or load remote data""" """look up or load remote data"""
# existing item # existing item
result = resolve_remote_id("http://example.com/a/b", model=models.User) result = resolve_remote_id("http://example.com/a/b", model=models.User)
@ -98,23 +101,22 @@ class BaseActivity(TestCase):
status=200, status=200,
) )
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): result = resolve_remote_id(
result = resolve_remote_id( "https://example.com/user/mouse", model=models.User
"https://example.com/user/mouse", model=models.User )
)
self.assertIsInstance(result, models.User) self.assertIsInstance(result, models.User)
self.assertEqual(result.remote_id, "https://example.com/user/mouse") self.assertEqual(result.remote_id, "https://example.com/user/mouse")
self.assertEqual(result.name, "MOUSE?? MOUSE!!") self.assertEqual(result.name, "MOUSE?? MOUSE!!")
def test_to_model_invalid_model(self, _): def test_to_model_invalid_model(self, *_):
"""catch mismatch between activity type and model type""" """catch mismatch between activity type and model type"""
instance = ActivityObject(id="a", type="b") instance = ActivityObject(id="a", type="b")
with self.assertRaises(ActivitySerializerError): with self.assertRaises(ActivitySerializerError):
instance.to_model(model=models.User) instance.to_model(model=models.User)
@responses.activate @responses.activate
def test_to_model_image(self, _): def test_to_model_image(self, *_):
"""update an image field""" """update an image field"""
activity = activitypub.Person( activity = activitypub.Person(
id=self.user.remote_id, id=self.user.remote_id,
@ -141,24 +143,22 @@ 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.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): 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")
self.assertEqual(self.user.key_pair.public_key, "hi") self.assertEqual(self.user.key_pair.public_key, "hi")
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.delay"):
status = models.Status.objects.create( status = models.Status.objects.create(
content="test status", content="test status",
user=self.user, user=self.user,
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): book = models.Edition.objects.create(
book = models.Edition.objects.create( title="Test Edition", remote_id="http://book.com/book"
title="Test Edition", remote_id="http://book.com/book" )
)
update_data = activitypub.Note( update_data = activitypub.Note(
id=status.remote_id, id=status.remote_id,
content=status.content, content=status.content,
@ -180,7 +180,7 @@ class BaseActivity(TestCase):
self.assertEqual(status.mention_books.first(), book) self.assertEqual(status.mention_books.first(), book)
@responses.activate @responses.activate
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.delay"):
@ -219,7 +219,7 @@ class BaseActivity(TestCase):
self.assertIsNone(status.attachments.first()) self.assertIsNone(status.attachments.first())
@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.delay"):
status = models.Status.objects.create( status = models.Status.objects.create(

View file

@ -20,9 +20,8 @@ class Person(TestCase):
def test_user_to_model(self): def test_user_to_model(self):
activity = activitypub.Person(**self.user_data) activity = activitypub.Person(**self.user_data)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): user = activity.to_model(model=models.User)
user = activity.to_model(model=models.User)
self.assertEqual(user.username, "mouse@example.com") self.assertEqual(user.username, "mouse@example.com")
self.assertEqual(user.remote_id, "https://example.com/user/mouse") self.assertEqual(user.remote_id, "https://example.com/user/mouse")
self.assertFalse(user.local) self.assertFalse(user.local)

View file

@ -12,22 +12,20 @@ class Quotation(TestCase):
def setUp(self): def setUp(self):
"""model objects we'll need""" """model objects we'll need"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): self.user = models.User.objects.create_user(
self.user = models.User.objects.create_user( "mouse",
"mouse", "mouse@mouse.mouse",
"mouse@mouse.mouse", "mouseword",
"mouseword", local=False,
local=False, inbox="https://example.com/user/mouse/inbox",
inbox="https://example.com/user/mouse/inbox", outbox="https://example.com/user/mouse/outbox",
outbox="https://example.com/user/mouse/outbox", remote_id="https://example.com/user/mouse",
remote_id="https://example.com/user/mouse",
)
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
) )
self.book = models.Edition.objects.create(
title="Example Edition",
remote_id="https://example.com/book/1",
)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_quotation.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_quotation.json")
self.status_data = json.loads(datafile.read_bytes()) self.status_data = json.loads(datafile.read_bytes())

View file

@ -74,12 +74,11 @@ class AbstractConnector(TestCase):
Mapping("openlibraryKey"), Mapping("openlibraryKey"),
] ]
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): self.book = models.Edition.objects.create(
self.book = models.Edition.objects.create( title="Test Book",
title="Test Book", remote_id="https://example.com/book/1234",
remote_id="https://example.com/book/1234", openlibrary_key="OL1234M",
openlibrary_key="OL1234M", )
)
def test_abstract_connector_init(self): def test_abstract_connector_init(self):
"""barebones connector for search with defaults""" """barebones connector for search with defaults"""
@ -111,11 +110,8 @@ class AbstractConnector(TestCase):
responses.add( responses.add(
responses.GET, "https://example.com/book/abcd", json=self.edition_data responses.GET, "https://example.com/book/abcd", json=self.edition_data
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.connectors.abstract_connector.load_more_data.delay"):
with patch("bookwyrm.connectors.abstract_connector.load_more_data.delay"): result = self.connector.get_or_create_book("https://example.com/book/abcd")
result = self.connector.get_or_create_book(
"https://example.com/book/abcd"
)
self.assertEqual(result, self.book) self.assertEqual(result, self.book)
self.assertEqual(models.Edition.objects.count(), 1) self.assertEqual(models.Edition.objects.count(), 1)
self.assertEqual(models.Edition.objects.count(), 1) self.assertEqual(models.Edition.objects.count(), 1)
@ -123,10 +119,12 @@ class AbstractConnector(TestCase):
@responses.activate @responses.activate
def test_get_or_create_author(self): def test_get_or_create_author(self):
"""load an author""" """load an author"""
self.connector.author_mappings = [ self.connector.author_mappings = (
Mapping("id"), [ # pylint: disable=attribute-defined-outside-init
Mapping("name"), Mapping("id"),
] Mapping("name"),
]
)
responses.add( responses.add(
responses.GET, responses.GET,

View file

@ -1,5 +1,4 @@
""" testing book data connectors """ """ testing book data connectors """
from unittest.mock import patch
import json import json
import pathlib import pathlib
from django.test import TestCase from django.test import TestCase
@ -26,9 +25,8 @@ class BookWyrmConnector(TestCase):
def test_get_or_create_book_existing(self): def test_get_or_create_book_existing(self):
"""load book activity""" """load book activity"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): work = models.Work.objects.create(title="Test Work")
work = models.Work.objects.create(title="Test Work") book = models.Edition.objects.create(title="Test Edition", parent_work=work)
book = models.Edition.objects.create(title="Test Edition", parent_work=work)
result = self.connector.get_or_create_book(book.remote_id) result = self.connector.get_or_create_book(book.remote_id)
self.assertEqual(book, result) self.assertEqual(book, result)

View file

@ -1,5 +1,4 @@
""" interface between the app and various connectors """ """ interface between the app and various connectors """
from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
import responses import responses
@ -14,15 +13,14 @@ class ConnectorManager(TestCase):
def setUp(self): def setUp(self):
"""we'll need some books and a connector info entry""" """we'll need some books and a connector info entry"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): self.work = models.Work.objects.create(title="Example Work")
self.work = models.Work.objects.create(title="Example Work")
self.edition = models.Edition.objects.create( self.edition = models.Edition.objects.create(
title="Example Edition", parent_work=self.work, isbn_10="0000000000" title="Example Edition", parent_work=self.work, isbn_10="0000000000"
) )
self.edition = models.Edition.objects.create( self.edition = models.Edition.objects.create(
title="Another Edition", parent_work=self.work, isbn_10="1111111111" title="Another Edition", parent_work=self.work, isbn_10="1111111111"
) )
self.connector = models.Connector.objects.create( self.connector = models.Connector.objects.create(
identifier="test_connector", identifier="test_connector",

View file

@ -178,26 +178,20 @@ class Openlibrary(TestCase):
@responses.activate @responses.activate
def test_expand_book_data(self): def test_expand_book_data(self):
"""given a book, get more editions""" """given a book, get more editions"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): work = models.Work.objects.create(title="Test Work", openlibrary_key="OL1234W")
work = models.Work.objects.create( edition = models.Edition.objects.create(title="Test Edition", parent_work=work)
title="Test Work", openlibrary_key="OL1234W"
)
edition = models.Edition.objects.create(
title="Test Edition", parent_work=work
)
responses.add( responses.add(
responses.GET, responses.GET,
"https://openlibrary.org/works/OL1234W/editions", "https://openlibrary.org/works/OL1234W/editions",
json={"entries": []}, json={"entries": []},
) )
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch(
with patch( "bookwyrm.connectors.abstract_connector.AbstractConnector."
"bookwyrm.connectors.abstract_connector.AbstractConnector." "create_edition_from_data"
"create_edition_from_data" ):
): self.connector.expand_book_data(edition)
self.connector.expand_book_data(edition) self.connector.expand_book_data(work)
self.connector.expand_book_data(work)
def test_get_description(self): def test_get_description(self):
"""should do some cleanup on the description data""" """should do some cleanup on the description data"""
@ -230,14 +224,11 @@ class Openlibrary(TestCase):
json={"hi": "there"}, json={"hi": "there"},
status=200, status=200,
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch(
with patch( "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data"
"bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data" ) as mock:
) as mock: mock.return_value = []
mock.return_value = [] result = self.connector.create_edition_from_data(work, self.edition_data)
result = self.connector.create_edition_from_data(
work, self.edition_data
)
self.assertEqual(result.parent_work, work) self.assertEqual(result.parent_work, work)
self.assertEqual(result.title, "Sabriel") self.assertEqual(result.title, "Sabriel")
self.assertEqual(result.isbn_10, "0060273224") self.assertEqual(result.isbn_10, "0060273224")

View file

@ -1,5 +1,4 @@
""" testing book data connectors """ """ testing book data connectors """
from unittest.mock import patch
import datetime import datetime
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
@ -30,13 +29,12 @@ class SelfConnector(TestCase):
def test_format_search_result(self): def test_format_search_result(self):
"""create a SearchResult""" """create a SearchResult"""
author = models.Author.objects.create(name="Anonymous") author = models.Author.objects.create(name="Anonymous")
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): edition = models.Edition.objects.create(
edition = models.Edition.objects.create( title="Edition of Example Work",
title="Edition of Example Work", published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc),
published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc), )
) edition.authors.add(author)
edition.authors.add(author) result = self.connector.search("Edition of Example")[0]
result = self.connector.search("Edition of Example")[0]
self.assertEqual(result.title, "Edition of Example Work") self.assertEqual(result.title, "Edition of Example Work")
self.assertEqual(result.key, edition.remote_id) self.assertEqual(result.key, edition.remote_id)
self.assertEqual(result.author, "Anonymous") self.assertEqual(result.author, "Anonymous")
@ -46,65 +44,64 @@ class SelfConnector(TestCase):
def test_search_rank(self): def test_search_rank(self):
"""prioritize certain results""" """prioritize certain results"""
author = models.Author.objects.create(name="Anonymous") author = models.Author.objects.create(name="Anonymous")
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): edition = models.Edition.objects.create(
edition = models.Edition.objects.create( title="Edition of Example Work",
title="Edition of Example Work", published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc),
published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc), parent_work=models.Work.objects.create(title=""),
parent_work=models.Work.objects.create(title=""), )
) # author text is rank B
# author text is rank C edition.authors.add(author)
edition.authors.add(author)
# series is rank D # series is rank D
models.Edition.objects.create( models.Edition.objects.create(
title="Another Edition", title="Another Edition",
series="Anonymous", series="Anonymous",
parent_work=models.Work.objects.create(title=""), parent_work=models.Work.objects.create(title=""),
) )
# subtitle is rank B # subtitle is rank B
models.Edition.objects.create( models.Edition.objects.create(
title="More Editions", title="More Editions",
subtitle="The Anonymous Edition", subtitle="The Anonymous Edition",
parent_work=models.Work.objects.create(title=""), parent_work=models.Work.objects.create(title=""),
) )
# title is rank A # title is rank A
models.Edition.objects.create(title="Anonymous") models.Edition.objects.create(title="Anonymous")
# doesn't rank in this search # doesn't rank in this search
edition = models.Edition.objects.create( models.Edition.objects.create(
title="An Edition", parent_work=models.Work.objects.create(title="") title="An Edition", parent_work=models.Work.objects.create(title="")
) )
results = self.connector.search("Anonymous") results = self.connector.search("Anonymous")
self.assertEqual(len(results), 3) self.assertEqual(len(results), 4)
self.assertEqual(results[0].title, "Anonymous") self.assertEqual(results[0].title, "Anonymous")
self.assertEqual(results[1].title, "More Editions") self.assertEqual(results[1].title, "More Editions")
self.assertEqual(results[2].title, "Edition of Example Work") self.assertEqual(results[2].title, "Edition of Example Work")
self.assertEqual(results[3].title, "Another Edition")
def test_search_multiple_editions(self): def test_search_multiple_editions(self):
"""it should get rid of duplicate editions for the same work""" """it should get rid of duplicate editions for the same work"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): work = models.Work.objects.create(title="Work Title")
work = models.Work.objects.create(title="Work Title") edition_1 = models.Edition.objects.create(
edition_1 = models.Edition.objects.create( title="Edition 1 Title", parent_work=work
title="Edition 1 Title", parent_work=work )
) edition_2 = models.Edition.objects.create(
edition_2 = models.Edition.objects.create( title="Edition 2 Title",
title="Edition 2 Title", parent_work=work,
parent_work=work, isbn_13="123456789", # this is now the defualt edition
edition_rank=20, # that's default babey )
) edition_3 = models.Edition.objects.create(title="Fish", parent_work=work)
edition_3 = models.Edition.objects.create(title="Fish", parent_work=work)
# pick the best edition # pick the best edition
results = self.connector.search("Edition 1 Title") results = self.connector.search("Edition 1 Title")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertEqual(results[0].key, edition_1.remote_id) self.assertEqual(results[0].key, edition_1.remote_id)
# pick the default edition when no match is best # pick the default edition when no match is best
results = self.connector.search("Edition Title") results = self.connector.search("Edition Title")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertEqual(results[0].key, edition_2.remote_id) self.assertEqual(results[0].key, edition_2.remote_id)
# only matches one edition, so no deduplication takes place # only matches one edition, so no deduplication takes place
results = self.connector.search("Fish") results = self.connector.search("Fish")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertEqual(results[0].key, edition_3.remote_id) self.assertEqual(results[0].key, edition_3.remote_id)

View file

@ -28,7 +28,7 @@
}, },
"bookwyrmUser": true, "bookwyrmUser": true,
"manuallyApprovesFollowers": false, "manuallyApprovesFollowers": false,
"discoverable": true, "discoverable": false,
"devices": "https://friend.camp/users/tripofmice/collections/devices", "devices": "https://friend.camp/users/tripofmice/collections/devices",
"tag": [], "tag": [],
"icon": { "icon": {

View file

@ -3,6 +3,8 @@ from collections import namedtuple
import csv import csv
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
import datetime
import pytz
from django.test import TestCase from django.test import TestCase
import responses import responses
@ -13,6 +15,13 @@ from bookwyrm.importers.importer import import_data, handle_imported_book
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
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")
class GoodreadsImport(TestCase): class GoodreadsImport(TestCase):
"""importing from goodreads csv""" """importing from goodreads csv"""
@ -21,7 +30,7 @@ class GoodreadsImport(TestCase):
self.importer = GoodreadsImporter() self.importer = GoodreadsImporter()
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
self.csv = open(datafile, "r", encoding=self.importer.encoding) self.csv = open(datafile, "r", encoding=self.importer.encoding)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True "mouse", "mouse@mouse.mouse", "password", local=True
) )
@ -37,15 +46,14 @@ class GoodreadsImport(TestCase):
search_url="https://%s/search?q=" % DOMAIN, search_url="https://%s/search?q=" % DOMAIN,
priority=1, priority=1,
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): work = models.Work.objects.create(title="Test Work")
work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create(
self.book = models.Edition.objects.create( title="Example Edition",
title="Example Edition", remote_id="https://example.com/book/1",
remote_id="https://example.com/book/1", parent_work=work,
parent_work=work, )
)
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.user, self.csv, False, "public")
self.assertEqual(import_job.user, self.user) self.assertEqual(import_job.user, self.user)
@ -61,7 +69,7 @@ class GoodreadsImport(TestCase):
self.assertEqual(import_items[2].index, 2) self.assertEqual(import_items[2].index, 2)
self.assertEqual(import_items[2].data["Book Id"], "28694510") self.assertEqual(import_items[2].data["Book Id"], "28694510")
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.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]
@ -79,7 +87,7 @@ 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): def test_start_import(self, _):
"""begin loading books""" """begin loading books"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
MockTask = namedtuple("Task", ("id")) MockTask = namedtuple("Task", ("id"))
@ -91,11 +99,10 @@ class GoodreadsImport(TestCase):
self.assertEqual(import_job.task_id, "7") self.assertEqual(import_job.task_id, "7")
@responses.activate @responses.activate
def test_import_data(self): def test_import_data(self, _):
"""resolve entry""" """resolve entry"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): book = models.Edition.objects.create(title="Test Book")
book = models.Edition.objects.create(title="Test Book")
with patch( with patch(
"bookwyrm.models.import_job.ImportItem.get_book_from_isbn" "bookwyrm.models.import_job.ImportItem.get_book_from_isbn"
@ -107,46 +114,11 @@ class GoodreadsImport(TestCase):
import_item = models.ImportItem.objects.get(job=import_job, index=0) import_item = models.ImportItem.objects.get(job=import_job, index=0)
self.assertEqual(import_item.book.id, book.id) 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.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)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
csv_file = open(datafile, "r")
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.preview_images.generate_edition_preview_image_task.delay"):
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)
readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up.
self.assertEqual(readthrough.start_date.year, 2020)
self.assertEqual(readthrough.start_date.month, 10)
self.assertEqual(readthrough.start_date.day, 21)
self.assertEqual(readthrough.finish_date.year, 2020)
self.assertEqual(readthrough.finish_date.month, 10)
self.assertEqual(readthrough.finish_date.day, 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)
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
csv_file = open(datafile, "r") csv_file = open(datafile, "r")
@ -164,17 +136,54 @@ class GoodreadsImport(TestCase):
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.assertEqual(
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.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date.year, 2020) self.assertEqual(readthrough.start_date, make_date(2020, 10, 21))
self.assertEqual(readthrough.start_date.month, 10) self.assertEqual(readthrough.finish_date, make_date(2020, 10, 25))
self.assertEqual(readthrough.start_date.day, 21)
self.assertEqual(readthrough.finish_date.year, 2020)
self.assertEqual(readthrough.finish_date.month, 10)
self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_import_twice(self): 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")
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""" """re-importing books"""
shelf = self.user.shelf_set.filter(identifier="read").first() shelf = self.user.shelf_set.filter(identifier="read").first()
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
@ -187,30 +196,27 @@ class GoodreadsImport(TestCase):
) )
break break
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, False, "public"
self.importer.service, self.user, import_item, False, "public" )
) handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, False, "public"
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)
self.assertEqual(
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.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up. self.assertEqual(readthrough.start_date, make_date(2020, 10, 21))
self.assertEqual(readthrough.start_date.year, 2020) self.assertEqual(readthrough.finish_date, make_date(2020, 10, 25))
self.assertEqual(readthrough.start_date.month, 10)
self.assertEqual(readthrough.start_date.day, 21)
self.assertEqual(readthrough.finish_date.year, 2020)
self.assertEqual(readthrough.finish_date.month, 10)
self.assertEqual(readthrough.finish_date.day, 25)
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
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 = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
@ -221,21 +227,18 @@ class GoodreadsImport(TestCase):
job_id=import_job.id, index=0, data=entry, book=self.book job_id=import_job.id, index=0, data=entry, book=self.book
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, True, "unlisted"
self.importer.service, self.user, import_item, True, "unlisted" )
)
review = models.Review.objects.get(book=self.book, user=self.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.year, 2019) self.assertEqual(review.published_date, make_date(2019, 7, 8))
self.assertEqual(review.published_date.month, 7)
self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, "unlisted") self.assertEqual(review.privacy, "unlisted")
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
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 = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath( datafile = pathlib.Path(__file__).parent.joinpath(
@ -248,20 +251,17 @@ class GoodreadsImport(TestCase):
job_id=import_job.id, index=0, data=entry, book=self.book job_id=import_job.id, index=0, data=entry, book=self.book
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, True, "unlisted"
self.importer.service, self.user, import_item, True, "unlisted" )
)
review = models.ReviewRating.objects.get(book=self.book, user=self.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, 2)
self.assertEqual(review.published_date.year, 2019) self.assertEqual(review.published_date, make_date(2019, 7, 8))
self.assertEqual(review.published_date.month, 7)
self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, "unlisted") self.assertEqual(review.privacy, "unlisted")
def test_handle_imported_book_reviews_disabled(self): def test_handle_imported_book_reviews_disabled(self, _):
"""goodreads review import""" """goodreads review import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
@ -272,11 +272,10 @@ class GoodreadsImport(TestCase):
job_id=import_job.id, index=0, data=entry, book=self.book job_id=import_job.id, index=0, data=entry, book=self.book
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, False, "unlisted"
self.importer.service, self.user, import_item, False, "unlisted" )
)
self.assertFalse( self.assertFalse(
models.Review.objects.filter(book=self.book, user=self.user).exists() models.Review.objects.filter(book=self.book, user=self.user).exists()
) )

View file

@ -2,6 +2,8 @@
import csv import csv
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
import datetime
import pytz
from django.test import TestCase from django.test import TestCase
import responses import responses
@ -12,6 +14,13 @@ from bookwyrm.importers.importer import import_data, handle_imported_book
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
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")
class LibrarythingImport(TestCase): class LibrarythingImport(TestCase):
"""importing from librarything tsv""" """importing from librarything tsv"""
@ -22,7 +31,7 @@ class LibrarythingImport(TestCase):
# Librarything generates latin encoded exports... # Librarything generates latin encoded exports...
self.csv = open(datafile, "r", encoding=self.importer.encoding) self.csv = open(datafile, "r", encoding=self.importer.encoding)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mmai", "mmai@mmai.mmai", "password", local=True "mmai", "mmai@mmai.mmai", "password", local=True
) )
@ -38,15 +47,14 @@ class LibrarythingImport(TestCase):
search_url="https://%s/search?q=" % DOMAIN, search_url="https://%s/search?q=" % DOMAIN,
priority=1, priority=1,
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): work = models.Work.objects.create(title="Test Work")
work = models.Work.objects.create(title="Test Work") self.book = models.Edition.objects.create(
self.book = models.Edition.objects.create( title="Example Edition",
title="Example Edition", remote_id="https://example.com/book/1",
remote_id="https://example.com/book/1", parent_work=work,
parent_work=work, )
)
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.user, self.csv, False, "public")
self.assertEqual(import_job.user, self.user) self.assertEqual(import_job.user, self.user)
@ -62,7 +70,7 @@ class LibrarythingImport(TestCase):
self.assertEqual(import_items[2].index, 2) self.assertEqual(import_items[2].index, 2)
self.assertEqual(import_items[2].data["Book Id"], "5015399") self.assertEqual(import_items[2].data["Book Id"], "5015399")
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.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]
@ -81,11 +89,10 @@ class LibrarythingImport(TestCase):
self.assertEqual(retry_items[1].data["Book Id"], "5015319") self.assertEqual(retry_items[1].data["Book Id"], "5015319")
@responses.activate @responses.activate
def test_import_data(self): def test_import_data(self, _):
"""resolve entry""" """resolve entry"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): book = models.Edition.objects.create(title="Test Book")
book = models.Edition.objects.create(title="Test Book")
with patch( with patch(
"bookwyrm.models.import_job.ImportItem.get_book_from_isbn" "bookwyrm.models.import_job.ImportItem.get_book_from_isbn"
@ -97,7 +104,7 @@ class LibrarythingImport(TestCase):
import_item = models.ImportItem.objects.get(job=import_job, index=0) import_item = models.ImportItem.objects.get(job=import_job, index=0)
self.assertEqual(import_item.book.id, book.id) 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.user.shelf_set.filter(identifier="read").first()
self.assertIsNone(shelf.books.first()) self.assertIsNone(shelf.books.first())
@ -114,26 +121,20 @@ class LibrarythingImport(TestCase):
) )
break break
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, False, "public"
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.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up. self.assertEqual(readthrough.start_date, make_date(2007, 4, 16))
self.assertEqual(readthrough.start_date.year, 2007) self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8))
self.assertEqual(readthrough.start_date.month, 4)
self.assertEqual(readthrough.start_date.day, 16)
self.assertEqual(readthrough.finish_date.year, 2007)
self.assertEqual(readthrough.finish_date.month, 5)
self.assertEqual(readthrough.finish_date.day, 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.delay"):
shelf = self.user.shelf_set.filter(identifier="to-read").first() shelf = self.user.shelf_set.filter(identifier="to-read").first()
@ -151,25 +152,21 @@ class LibrarythingImport(TestCase):
) )
break break
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, False, "public"
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)
self.assertIsNone(self.user.shelf_set.get(identifier="read").books.first()) self.assertIsNone(self.user.shelf_set.get(identifier="read").books.first())
readthrough = models.ReadThrough.objects.get(user=self.user) 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.year, 2007) self.assertEqual(readthrough.start_date, make_date(2007, 4, 16))
self.assertEqual(readthrough.start_date.month, 4) self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8))
self.assertEqual(readthrough.start_date.day, 16)
self.assertEqual(readthrough.finish_date.year, 2007)
self.assertEqual(readthrough.finish_date.month, 5)
self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_import_twice(self): def test_handle_import_twice(self, _):
"""re-importing books""" """re-importing books"""
shelf = self.user.shelf_set.filter(identifier="read").first() shelf = self.user.shelf_set.filter(identifier="read").first()
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
@ -184,30 +181,24 @@ class LibrarythingImport(TestCase):
) )
break break
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, False, "public"
self.importer.service, self.user, import_item, False, "public" )
) handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, False, "public"
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.user)
self.assertEqual(readthrough.book, self.book) self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up. self.assertEqual(readthrough.start_date, make_date(2007, 4, 16))
self.assertEqual(readthrough.start_date.year, 2007) self.assertEqual(readthrough.finish_date, make_date(2007, 5, 8))
self.assertEqual(readthrough.start_date.month, 4)
self.assertEqual(readthrough.start_date.day, 16)
self.assertEqual(readthrough.finish_date.year, 2007)
self.assertEqual(readthrough.finish_date.month, 5)
self.assertEqual(readthrough.finish_date.day, 8)
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
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 = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
@ -218,20 +209,17 @@ class LibrarythingImport(TestCase):
job_id=import_job.id, index=0, data=entry, book=self.book job_id=import_job.id, index=0, data=entry, book=self.book
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, True, "unlisted"
self.importer.service, self.user, import_item, True, "unlisted" )
)
review = models.Review.objects.get(book=self.book, user=self.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, 5)
self.assertEqual(review.published_date.year, 2007) self.assertEqual(review.published_date, make_date(2007, 5, 8))
self.assertEqual(review.published_date.month, 5)
self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, "unlisted") self.assertEqual(review.privacy, "unlisted")
def test_handle_imported_book_reviews_disabled(self): def test_handle_imported_book_reviews_disabled(self, _):
"""librarything review import""" """librarything review import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
@ -242,11 +230,10 @@ class LibrarythingImport(TestCase):
job_id=import_job.id, index=0, data=entry, book=self.book job_id=import_job.id, index=0, data=entry, book=self.book
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): handle_imported_book(
handle_imported_book( self.importer.service, self.user, import_item, False, "unlisted"
self.importer.service, self.user, import_item, False, "unlisted" )
)
self.assertFalse( self.assertFalse(
models.Review.objects.filter(book=self.book, user=self.user).exists() models.Review.objects.filter(book=self.book, user=self.user).exists()
) )

View file

@ -12,7 +12,7 @@ class Activitystreams(TestCase):
def setUp(self): def setUp(self):
"""we need some stuff""" """we need some stuff"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
) )
@ -23,18 +23,17 @@ class Activitystreams(TestCase):
local=True, local=True,
localname="nutria", localname="nutria",
) )
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): self.book = models.Edition.objects.create(title="test book")
self.book = models.Edition.objects.create(title="test book")
def test_populate_streams(self, _): def test_populate_streams(self, _):
"""make sure the function on the redis manager gets called""" """make sure the function on the redis manager gets called"""

View file

@ -9,32 +9,40 @@ from django.test import TestCase
from bookwyrm.activitypub.base_activity import ActivityObject from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm import models from bookwyrm import models
from bookwyrm.models import base_model from bookwyrm.models import base_model
from bookwyrm.models.activitypub_mixin import ActivitypubMixin from bookwyrm.models.activitypub_mixin import (
from bookwyrm.models.activitypub_mixin import ActivityMixin, ObjectMixin ActivitypubMixin,
ActivityMixin,
ObjectMixin,
OrderedCollectionMixin,
to_ordered_collection_page,
)
from bookwyrm.settings import PAGE_LENGTH
# pylint: disable=invalid-name
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class ActivitypubMixins(TestCase): class ActivitypubMixins(TestCase):
"""functionality shared across models""" """functionality shared across models"""
def setUp(self): def setUp(self):
"""shared data""" """shared data"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
) )
self.local_user.remote_id = "http://example.com/a/b" self.local_user.remote_id = "http://example.com/a/b"
self.local_user.save(broadcast=False) self.local_user.save(broadcast=False, update_fields=["remote_id"])
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
self.object_mock = { self.object_mock = {
"to": "to field", "to": "to field",
@ -45,8 +53,7 @@ class ActivitypubMixins(TestCase):
"published": "2020-12-04T17:52:22.623807+00:00", "published": "2020-12-04T17:52:22.623807+00:00",
} }
# ActivitypubMixin def test_to_activity(self, *_):
def test_to_activity(self, _):
"""model to ActivityPub json""" """model to ActivityPub json"""
@dataclass(init=False) @dataclass(init=False)
@ -67,26 +74,24 @@ class ActivitypubMixins(TestCase):
self.assertEqual(activity["id"], "https://www.example.com/test") self.assertEqual(activity["id"], "https://www.example.com/test")
self.assertEqual(activity["type"], "Test") self.assertEqual(activity["type"], "Test")
def test_find_existing_by_remote_id(self, _): def test_find_existing_by_remote_id(self, *_):
"""attempt to match a remote id to an object in the db""" """attempt to match a remote id to an object in the db"""
# uses a different remote id scheme # uses a different remote id scheme
# this isn't really part of this test directly but it's helpful to state # this isn't really part of this test directly but it's helpful to state
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): book = models.Edition.objects.create(
book = models.Edition.objects.create( title="Test Edition", remote_id="http://book.com/book"
title="Test Edition", remote_id="http://book.com/book" )
)
self.assertEqual(book.origin_id, "http://book.com/book") self.assertEqual(book.origin_id, "http://book.com/book")
self.assertNotEqual(book.remote_id, "http://book.com/book") self.assertNotEqual(book.remote_id, "http://book.com/book")
# uses subclasses # uses subclasses
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.Comment.objects.create(
models.Comment.objects.create( user=self.local_user,
user=self.local_user, content="test status",
content="test status", book=book,
book=book, remote_id="https://comment.net",
remote_id="https://comment.net", )
)
result = models.User.find_existing_by_remote_id("hi") result = models.User.find_existing_by_remote_id("hi")
self.assertIsNone(result) self.assertIsNone(result)
@ -101,18 +106,17 @@ class ActivitypubMixins(TestCase):
# test subclass match # test subclass match
result = models.Status.find_existing_by_remote_id("https://comment.net") result = models.Status.find_existing_by_remote_id("https://comment.net")
def test_find_existing(self, _): def test_find_existing(self, *_):
"""match a blob of data to a model""" """match a blob of data to a model"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): book = models.Edition.objects.create(
book = models.Edition.objects.create( title="Test edition",
title="Test edition", openlibrary_key="OL1234",
openlibrary_key="OL1234", )
)
result = models.Edition.find_existing({"openlibraryKey": "OL1234"}) result = models.Edition.find_existing({"openlibraryKey": "OL1234"})
self.assertEqual(result, book) self.assertEqual(result, book)
def test_get_recipients_public_object(self, _): def test_get_recipients_public_object(self, *_):
"""determines the recipients for an object's broadcast""" """determines the recipients for an object's broadcast"""
MockSelf = namedtuple("Self", ("privacy")) MockSelf = namedtuple("Self", ("privacy"))
mock_self = MockSelf("public") mock_self = MockSelf("public")
@ -120,7 +124,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(len(recipients), 1) self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], self.remote_user.inbox) self.assertEqual(recipients[0], self.remote_user.inbox)
def test_get_recipients_public_user_object_no_followers(self, _): def test_get_recipients_public_user_object_no_followers(self, *_):
"""determines the recipients for a user's object broadcast""" """determines the recipients for a user's object broadcast"""
MockSelf = namedtuple("Self", ("privacy", "user")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
@ -128,7 +132,7 @@ class ActivitypubMixins(TestCase):
recipients = ActivitypubMixin.get_recipients(mock_self) recipients = ActivitypubMixin.get_recipients(mock_self)
self.assertEqual(len(recipients), 0) self.assertEqual(len(recipients), 0)
def test_get_recipients_public_user_object(self, _): def test_get_recipients_public_user_object(self, *_):
"""determines the recipients for a user's object broadcast""" """determines the recipients for a user's object broadcast"""
MockSelf = namedtuple("Self", ("privacy", "user")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
@ -138,22 +142,21 @@ class ActivitypubMixins(TestCase):
self.assertEqual(len(recipients), 1) self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], self.remote_user.inbox) self.assertEqual(recipients[0], self.remote_user.inbox)
def test_get_recipients_public_user_object_with_mention(self, _): def test_get_recipients_public_user_object_with_mention(self, *_):
"""determines the recipients for a user's object broadcast""" """determines the recipients for a user's object broadcast"""
MockSelf = namedtuple("Self", ("privacy", "user")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): another_remote_user = models.User.objects.create_user(
another_remote_user = models.User.objects.create_user( "nutria",
"nutria", "nutria@nutria.com",
"nutria@nutria.com", "nutriaword",
"nutriaword", local=False,
local=False, remote_id="https://example.com/users/nutria",
remote_id="https://example.com/users/nutria", inbox="https://example.com/users/nutria/inbox",
inbox="https://example.com/users/nutria/inbox", outbox="https://example.com/users/nutria/outbox",
outbox="https://example.com/users/nutria/outbox", )
)
MockSelf = namedtuple("Self", ("privacy", "user", "recipients")) MockSelf = namedtuple("Self", ("privacy", "user", "recipients"))
mock_self = MockSelf("public", self.local_user, [another_remote_user]) mock_self = MockSelf("public", self.local_user, [another_remote_user])
@ -162,22 +165,21 @@ class ActivitypubMixins(TestCase):
self.assertTrue(another_remote_user.inbox in recipients) self.assertTrue(another_remote_user.inbox in recipients)
self.assertTrue(self.remote_user.inbox in recipients) self.assertTrue(self.remote_user.inbox in recipients)
def test_get_recipients_direct(self, _): def test_get_recipients_direct(self, *_):
"""determines the recipients for a user's object broadcast""" """determines the recipients for a user's object broadcast"""
MockSelf = namedtuple("Self", ("privacy", "user")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): another_remote_user = models.User.objects.create_user(
another_remote_user = models.User.objects.create_user( "nutria",
"nutria", "nutria@nutria.com",
"nutria@nutria.com", "nutriaword",
"nutriaword", local=False,
local=False, remote_id="https://example.com/users/nutria",
remote_id="https://example.com/users/nutria", inbox="https://example.com/users/nutria/inbox",
inbox="https://example.com/users/nutria/inbox", outbox="https://example.com/users/nutria/outbox",
outbox="https://example.com/users/nutria/outbox", )
)
MockSelf = namedtuple("Self", ("privacy", "user", "recipients")) MockSelf = namedtuple("Self", ("privacy", "user", "recipients"))
mock_self = MockSelf("direct", self.local_user, [another_remote_user]) mock_self = MockSelf("direct", self.local_user, [another_remote_user])
@ -185,22 +187,21 @@ class ActivitypubMixins(TestCase):
self.assertEqual(len(recipients), 1) self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], another_remote_user.inbox) self.assertEqual(recipients[0], another_remote_user.inbox)
def test_get_recipients_combine_inboxes(self, _): def test_get_recipients_combine_inboxes(self, *_):
"""should combine users with the same shared_inbox""" """should combine users with the same shared_inbox"""
self.remote_user.shared_inbox = "http://example.com/inbox" self.remote_user.shared_inbox = "http://example.com/inbox"
self.remote_user.save(broadcast=False) self.remote_user.save(broadcast=False, update_fields=["shared_inbox"])
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): another_remote_user = models.User.objects.create_user(
another_remote_user = models.User.objects.create_user( "nutria",
"nutria", "nutria@nutria.com",
"nutria@nutria.com", "nutriaword",
"nutriaword", local=False,
local=False, remote_id="https://example.com/users/nutria",
remote_id="https://example.com/users/nutria", inbox="https://example.com/users/nutria/inbox",
inbox="https://example.com/users/nutria/inbox", shared_inbox="http://example.com/inbox",
shared_inbox="http://example.com/inbox", outbox="https://example.com/users/nutria/outbox",
outbox="https://example.com/users/nutria/outbox", )
)
MockSelf = namedtuple("Self", ("privacy", "user")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
@ -210,20 +211,19 @@ class ActivitypubMixins(TestCase):
self.assertEqual(len(recipients), 1) self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0], "http://example.com/inbox") self.assertEqual(recipients[0], "http://example.com/inbox")
def test_get_recipients_software(self, _): def test_get_recipients_software(self, *_):
"""should differentiate between bookwyrm and other remote users""" """should differentiate between bookwyrm and other remote users"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): another_remote_user = models.User.objects.create_user(
another_remote_user = models.User.objects.create_user( "nutria",
"nutria", "nutria@nutria.com",
"nutria@nutria.com", "nutriaword",
"nutriaword", local=False,
local=False, remote_id="https://example.com/users/nutria",
remote_id="https://example.com/users/nutria", inbox="https://example.com/users/nutria/inbox",
inbox="https://example.com/users/nutria/inbox", outbox="https://example.com/users/nutria/outbox",
outbox="https://example.com/users/nutria/outbox", bookwyrm_user=False,
bookwyrm_user=False, )
)
MockSelf = namedtuple("Self", ("privacy", "user")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
@ -241,7 +241,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], another_remote_user.inbox) self.assertEqual(recipients[0], another_remote_user.inbox)
# ObjectMixin # ObjectMixin
def test_object_save_create(self, _): def test_object_save_create(self, *_):
"""should save uneventufully when broadcast is disabled""" """should save uneventufully when broadcast is disabled"""
class Success(Exception): class Success(Exception):
@ -272,7 +272,7 @@ class ActivitypubMixins(TestCase):
ObjectModel(user=self.local_user).save(broadcast=False) ObjectModel(user=self.local_user).save(broadcast=False)
ObjectModel(user=None).save() ObjectModel(user=None).save()
def test_object_save_update(self, _): def test_object_save_update(self, *_):
"""should save uneventufully when broadcast is disabled""" """should save uneventufully when broadcast is disabled"""
class Success(Exception): class Success(Exception):
@ -298,7 +298,7 @@ class ActivitypubMixins(TestCase):
with self.assertRaises(Success): with self.assertRaises(Success):
UpdateObjectModel(id=1, last_edited_by=self.local_user).save() UpdateObjectModel(id=1, last_edited_by=self.local_user).save()
def test_object_save_delete(self, _): def test_object_save_delete(self, *_):
"""should create delete activities when objects are deleted by flag""" """should create delete activities when objects are deleted by flag"""
class ActivitySuccess(Exception): class ActivitySuccess(Exception):
@ -320,7 +320,7 @@ class ActivitypubMixins(TestCase):
with self.assertRaises(ActivitySuccess): with self.assertRaises(ActivitySuccess):
DeletableObjectModel(id=1, user=self.local_user, deleted=True).save() DeletableObjectModel(id=1, user=self.local_user, deleted=True).save()
def test_to_delete_activity(self, _): def test_to_delete_activity(self, *_):
"""wrapper for Delete activity""" """wrapper for Delete activity"""
MockSelf = namedtuple("Self", ("remote_id", "to_activity")) MockSelf = namedtuple("Self", ("remote_id", "to_activity"))
mock_self = MockSelf( mock_self = MockSelf(
@ -335,7 +335,7 @@ class ActivitypubMixins(TestCase):
activity["cc"], ["https://www.w3.org/ns/activitystreams#Public"] activity["cc"], ["https://www.w3.org/ns/activitystreams#Public"]
) )
def test_to_update_activity(self, _): def test_to_update_activity(self, *_):
"""ditto above but for Update""" """ditto above but for Update"""
MockSelf = namedtuple("Self", ("remote_id", "to_activity")) MockSelf = namedtuple("Self", ("remote_id", "to_activity"))
mock_self = MockSelf( mock_self = MockSelf(
@ -352,8 +352,7 @@ class ActivitypubMixins(TestCase):
) )
self.assertIsInstance(activity["object"], dict) self.assertIsInstance(activity["object"], dict)
# Activity mixin def test_to_undo_activity(self, *_):
def test_to_undo_activity(self, _):
"""and again, for Undo""" """and again, for Undo"""
MockSelf = namedtuple("Self", ("remote_id", "to_activity", "user")) MockSelf = namedtuple("Self", ("remote_id", "to_activity", "user"))
mock_self = MockSelf( mock_self = MockSelf(
@ -366,3 +365,57 @@ class ActivitypubMixins(TestCase):
self.assertEqual(activity["actor"], self.local_user.remote_id) self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["type"], "Undo") self.assertEqual(activity["type"], "Undo")
self.assertIsInstance(activity["object"], dict) self.assertIsInstance(activity["object"], dict)
def test_to_ordered_collection_page(self, *_):
"""make sure the paged results of an ordered collection work"""
self.assertEqual(PAGE_LENGTH, 15)
for number in range(0, 2 * PAGE_LENGTH):
models.Status.objects.create(
user=self.local_user,
content="test status {:d}".format(number),
)
page_1 = to_ordered_collection_page(
models.Status.objects.all(), "http://fish.com/", page=1
)
self.assertEqual(page_1.partOf, "http://fish.com/")
self.assertEqual(page_1.id, "http://fish.com/?page=1")
self.assertEqual(page_1.next, "http://fish.com/?page=2")
self.assertEqual(page_1.orderedItems[0]["content"], "test status 29")
self.assertEqual(page_1.orderedItems[1]["content"], "test status 28")
page_2 = to_ordered_collection_page(
models.Status.objects.all(), "http://fish.com/", page=2
)
self.assertEqual(page_2.partOf, "http://fish.com/")
self.assertEqual(page_2.id, "http://fish.com/?page=2")
self.assertEqual(page_2.orderedItems[0]["content"], "test status 14")
self.assertEqual(page_2.orderedItems[-1]["content"], "test status 0")
def test_to_ordered_collection(self, *_):
"""convert a queryset into an ordered collection object"""
self.assertEqual(PAGE_LENGTH, 15)
for number in range(0, 2 * PAGE_LENGTH):
models.Status.objects.create(
user=self.local_user,
content="test status {:d}".format(number),
)
MockSelf = namedtuple("Self", ("remote_id"))
mock_self = MockSelf("")
collection = OrderedCollectionMixin.to_ordered_collection(
mock_self, models.Status.objects.all(), remote_id="http://fish.com/"
)
self.assertEqual(collection.totalItems, 30)
self.assertEqual(collection.first, "http://fish.com/?page=1")
self.assertEqual(collection.last, "http://fish.com/?page=2")
page_2 = OrderedCollectionMixin.to_ordered_collection(
mock_self, models.Status.objects.all(), remote_id="http://fish.com/", page=2
)
self.assertEqual(page_2.partOf, "http://fish.com/")
self.assertEqual(page_2.id, "http://fish.com/?page=2")
self.assertEqual(page_2.orderedItems[0]["content"], "test status 14")
self.assertEqual(page_2.orderedItems[-1]["content"], "test status 0")

View file

@ -7,25 +7,26 @@ from bookwyrm.models import base_model
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
# pylint: disable=attribute-defined-outside-init
class BaseModel(TestCase): class BaseModel(TestCase):
"""functionality shared across models""" """functionality shared across models"""
def setUp(self): def setUp(self):
"""shared data""" """shared data"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
) )
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
class BookWyrmTestModel(base_model.BookWyrmModel): class BookWyrmTestModel(base_model.BookWyrmModel):
"""just making it not abstract""" """just making it not abstract"""

View file

@ -2,7 +2,6 @@
from dateutil.parser import parse from dateutil.parser import parse
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from unittest.mock import patch
from bookwyrm import models, settings from bookwyrm import models, settings
from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10 from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10
@ -13,18 +12,17 @@ class Book(TestCase):
def setUp(self): def setUp(self):
"""we'll need some books""" """we'll need some books"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): self.work = models.Work.objects.create(
self.work = models.Work.objects.create( title="Example Work", remote_id="https://example.com/book/1"
title="Example Work", remote_id="https://example.com/book/1" )
) self.first_edition = models.Edition.objects.create(
self.first_edition = models.Edition.objects.create( title="Example Edition",
title="Example Edition", parent_work=self.work,
parent_work=self.work, )
) self.second_edition = models.Edition.objects.create(
self.second_edition = models.Edition.objects.create( title="Another Example Edition",
title="Another Example Edition", parent_work=self.work,
parent_work=self.work, )
)
def test_remote_id(self): def test_remote_id(self):
"""fanciness with remote/origin ids""" """fanciness with remote/origin ids"""
@ -58,8 +56,7 @@ class Book(TestCase):
def test_get_edition_info(self): def test_get_edition_info(self):
"""text slug about an edition""" """text slug about an edition"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): book = models.Edition.objects.create(title="Test Edition")
book = models.Edition.objects.create(title="Test Edition")
self.assertEqual(book.edition_info, "") self.assertEqual(book.edition_info, "")
book.physical_format = "worm" book.physical_format = "worm"

View file

@ -11,30 +11,29 @@ class FederatedServer(TestCase):
def setUp(self): def setUp(self):
"""we'll need a user""" """we'll need a user"""
self.server = models.FederatedServer.objects.create(server_name="test.server") self.server = models.FederatedServer.objects.create(server_name="test.server")
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user(
self.remote_user = models.User.objects.create_user( "rat",
"rat", "rat@rat.com",
"rat@rat.com", "ratword",
"ratword", federated_server=self.server,
federated_server=self.server, local=False,
local=False, remote_id="https://example.com/users/rat",
remote_id="https://example.com/users/rat", inbox="https://example.com/users/rat/inbox",
inbox="https://example.com/users/rat/inbox", outbox="https://example.com/users/rat/outbox",
outbox="https://example.com/users/rat/outbox", )
) self.inactive_remote_user = models.User.objects.create_user(
self.inactive_remote_user = models.User.objects.create_user( "nutria",
"nutria", "nutria@nutria.com",
"nutria@nutria.com", "nutriaword",
"nutriaword", federated_server=self.server,
federated_server=self.server, local=False,
local=False, remote_id="https://example.com/users/nutria",
remote_id="https://example.com/users/nutria", inbox="https://example.com/users/nutria/inbox",
inbox="https://example.com/users/nutria/inbox", outbox="https://example.com/users/nutria/outbox",
outbox="https://example.com/users/nutria/outbox", is_active=False,
is_active=False, deactivation_reason="self_deletion",
deactivation_reason="self_deletion", )
)
def test_block_unblock(self): def test_block_unblock(self):
"""block a server and all users on it""" """block a server and all users on it"""

View file

@ -24,10 +24,11 @@ from bookwyrm.models.base_model import BookWyrmModel
from bookwyrm.models.activitypub_mixin import ActivitypubMixin from bookwyrm.models.activitypub_mixin import ActivitypubMixin
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
class ActivitypubFields(TestCase): @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
class ModelFields(TestCase):
"""overwrites standard model feilds to work with activitypub""" """overwrites standard model feilds to work with activitypub"""
def test_validate_remote_id(self): def test_validate_remote_id(self, _):
"""should look like a url""" """should look like a url"""
self.assertIsNone(fields.validate_remote_id("http://www.example.com")) self.assertIsNone(fields.validate_remote_id("http://www.example.com"))
self.assertIsNone(fields.validate_remote_id("https://www.example.com")) self.assertIsNone(fields.validate_remote_id("https://www.example.com"))
@ -44,7 +45,7 @@ class ActivitypubFields(TestCase):
"http://www.example.com/dlfjg 23/x", "http://www.example.com/dlfjg 23/x",
) )
def test_activitypub_field_mixin(self): def test_activitypub_field_mixin(self, _):
"""generic mixin with super basic to and from functionality""" """generic mixin with super basic to and from functionality"""
instance = fields.ActivitypubFieldMixin() instance = fields.ActivitypubFieldMixin()
self.assertEqual(instance.field_to_activity("fish"), "fish") self.assertEqual(instance.field_to_activity("fish"), "fish")
@ -62,7 +63,7 @@ class ActivitypubFields(TestCase):
instance.name = "snake_case_name" instance.name = "snake_case_name"
self.assertEqual(instance.get_activitypub_field(), "snakeCaseName") self.assertEqual(instance.get_activitypub_field(), "snakeCaseName")
def test_set_field_from_activity(self): def test_set_field_from_activity(self, _):
"""setter from entire json blob""" """setter from entire json blob"""
@dataclass @dataclass
@ -81,7 +82,7 @@ class ActivitypubFields(TestCase):
instance.set_field_from_activity(mock_model, data) instance.set_field_from_activity(mock_model, data)
self.assertEqual(mock_model.field_name, "hi") self.assertEqual(mock_model.field_name, "hi")
def test_set_activity_from_field(self): def test_set_activity_from_field(self, _):
"""set json field given entire model""" """set json field given entire model"""
@dataclass @dataclass
@ -99,7 +100,7 @@ class ActivitypubFields(TestCase):
instance.set_activity_from_field(data, mock_model) instance.set_activity_from_field(data, mock_model)
self.assertEqual(data["fieldName"], "bip") self.assertEqual(data["fieldName"], "bip")
def test_remote_id_field(self): def test_remote_id_field(self, _):
"""just sets some defaults on charfield""" """just sets some defaults on charfield"""
instance = fields.RemoteIdField() instance = fields.RemoteIdField()
self.assertEqual(instance.max_length, 255) self.assertEqual(instance.max_length, 255)
@ -108,7 +109,7 @@ class ActivitypubFields(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
instance.run_validators("http://www.example.com/dlfjg 23/x") instance.run_validators("http://www.example.com/dlfjg 23/x")
def test_username_field(self): def test_username_field(self, _):
"""again, just setting defaults on username field""" """again, just setting defaults on username field"""
instance = fields.UsernameField() instance = fields.UsernameField()
self.assertEqual(instance.activitypub_field, "preferredUsername") self.assertEqual(instance.activitypub_field, "preferredUsername")
@ -129,7 +130,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.field_to_activity("test@example.com"), "test") self.assertEqual(instance.field_to_activity("test@example.com"), "test")
def test_privacy_field_defaults(self): def test_privacy_field_defaults(self, _):
"""post privacy field's many default values""" """post privacy field's many default values"""
instance = fields.PrivacyField() instance = fields.PrivacyField()
self.assertEqual(instance.max_length, 255) self.assertEqual(instance.max_length, 255)
@ -142,7 +143,7 @@ class ActivitypubFields(TestCase):
instance.public, "https://www.w3.org/ns/activitystreams#Public" instance.public, "https://www.w3.org/ns/activitystreams#Public"
) )
def test_privacy_field_set_field_from_activity(self): def test_privacy_field_set_field_from_activity(self, _):
"""translate between to/cc fields and privacy""" """translate between to/cc fields and privacy"""
@dataclass(init=False) @dataclass(init=False)
@ -188,10 +189,9 @@ class ActivitypubFields(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_privacy_field_set_activity_from_field(self, *_): def test_privacy_field_set_activity_from_field(self, *_):
"""translate between to/cc fields and privacy""" """translate between to/cc fields and privacy"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): user = User.objects.create_user(
user = User.objects.create_user( "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" )
)
public = "https://www.w3.org/ns/activitystreams#Public" public = "https://www.w3.org/ns/activitystreams#Public"
followers = "%s/followers" % user.remote_id followers = "%s/followers" % user.remote_id
@ -231,7 +231,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(activity["to"], [user.remote_id]) self.assertEqual(activity["to"], [user.remote_id])
self.assertEqual(activity["cc"], []) self.assertEqual(activity["cc"], [])
def test_foreign_key(self): def test_foreign_key(self, _):
"""should be able to format a related model""" """should be able to format a related model"""
instance = fields.ForeignKey("User", on_delete=models.CASCADE) instance = fields.ForeignKey("User", on_delete=models.CASCADE)
Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
@ -240,7 +240,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.field_to_activity(item), "https://e.b/c") self.assertEqual(instance.field_to_activity(item), "https://e.b/c")
@responses.activate @responses.activate
def test_foreign_key_from_activity_str(self): def test_foreign_key_from_activity_str(self, _):
"""create a new object from a foreign key""" """create a new object from a foreign key"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
@ -249,26 +249,25 @@ class ActivitypubFields(TestCase):
del userdata["icon"] del userdata["icon"]
# it shouldn't match with this unrelated user: # it shouldn't match with this unrelated user:
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): unrelated_user = User.objects.create_user(
unrelated_user = User.objects.create_user( "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" )
)
# test receiving an unknown remote id and loading data # test receiving an unknown remote id and loading data
responses.add( responses.add(
responses.GET, responses.GET,
"https://example.com/user/mouse", "https://example.com/user/mouse",
json=userdata, json=userdata,
status=200, status=200,
) )
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
value = instance.field_from_activity("https://example.com/user/mouse") value = instance.field_from_activity("https://example.com/user/mouse")
self.assertIsInstance(value, User) self.assertIsInstance(value, User)
self.assertNotEqual(value, unrelated_user) self.assertNotEqual(value, unrelated_user)
self.assertEqual(value.remote_id, "https://example.com/user/mouse") self.assertEqual(value.remote_id, "https://example.com/user/mouse")
self.assertEqual(value.name, "MOUSE?? MOUSE!!") self.assertEqual(value.name, "MOUSE?? MOUSE!!")
def test_foreign_key_from_activity_dict(self): def test_foreign_key_from_activity_dict(self, *_):
"""test recieving activity json""" """test recieving activity json"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
@ -277,60 +276,57 @@ class ActivitypubFields(TestCase):
del userdata["icon"] del userdata["icon"]
# it shouldn't match with this unrelated user: # it shouldn't match with this unrelated user:
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): unrelated_user = User.objects.create_user(
unrelated_user = User.objects.create_user( "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" )
) with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): value = instance.field_from_activity(activitypub.Person(**userdata))
value = instance.field_from_activity(activitypub.Person(**userdata))
self.assertIsInstance(value, User) self.assertIsInstance(value, User)
self.assertNotEqual(value, unrelated_user) self.assertNotEqual(value, unrelated_user)
self.assertEqual(value.remote_id, "https://example.com/user/mouse") self.assertEqual(value.remote_id, "https://example.com/user/mouse")
self.assertEqual(value.name, "MOUSE?? MOUSE!!") self.assertEqual(value.name, "MOUSE?? MOUSE!!")
# et cetera but we're not testing serializing user json # et cetera but we're not testing serializing user json
def test_foreign_key_from_activity_dict_existing(self): def test_foreign_key_from_activity_dict_existing(self, _):
"""test receiving a dict of an existing object in the db""" """test receiving a dict of an existing object in the db"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes()) userdata = json.loads(datafile.read_bytes())
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): user = User.objects.create_user(
user = User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" )
) user.remote_id = "https://example.com/user/mouse"
user.remote_id = "https://example.com/user/mouse" user.save(broadcast=False, update_fields=["remote_id"])
user.save(broadcast=False)
User.objects.create_user( User.objects.create_user(
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
) )
with patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast"): with patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast"):
value = instance.field_from_activity(activitypub.Person(**userdata)) value = instance.field_from_activity(activitypub.Person(**userdata))
self.assertEqual(value, user) self.assertEqual(value, user)
def test_foreign_key_from_activity_str_existing(self): def test_foreign_key_from_activity_str_existing(self, _):
"""test receiving a remote id of an existing object in the db""" """test receiving a remote id of an existing object in the db"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): user = User.objects.create_user(
user = User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" )
) User.objects.create_user(
User.objects.create_user( "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" )
)
value = instance.field_from_activity(user.remote_id) value = instance.field_from_activity(user.remote_id)
self.assertEqual(value, user) self.assertEqual(value, user)
def test_one_to_one_field(self): def test_one_to_one_field(self, _):
"""a gussied up foreign key""" """a gussied up foreign key"""
instance = fields.OneToOneField("User", on_delete=models.CASCADE) instance = fields.OneToOneField("User", on_delete=models.CASCADE)
Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
item = Serializable(lambda: {"a": "b"}, "https://e.b/c") item = Serializable(lambda: {"a": "b"}, "https://e.b/c")
self.assertEqual(instance.field_to_activity(item), {"a": "b"}) self.assertEqual(instance.field_to_activity(item), {"a": "b"})
def test_many_to_many_field(self): def test_many_to_many_field(self, _):
"""lists!""" """lists!"""
instance = fields.ManyToManyField("User") instance = fields.ManyToManyField("User")
@ -348,7 +344,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.field_to_activity(items), "example.com/snake_case") self.assertEqual(instance.field_to_activity(items), "example.com/snake_case")
@responses.activate @responses.activate
def test_many_to_many_field_from_activity(self): def test_many_to_many_field_from_activity(self, _):
"""resolve related fields for a list, takes a list of remote ids""" """resolve related fields for a list, takes a list of remote ids"""
instance = fields.ManyToManyField(User) instance = fields.ManyToManyField(User)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
@ -360,16 +356,15 @@ class ActivitypubFields(TestCase):
responses.add( responses.add(
responses.GET, "https://example.com/user/mouse", json=userdata, status=200 responses.GET, "https://example.com/user/mouse", json=userdata, status=200
) )
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): value = instance.field_from_activity(
value = instance.field_from_activity( ["https://example.com/user/mouse", "bleh"]
["https://example.com/user/mouse", "bleh"] )
)
self.assertIsInstance(value, list) self.assertIsInstance(value, list)
self.assertEqual(len(value), 1) self.assertEqual(len(value), 1)
self.assertIsInstance(value[0], User) self.assertIsInstance(value[0], User)
def test_tag_field(self): def test_tag_field(self, _):
"""a special type of many to many field""" """a special type of many to many field"""
instance = fields.TagField("User") instance = fields.TagField("User")
@ -388,25 +383,25 @@ class ActivitypubFields(TestCase):
self.assertEqual(result[0].name, "Name") self.assertEqual(result[0].name, "Name")
self.assertEqual(result[0].type, "Serializable") self.assertEqual(result[0].type, "Serializable")
def test_tag_field_from_activity(self): def test_tag_field_from_activity(self, _):
"""loadin' a list of items from Links""" """loadin' a list of items from Links"""
# TODO # TODO
@responses.activate @responses.activate
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
def test_image_field(self, _): @patch("bookwyrm.suggested_users.remove_user_task.delay")
def test_image_field(self, *_):
"""storing images""" """storing images"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): user = User.objects.create_user(
user = User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" )
) image_file = pathlib.Path(__file__).parent.joinpath(
image_file = pathlib.Path(__file__).parent.joinpath( "../../static/images/default_avi.jpg"
"../../static/images/default_avi.jpg" )
) 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) 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") output = fields.image_serializer(user.avatar, alt="alt text")
self.assertIsNotNone( self.assertIsNotNone(
@ -433,7 +428,16 @@ class ActivitypubFields(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_datetime_field(self): 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, _):
"""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()
now = timezone.now() now = timezone.now()
@ -441,12 +445,12 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.field_from_activity(now.isoformat()), now) self.assertEqual(instance.field_from_activity(now.isoformat()), now)
self.assertEqual(instance.field_from_activity("bip"), None) self.assertEqual(instance.field_from_activity("bip"), None)
def test_array_field(self): def test_array_field(self, _):
"""idk why it makes them strings but probably for a good reason""" """idk why it makes them strings but probably for a good reason"""
instance = fields.ArrayField(fields.IntegerField) instance = fields.ArrayField(fields.IntegerField)
self.assertEqual(instance.field_to_activity([0, 1]), ["0", "1"]) self.assertEqual(instance.field_to_activity([0, 1]), ["0", "1"])
def test_html_field(self): def test_html_field(self, _):
"""sanitizes html, the sanitizer has its own tests""" """sanitizes html, the sanitizer has its own tests"""
instance = fields.HtmlField() instance = fields.HtmlField()
self.assertEqual( self.assertEqual(

View file

@ -59,7 +59,7 @@ class ImportJob(TestCase):
unknown_read_data["Exclusive Shelf"] = "read" unknown_read_data["Exclusive Shelf"] = "read"
unknown_read_data["Date Read"] = "" unknown_read_data["Date Read"] = ""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
user = models.User.objects.create_user( user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
@ -175,9 +175,6 @@ class ImportJob(TestCase):
with patch( with patch(
"bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data" "bookwyrm.connectors.openlibrary.Connector." "get_authors_from_data"
): ):
with patch( book = self.item_1.get_book_from_isbn()
"bookwyrm.preview_images.generate_edition_preview_image_task.delay"
):
book = self.item_1.get_book_from_isbn()
self.assertEqual(book.title, "Sabriel") self.assertEqual(book.title, "Sabriel")

View file

@ -11,13 +11,12 @@ class List(TestCase):
def setUp(self): def setUp(self):
"""look, a list""" """look, a list"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_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", "mouseword", local=True, localname="mouse"
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): work = models.Work.objects.create(title="hello")
work = models.Work.objects.create(title="hello") self.book = models.Edition.objects.create(title="hi", parent_work=work)
self.book = models.Edition.objects.create(title="hi", parent_work=work)
def test_remote_id(self, _): def test_remote_id(self, _):
"""shelves use custom remote ids""" """shelves use custom remote ids"""

View file

@ -1,7 +1,7 @@
""" testing models """ """ testing models """
from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from unittest.mock import patch
from bookwyrm import models from bookwyrm import models
@ -11,16 +11,15 @@ class ReadThrough(TestCase):
def setUp(self): def setUp(self):
"""look, a shelf""" """look, a shelf"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): self.work = models.Work.objects.create(title="Example Work")
self.work = models.Work.objects.create(title="Example Work") self.edition = models.Edition.objects.create(
self.edition = models.Edition.objects.create( title="Example Edition", parent_work=self.work
title="Example Edition", parent_work=self.work )
)
self.readthrough = models.ReadThrough.objects.create( self.readthrough = models.ReadThrough.objects.create(
user=self.user, book=self.edition user=self.user, book=self.edition

View file

@ -1,4 +1,5 @@
""" testing models """ """ testing models """
import json
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
@ -10,36 +11,31 @@ class Relationship(TestCase):
def setUp(self): def setUp(self):
"""we need some users for this""" """we need some users for this"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user(
self.remote_user = models.User.objects.create_user( "rat",
"rat", "rat@rat.com",
"rat@rat.com", "ratword",
"ratword", local=False,
local=False, remote_id="https://example.com/users/rat",
remote_id="https://example.com/users/rat", inbox="https://example.com/users/rat/inbox",
inbox="https://example.com/users/rat/inbox", outbox="https://example.com/users/rat/outbox",
outbox="https://example.com/users/rat/outbox", )
) with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
) )
self.local_user.remote_id = "http://local.com/user/mouse" self.local_user.remote_id = "http://local.com/user/mouse"
self.local_user.save(broadcast=False) self.local_user.save(broadcast=False, update_fields=["remote_id"])
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"""
real_broadcast = models.UserFollowRequest.broadcast with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
request = models.UserFollowRequest.objects.create(
def mock_broadcast(_, activity, user): user_subject=self.local_user, user_object=self.remote_user
"""introspect what's being sent out""" )
self.assertEqual(user.remote_id, self.local_user.remote_id) activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Follow") self.assertEqual(activity["type"], "Follow")
models.UserFollowRequest.broadcast = mock_broadcast
request = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user
)
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
) )
@ -52,7 +48,6 @@ class Relationship(TestCase):
self.assertEqual(rel.status, "follows") self.assertEqual(rel.status, "follows")
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)
models.UserFollowRequest.broadcast = real_broadcast
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"""
@ -71,36 +66,26 @@ 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)
def test_follow_request_activity(self): @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
def test_follow_request_activity(self, broadcast_mock):
"""accept a request and make it a relationship""" """accept a request and make it a relationship"""
real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user):
self.assertEqual(user.remote_id, 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["type"], "Follow")
models.UserFollowRequest.broadcast = mock_broadcast
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,
) )
models.UserFollowRequest.broadcast = real_broadcast activity = json.loads(broadcast_mock.call_args[0][1])
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"], self.remote_user.remote_id)
self.assertEqual(activity["type"], "Follow")
def test_follow_request_accept(self): @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
def test_follow_request_accept(self, broadcast_mock):
"""accept a request and make it a relationship""" """accept a request and make it a relationship"""
real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user):
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity["type"], "Accept")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["id"], "https://www.hi.com/")
self.local_user.manually_approves_followers = True self.local_user.manually_approves_followers = True
self.local_user.save(broadcast=False) self.local_user.save(
models.UserFollowRequest.broadcast = mock_broadcast broadcast=False, update_fields=["manually_approves_followers"]
)
request = models.UserFollowRequest.objects.create( request = models.UserFollowRequest.objects.create(
user_subject=self.remote_user, user_subject=self.remote_user,
user_object=self.local_user, user_object=self.local_user,
@ -108,32 +93,34 @@ class Relationship(TestCase):
) )
request.accept() request.accept()
activity = json.loads(broadcast_mock.call_args[0][1])
self.assertEqual(activity["type"], "Accept")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["id"], "https://www.hi.com/")
self.assertFalse(models.UserFollowRequest.objects.exists()) self.assertFalse(models.UserFollowRequest.objects.exists())
self.assertTrue(models.UserFollows.objects.exists()) self.assertTrue(models.UserFollows.objects.exists())
rel = models.UserFollows.objects.get() rel = models.UserFollows.objects.get()
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)
models.UserFollowRequest.broadcast = real_broadcast
def test_follow_request_reject(self): @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
def test_follow_request_reject(self, broadcast_mock):
"""accept a request and make it a relationship""" """accept a request and make it a relationship"""
real_broadcast = models.UserFollowRequest.broadcast
def mock_reject(_, activity, user):
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity["type"], "Reject")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["id"], request.remote_id)
models.UserFollowRequest.broadcast = mock_reject
self.local_user.manually_approves_followers = True self.local_user.manually_approves_followers = True
self.local_user.save(broadcast=False) self.local_user.save(
broadcast=False, update_fields=["manually_approves_followers"]
)
request = models.UserFollowRequest.objects.create( request = models.UserFollowRequest.objects.create(
user_subject=self.remote_user, user_subject=self.remote_user,
user_object=self.local_user, user_object=self.local_user,
) )
request.reject() request.reject()
activity = json.loads(broadcast_mock.call_args[0][1])
self.assertEqual(activity["type"], "Reject")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["id"], request.remote_id)
self.assertFalse(models.UserFollowRequest.objects.exists()) self.assertFalse(models.UserFollowRequest.objects.exists())
self.assertFalse(models.UserFollows.objects.exists()) self.assertFalse(models.UserFollows.objects.exists())
models.UserFollowRequest.broadcast = real_broadcast

View file

@ -7,22 +7,20 @@ from bookwyrm import models, settings
# pylint: disable=unused-argument # pylint: disable=unused-argument
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
class Shelf(TestCase): class Shelf(TestCase):
"""some activitypub oddness ahead""" """some activitypub oddness ahead"""
def setUp(self): def setUp(self):
"""look, a shelf""" """look, a shelf"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_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", "mouseword", local=True, localname="mouse"
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): 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
)
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.delay"):
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
@ -31,7 +29,7 @@ class Shelf(TestCase):
expected_id = "https://%s/user/mouse/books/test-shelf" % settings.DOMAIN expected_id = "https://%s/user/mouse/books/test-shelf" % settings.DOMAIN
self.assertEqual(shelf.get_remote_id(), expected_id) self.assertEqual(shelf.get_remote_id(), expected_id)
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.delay"):
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
@ -45,7 +43,7 @@ class Shelf(TestCase):
self.assertEqual(activity_json["name"], "Test Shelf") self.assertEqual(activity_json["name"], "Test Shelf")
self.assertEqual(activity_json["owner"], self.local_user.remote_id) self.assertEqual(activity_json["owner"], self.local_user.remote_id)
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.delay") as mock:
@ -66,7 +64,7 @@ class Shelf(TestCase):
self.assertEqual(activity["object"]["name"], "arthur russel") self.assertEqual(activity["object"]["name"], "arthur russel")
self.assertEqual(shelf.name, "arthur russel") self.assertEqual(shelf.name, "arthur russel")
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.delay"):
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(

View file

@ -22,31 +22,30 @@ class Status(TestCase):
def setUp(self): def setUp(self):
"""useful things for creating a status""" """useful things for creating a status"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_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", "mouseword", local=True, localname="mouse"
) )
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
)
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.book = models.Edition.objects.create(title="Test Edition")
image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/default_avi.jpg"
) )
image = Image.open(image_file) self.book = models.Edition.objects.create(title="Test Edition")
output = BytesIO()
with patch("bookwyrm.models.Status.broadcast"): image_file = pathlib.Path(__file__).parent.joinpath(
image.save(output, format=image.format) "../../static/images/default_avi.jpg"
self.book.cover.save("test.jpg", ContentFile(output.getvalue())) )
image = Image.open(image_file)
output = BytesIO()
with patch("bookwyrm.models.Status.broadcast"):
image.save(output, format=image.format)
self.book.cover.save("test.jpg", ContentFile(output.getvalue()))
def test_status_generated_fields(self, *_): def test_status_generated_fields(self, *_):
"""setting remote id""" """setting remote id"""
@ -61,10 +60,9 @@ class Status(TestCase):
child = models.Status.objects.create( child = models.Status.objects.create(
content="hello", reply_parent=parent, user=self.local_user content="hello", reply_parent=parent, user=self.local_user
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): models.Review.objects.create(
models.Review.objects.create( content="hey", reply_parent=parent, user=self.local_user, book=self.book
content="hey", reply_parent=parent, user=self.local_user, book=self.book )
)
models.Status.objects.create( models.Status.objects.create(
content="hi hello", reply_parent=child, user=self.local_user content="hi hello", reply_parent=child, user=self.local_user
) )
@ -96,10 +94,9 @@ class Status(TestCase):
child = models.Status.objects.create( child = models.Status.objects.create(
content="hello", reply_parent=parent, user=self.local_user content="hello", reply_parent=parent, user=self.local_user
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): models.Review.objects.create(
models.Review.objects.create( content="hey", reply_parent=parent, user=self.local_user, book=self.book
content="hey", reply_parent=parent, user=self.local_user, book=self.book )
)
models.Status.objects.create( models.Status.objects.create(
content="hi hello", reply_parent=child, user=self.local_user content="hi hello", reply_parent=child, user=self.local_user
) )
@ -256,15 +253,14 @@ class Status(TestCase):
def test_review_to_activity(self, *_): def test_review_to_activity(self, *_):
"""subclass of the base model version with a "pure" serializer""" """subclass of the base model version with a "pure" serializer"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): status = models.Review.objects.create(
status = models.Review.objects.create( name="Review name",
name="Review name", content="test content",
content="test content", rating=3.0,
rating=3.0, user=self.local_user,
user=self.local_user, book=self.book,
book=self.book, )
) activity = status.to_activity()
activity = status.to_activity()
self.assertEqual(activity["id"], status.remote_id) self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Review") self.assertEqual(activity["type"], "Review")
self.assertEqual(activity["rating"], 3) self.assertEqual(activity["rating"], 3)
@ -274,15 +270,14 @@ class Status(TestCase):
def test_review_to_pure_activity(self, *_): def test_review_to_pure_activity(self, *_):
"""subclass of the base model version with a "pure" serializer""" """subclass of the base model version with a "pure" serializer"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): status = models.Review.objects.create(
status = models.Review.objects.create( name="Review's name",
name="Review's name", content="test content",
content="test content", rating=3.0,
rating=3.0, user=self.local_user,
user=self.local_user, book=self.book,
book=self.book, )
) activity = status.to_activity(pure=True)
activity = status.to_activity(pure=True)
self.assertEqual(activity["id"], status.remote_id) self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Article") self.assertEqual(activity["type"], "Article")
self.assertEqual( self.assertEqual(
@ -299,14 +294,13 @@ class Status(TestCase):
def test_review_to_pure_activity_no_rating(self, *_): def test_review_to_pure_activity_no_rating(self, *_):
"""subclass of the base model version with a "pure" serializer""" """subclass of the base model version with a "pure" serializer"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): status = models.Review.objects.create(
status = models.Review.objects.create( name="Review name",
name="Review name", content="test content",
content="test content", user=self.local_user,
user=self.local_user, book=self.book,
book=self.book, )
) activity = status.to_activity(pure=True)
activity = status.to_activity(pure=True)
self.assertEqual(activity["id"], status.remote_id) self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Article") self.assertEqual(activity["type"], "Article")
self.assertEqual( self.assertEqual(
@ -322,13 +316,12 @@ class Status(TestCase):
def test_reviewrating_to_pure_activity(self, *_): def test_reviewrating_to_pure_activity(self, *_):
"""subclass of the base model version with a "pure" serializer""" """subclass of the base model version with a "pure" serializer"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): status = models.ReviewRating.objects.create(
status = models.ReviewRating.objects.create( rating=3.0,
rating=3.0, user=self.local_user,
user=self.local_user, book=self.book,
book=self.book, )
) activity = status.to_activity(pure=True)
activity = status.to_activity(pure=True)
self.assertEqual(activity["id"], status.remote_id) self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Note") self.assertEqual(activity["type"], "Note")
self.assertEqual( self.assertEqual(
@ -357,12 +350,11 @@ class Status(TestCase):
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.preview_images.generate_user_preview_image_task.delay"): fav = models.Favorite.objects.create(status=status, user=self.local_user)
fav = models.Favorite.objects.create(status=status, user=self.local_user)
# can't fav a status twice # can't fav a status twice
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
models.Favorite.objects.create(status=status, user=self.local_user) models.Favorite.objects.create(status=status, user=self.local_user)
activity = fav.to_activity() activity = fav.to_activity()
self.assertEqual(activity["type"], "Like") self.assertEqual(activity["type"], "Like")

View file

@ -11,7 +11,7 @@ from bookwyrm.settings import DOMAIN
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
class User(TestCase): class User(TestCase):
def setUp(self): def setUp(self):
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mouse@%s" % DOMAIN, "mouse@%s" % DOMAIN,
"mouse@mouse.mouse", "mouse@mouse.mouse",
@ -35,16 +35,15 @@ class User(TestCase):
self.assertIsNotNone(self.user.key_pair.public_key) self.assertIsNotNone(self.user.key_pair.public_key)
def test_remote_user(self): def test_remote_user(self):
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"): user = models.User.objects.create_user(
user = models.User.objects.create_user( "rat",
"rat", "rat@rat.rat",
"rat@rat.rat", "ratword",
"ratword", local=False,
local=False, remote_id="https://example.com/dfjkg",
remote_id="https://example.com/dfjkg", bookwyrm_user=False,
bookwyrm_user=False, )
)
self.assertEqual(user.username, "rat@example.com") self.assertEqual(user.username, "rat@example.com")
def test_user_shelves(self): def test_user_shelves(self):
@ -156,7 +155,8 @@ class User(TestCase):
self.assertIsNone(server.application_type) self.assertIsNone(server.application_type)
self.assertIsNone(server.application_version) self.assertIsNone(server.application_version)
def test_delete_user(self): @patch("bookwyrm.suggested_users.remove_user_task.delay")
def test_delete_user(self, _):
"""deactivate a user""" """deactivate a user"""
self.assertTrue(self.user.is_active) self.assertTrue(self.user.is_active)
with patch( with patch(

View file

@ -11,7 +11,7 @@ class Activitystreams(TestCase):
def setUp(self): def setUp(self):
"""use a test csv""" """use a test csv"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
) )
@ -22,18 +22,17 @@ class Activitystreams(TestCase):
local=True, local=True,
localname="nutria", localname="nutria",
) )
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): self.book = models.Edition.objects.create(title="test book")
self.book = models.Edition.objects.create(title="test book")
class TestStream(activitystreams.ActivityStream): class TestStream(activitystreams.ActivityStream):
"""test stream, don't have to do anything here""" """test stream, don't have to do anything here"""

View file

@ -3,7 +3,6 @@ from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
import responses
from bookwyrm import emailing, models from bookwyrm import emailing, models
@ -15,7 +14,7 @@ class Emailing(TestCase):
def setUp(self): def setUp(self):
"""we need basic test data and mocks""" """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@local.com", "mouse@local.com",
"mouse@mouse.mouse", "mouse@mouse.mouse",
@ -23,8 +22,7 @@ class Emailing(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"): models.SiteSettings.objects.create()
models.SiteSettings.objects.create()
def test_invite_email(self, email_mock): def test_invite_email(self, email_mock):
"""load the invite email""" """load the invite email"""

View file

@ -0,0 +1,76 @@
""" django configuration of postgres """
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class PostgresTriggers(TestCase):
"""special migrations, fancy stuff ya know"""
def test_search_vector_on_create(self, _):
"""make sure that search_vector is being set correctly on create"""
book = models.Edition.objects.create(title="The Long Goodbye")
book.refresh_from_db()
self.assertEqual(book.search_vector, "'goodby':3A 'long':2A")
def test_search_vector_on_update(self, _):
"""make sure that search_vector is being set correctly on edit"""
book = models.Edition.objects.create(title="The Long Goodbye")
book.title = "The Even Longer Goodbye"
book.save(broadcast=False)
book.refresh_from_db()
self.assertEqual(book.search_vector, "'even':2A 'goodby':4A 'longer':3A")
def test_search_vector_fields(self, _):
"""use multiple fields to create search vector"""
author = models.Author.objects.create(name="The Rays")
book = models.Edition.objects.create(
title="The Long Goodbye",
subtitle="wow cool",
series="series name",
languages=["irrelevent"],
)
book.authors.add(author)
book.refresh_from_db()
self.assertEqual(
book.search_vector,
"'cool':5B 'goodby':3A 'long':2A 'name':9 'rays':7C 'seri':8 'the':6C 'wow':4B",
)
def test_seach_vector_on_author_update(self, _):
"""update search when an author name changes"""
author = models.Author.objects.create(name="The Rays")
book = models.Edition.objects.create(
title="The Long Goodbye",
)
book.authors.add(author)
author.name = "Jeremy"
author.save(broadcast=False)
book.refresh_from_db()
self.assertEqual(book.search_vector, "'goodby':3A 'jeremy':4C 'long':2A")
def test_seach_vector_on_author_delete(self, _):
"""update search when an author name changes"""
author = models.Author.objects.create(name="Jeremy")
book = models.Edition.objects.create(
title="The Long Goodbye",
)
book.authors.add(author)
book.refresh_from_db()
self.assertEqual(book.search_vector, "'goodby':3A 'jeremy':4C 'long':2A")
book.authors.remove(author)
book.refresh_from_db()
self.assertEqual(book.search_vector, "'goodby':3A 'long':2A")
def test_search_vector_stop_word_fallback(self, _):
"""use a fallback when removing stop words leads to an empty vector"""
book = models.Edition.objects.create(
title="there there",
)
book.refresh_from_db()
self.assertEqual(book.search_vector, "'there':1A,2A")

View file

@ -9,7 +9,6 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models.fields.files import ImageFieldFile from django.db.models.fields.files import ImageFieldFile
from bookwyrm import models, settings from bookwyrm import models, settings
from bookwyrm.preview_images import ( from bookwyrm.preview_images import (
generate_site_preview_image_task, generate_site_preview_image_task,
generate_edition_preview_image_task, generate_edition_preview_image_task,
@ -27,10 +26,10 @@ class PreviewImages(TestCase):
def setUp(self): def setUp(self):
"""we need basic test data and mocks""" """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): avatar_file = pathlib.Path(__file__).parent.joinpath(
avatar_file = pathlib.Path(__file__).parent.joinpath( "../static/images/no_cover.jpg"
"../static/images/no_cover.jpg" )
) with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"possum@local.com", "possum@local.com",
"possum@possum.possum", "possum@possum.possum",
@ -43,15 +42,17 @@ class PreviewImages(TestCase):
content_type="image/jpeg", content_type="image/jpeg",
), ),
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
self.work = models.Work.objects.create(title="Test Work") self.work = models.Work.objects.create(title="Test Work")
self.edition = models.Edition.objects.create( self.edition = models.Edition.objects.create(
title="Example Edition", title="Example Edition",
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.preview_images.generate_site_preview_image_task.delay"):
self.site = models.SiteSettings.objects.create() self.site = models.SiteSettings.objects.create()
settings.ENABLE_PREVIEW_IMAGES = True
def test_generate_preview_image(self, *args, **kwargs): def test_generate_preview_image(self, *args, **kwargs):
image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(

View file

@ -37,7 +37,7 @@ class Signature(TestCase):
def setUp(self): def setUp(self):
"""create users and test data""" """create users and test data"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.mouse = models.User.objects.create_user( self.mouse = models.User.objects.create_user(
"mouse@%s" % DOMAIN, "mouse@%s" % DOMAIN,
"mouse@example.com", "mouse@example.com",
@ -58,8 +58,7 @@ class Signature(TestCase):
"http://localhost/user/remote", KeyPair(private_key, public_key) "http://localhost/user/remote", KeyPair(private_key, public_key)
) )
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"): models.SiteSettings.objects.create()
models.SiteSettings.objects.create()
def send(self, signature, now, data, digest): def send(self, signature, now, data, digest):
"""test request""" """test request"""
@ -119,10 +118,9 @@ class Signature(TestCase):
status=200, status=200,
) )
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.get_remote_reviews.delay"):
with patch("bookwyrm.models.user.get_remote_reviews.delay"): response = self.send_test_request(sender=self.fake_remote)
response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 200)
@responses.activate @responses.activate
def test_key_needs_refresh(self): def test_key_needs_refresh(self):
@ -143,23 +141,22 @@ class Signature(TestCase):
data["publicKey"]["publicKeyPem"] = key_pair.public_key data["publicKey"]["publicKeyPem"] = key_pair.public_key
responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200) responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200)
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.models.user.get_remote_reviews.delay"):
with patch("bookwyrm.models.user.get_remote_reviews.delay"): # Key correct:
# Key correct: response = self.send_test_request(sender=self.fake_remote)
response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 200)
# Old key is cached, so still works: # Old key is cached, so still works:
response = self.send_test_request(sender=self.fake_remote) response = self.send_test_request(sender=self.fake_remote)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Try with new key: # Try with new key:
response = self.send_test_request(sender=new_sender) response = self.send_test_request(sender=new_sender)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Now the old key will fail: # Now the old key will fail:
response = self.send_test_request(sender=self.fake_remote) response = self.send_test_request(sender=self.fake_remote)
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)
@responses.activate @responses.activate
def test_nonexistent_signer(self): def test_nonexistent_signer(self):

View file

@ -0,0 +1,173 @@
""" testing user follow suggestions """
from collections import namedtuple
from unittest.mock import patch
from django.db.models import Q
from django.test import TestCase
from bookwyrm import models
from bookwyrm.suggested_users import suggested_users, get_annotated_users
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.suggested_users.rerank_user_task.delay")
@patch("bookwyrm.suggested_users.remove_user_task.delay")
class SuggestedUsers(TestCase):
"""using redis to build activity streams"""
def setUp(self):
"""use a test csv"""
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
)
def test_get_rank(self, *_):
"""a float that reflects both the mutuals count and shared books"""
Mock = namedtuple("AnnotatedUserMock", ("mutuals", "shared_books"))
annotated_user_mock = Mock(3, 27)
rank = suggested_users.get_rank(annotated_user_mock)
self.assertEqual(rank, 3.9642857142857144)
def test_store_id(self, *_):
"""redis key generation"""
self.assertEqual(
suggested_users.store_id(self.local_user),
"{:d}-suggestions".format(self.local_user.id),
)
def test_get_counts_from_rank(self, *_):
"""reverse the rank computation to get the mutuals and shared books counts"""
counts = suggested_users.get_counts_from_rank(3.9642857142857144)
self.assertEqual(counts["mutuals"], 3)
self.assertEqual(counts["shared_books"], 27)
def test_get_objects_for_store(self, *_):
"""list of people to follow for a given user"""
mutual_user = models.User.objects.create_user(
"rat", "rat@local.rat", "password", local=True, localname="rat"
)
suggestable_user = models.User.objects.create_user(
"nutria",
"nutria@nutria.nutria",
"password",
local=True,
localname="nutria",
discoverable=True,
)
# you follow rat
mutual_user.followers.add(self.local_user)
# rat follows the suggested user
suggestable_user.followers.add(mutual_user)
results = suggested_users.get_objects_for_store(
"{:d}-suggestions".format(self.local_user.id)
)
self.assertEqual(results.count(), 1)
match = results.first()
self.assertEqual(match.id, suggestable_user.id)
self.assertEqual(match.mutuals, 1)
def test_create_user_signal(self, *_):
"""build suggestions for new users"""
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") as mock:
models.User.objects.create_user(
"nutria", "nutria@nu.tria", "password", local=True, localname="nutria"
)
self.assertEqual(mock.call_count, 1)
def test_get_annotated_users(self, *_):
"""list of people you might know"""
user_1 = models.User.objects.create_user(
"nutria@local.com",
"nutria@nutria.com",
"nutriaword",
local=True,
localname="nutria",
discoverable=True,
)
user_2 = models.User.objects.create_user(
"fish@local.com",
"fish@fish.com",
"fishword",
local=True,
localname="fish",
)
work = models.Work.objects.create(title="Test Work")
book = models.Edition.objects.create(
title="Test Book",
remote_id="https://example.com/book/1",
parent_work=work,
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
# 1 shared follow
self.local_user.following.add(user_2)
user_1.followers.add(user_2)
# 1 shared book
models.ShelfBook.objects.create(
user=self.local_user,
book=book,
shelf=self.local_user.shelf_set.first(),
)
models.ShelfBook.objects.create(
user=user_1, book=book, shelf=user_1.shelf_set.first()
)
result = get_annotated_users(self.local_user)
self.assertEqual(result.count(), 1)
self.assertTrue(user_1 in result)
self.assertFalse(user_2 in result)
user_1_annotated = result.get(id=user_1.id)
self.assertEqual(user_1_annotated.mutuals, 1)
self.assertEqual(user_1_annotated.shared_books, 1)
def test_get_annotated_users_counts(self, *_):
"""correct counting for multiple shared attributed"""
user_1 = models.User.objects.create_user(
"nutria@local.com",
"nutria@nutria.com",
"nutriaword",
local=True,
localname="nutria",
discoverable=True,
)
for i in range(3):
user = models.User.objects.create_user(
"{:d}@local.com".format(i),
"{:d}@nutria.com".format(i),
"password",
local=True,
localname=i,
)
user.following.add(user_1)
user.followers.add(self.local_user)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
for i in range(3):
book = models.Edition.objects.create(
title=i,
parent_work=models.Work.objects.create(title=i),
)
models.ShelfBook.objects.create(
user=self.local_user,
book=book,
shelf=self.local_user.shelf_set.first(),
)
models.ShelfBook.objects.create(
user=user_1, book=book, shelf=user_1.shelf_set.first()
)
result = get_annotated_users(
self.local_user,
~Q(id=self.local_user.id),
~Q(followers=self.local_user),
)
user_1_annotated = result.get(id=user_1.id)
self.assertEqual(user_1_annotated.mutuals, 3)

View file

@ -22,7 +22,7 @@ class TemplateTags(TestCase):
def setUp(self): def setUp(self):
"""create some filler objects""" """create some filler objects"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.mouse", "mouse@mouse.mouse",
@ -30,22 +30,20 @@ class TemplateTags(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.rat", "rat@rat.rat",
"ratword", "ratword",
remote_id="http://example.com/rat", remote_id="http://example.com/rat",
local=False, local=False,
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): self.book = models.Edition.objects.create(title="Test Book")
self.book = models.Edition.objects.create(title="Test Book")
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.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): 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)
def test_get_user_rating_doesnt_exist(self, *_): def test_get_user_rating_doesnt_exist(self, *_):
@ -63,30 +61,27 @@ 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")
def test_get_replies(self, *_): def test_get_replies(self, *_):
"""direct replies to a status""" """direct replies to a status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): parent = models.Review.objects.create(
with patch( user=self.user, book=self.book, content="hi"
"bookwyrm.preview_images.generate_edition_preview_image_task.delay" )
): first_child = models.Status.objects.create(
parent = models.Review.objects.create( reply_parent=parent, user=self.user, content="hi"
user=self.user, book=self.book, content="hi" )
) second_child = models.Status.objects.create(
first_child = models.Status.objects.create( reply_parent=parent, user=self.user, content="hi"
reply_parent=parent, user=self.user, content="hi" )
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
):
third_child = models.Status.objects.create(
reply_parent=parent,
user=self.user,
deleted=True,
deleted_date=timezone.now(),
) )
second_child = models.Status.objects.create(
reply_parent=parent, user=self.user, content="hi"
)
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
):
third_child = models.Status.objects.create(
reply_parent=parent,
user=self.user,
deleted=True,
deleted_date=timezone.now(),
)
replies = status_display.get_replies(parent) replies = status_display.get_replies(parent)
self.assertEqual(len(replies), 2) self.assertEqual(len(replies), 2)
@ -97,12 +92,9 @@ 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.delay"):
with patch( parent = models.Review.objects.create(
"bookwyrm.preview_images.generate_edition_preview_image_task.delay" user=self.user, book=self.book, content="hi"
): )
parent = models.Review.objects.create(
user=self.user, book=self.book, content="hi"
)
child = models.Status.objects.create( child = models.Status.objects.create(
reply_parent=parent, user=self.user, content="hi" reply_parent=parent, user=self.user, content="hi"
) )
@ -113,8 +105,7 @@ class TemplateTags(TestCase):
def test_get_user_liked(self, *_): def test_get_user_liked(self, *_):
"""did a user like a status""" """did a user like a status"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): 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.delay"):
@ -123,8 +114,7 @@ class TemplateTags(TestCase):
def test_get_user_boosted(self, *_): def test_get_user_boosted(self, *_):
"""did a user boost a status""" """did a user boost a status"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): 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.delay"):
@ -133,34 +123,28 @@ class TemplateTags(TestCase):
def test_get_boosted(self, *_): def test_get_boosted(self, *_):
"""load a boosted status""" """load a boosted status"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Review.objects.create(user=self.remote_user, book=self.book)
status = models.Review.objects.create( boost = models.Boost.objects.create(user=self.user, boosted_status=status)
user=self.remote_user, book=self.book
)
boost = models.Boost.objects.create(
user=self.user, boosted_status=status
)
boosted = status_display.get_boosted(boost) boosted = status_display.get_boosted(boost)
self.assertIsInstance(boosted, models.Review) self.assertIsInstance(boosted, models.Review)
self.assertEqual(boosted, status) self.assertEqual(boosted, status)
def test_get_book_description(self, *_): def test_get_book_description(self, *_):
"""grab it from the edition or the parent""" """grab it from the edition or the parent"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): work = models.Work.objects.create(title="Test Work")
work = models.Work.objects.create(title="Test Work") self.book.parent_work = work
self.book.parent_work = work self.book.save()
self.book.save()
self.assertIsNone(bookwyrm_tags.get_book_description(self.book)) self.assertIsNone(bookwyrm_tags.get_book_description(self.book))
work.description = "hi" work.description = "hi"
work.save() work.save()
self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hi") self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hi")
self.book.description = "hello" self.book.description = "hello"
self.book.save() self.book.save()
self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hello") self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hello")
def test_get_uuid(self, *_): def test_get_uuid(self, *_):
"""uuid functionality""" """uuid functionality"""

View file

@ -19,7 +19,7 @@ class Inbox(TestCase):
self.client = Client() self.client = Client()
self.factory = RequestFactory() self.factory = RequestFactory()
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
local_user = models.User.objects.create_user( local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -27,18 +27,18 @@ class Inbox(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
local_user.remote_id = "https://example.com/user/mouse" local_user.remote_id = "https://example.com/user/mouse"
local_user.save(broadcast=False) local_user.save(broadcast=False, update_fields=["remote_id"])
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
self.create_json = { self.create_json = {
"id": "hi", "id": "hi",
"type": "Create", "type": "Create",
@ -47,8 +47,7 @@ class Inbox(TestCase):
"cc": ["https://example.com/user/mouse/followers"], "cc": ["https://example.com/user/mouse/followers"],
"object": {}, "object": {},
} }
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"): models.SiteSettings.objects.create()
models.SiteSettings.objects.create()
def test_inbox_invalid_get(self): def test_inbox_invalid_get(self):
"""shouldn't try to handle if the user is not found""" """shouldn't try to handle if the user is not found"""
@ -146,7 +145,8 @@ class Inbox(TestCase):
) )
self.assertTrue(views.inbox.is_blocked_activity(activity)) self.assertTrue(views.inbox.is_blocked_activity(activity))
def test_create_by_deactivated_user(self): @patch("bookwyrm.suggested_users.remove_user_task.delay")
def test_create_by_deactivated_user(self, _):
"""don't let deactivated users post""" """don't let deactivated users post"""
self.remote_user.delete(broadcast=False) self.remote_user.delete(broadcast=False)
self.assertTrue(self.remote_user.deleted) self.assertTrue(self.remote_user.deleted)

View file

@ -13,7 +13,7 @@ class InboxAdd(TestCase):
def setUp(self): def setUp(self):
"""basic user and book data""" """basic user and book data"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
local_user = models.User.objects.create_user( local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -21,27 +21,27 @@ class InboxAdd(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
local_user.remote_id = "https://example.com/user/mouse" local_user.remote_id = "https://example.com/user/mouse"
local_user.save(broadcast=False) local_user.save(broadcast=False, update_fields=["remote_id"])
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
)
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
work = models.Work.objects.create(title="work title")
self.book = models.Edition.objects.create(
title="Test",
remote_id="https://example.com/book/37292",
parent_work=work,
) )
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"):
models.SiteSettings.objects.create() work = models.Work.objects.create(title="work title")
self.book = models.Edition.objects.create(
title="Test",
remote_id="https://example.com/book/37292",
parent_work=work,
)
models.SiteSettings.objects.create()
@responses.activate @responses.activate
def test_handle_add_book_to_shelf(self): def test_handle_add_book_to_shelf(self):

View file

@ -13,7 +13,7 @@ class InboxActivities(TestCase):
def setUp(self): def setUp(self):
"""basic user and book data""" """basic user and book data"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -21,26 +21,26 @@ class InboxActivities(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
self.local_user.remote_id = "https://example.com/user/mouse" self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False) self.local_user.save(broadcast=False, update_fields=["remote_id"])
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
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.activitystreams.ActivityStream.add_status"):
self.status = models.Status.objects.create(
user=self.local_user,
content="Test status",
remote_id="https://example.com/status/1",
) )
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
self.status = models.Status.objects.create(
user=self.local_user,
content="Test status",
remote_id="https://example.com/status/1",
)
self.create_json = { self.create_json = {
"id": "hi", "id": "hi",
@ -50,8 +50,8 @@ class InboxActivities(TestCase):
"cc": ["https://example.com/user/mouse/followers"], "cc": ["https://example.com/user/mouse/followers"],
"object": {}, "object": {},
} }
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
@ -88,13 +88,12 @@ class InboxActivities(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores") @patch("bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores")
def test_boost_remote_status(self, redis_mock, _): def test_boost_remote_status(self, redis_mock, _):
"""boost a status from a remote server""" """boost a status from a remote server"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): work = models.Work.objects.create(title="work title")
work = models.Work.objects.create(title="work title") book = models.Edition.objects.create(
book = models.Edition.objects.create( title="Test",
title="Test", remote_id="https://bookwyrm.social/book/37292",
remote_id="https://bookwyrm.social/book/37292", parent_work=work,
parent_work=work, )
)
self.assertEqual(models.Notification.objects.count(), 0) self.assertEqual(models.Notification.objects.count(), 0)
activity = { activity = {
"type": "Announce", "type": "Announce",

View file

@ -12,7 +12,7 @@ class InboxBlock(TestCase):
def setUp(self): def setUp(self):
"""basic user and book data""" """basic user and book data"""
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -20,20 +20,20 @@ class InboxBlock(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
self.local_user.remote_id = "https://example.com/user/mouse" self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False) self.local_user.save(broadcast=False, update_fields=["remote_id"])
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
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"""

View file

@ -10,12 +10,13 @@ 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")
class InboxCreate(TestCase): class InboxCreate(TestCase):
"""readthrough tests""" """readthrough tests"""
def setUp(self): def setUp(self):
"""basic user and book data""" """basic user and book data"""
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -23,25 +24,18 @@ class InboxCreate(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
self.local_user.remote_id = "https://example.com/user/mouse" self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False) self.local_user.save(broadcast=False, update_fields=["remote_id"])
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
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.activitystreams.ActivityStream.add_status"):
self.status = models.Status.objects.create(
user=self.local_user,
content="Test status",
remote_id="https://example.com/status/1",
)
self.create_json = { self.create_json = {
"id": "hi", "id": "hi",
@ -51,22 +45,18 @@ class InboxCreate(TestCase):
"cc": ["https://example.com/user/mouse/followers"], "cc": ["https://example.com/user/mouse/followers"],
"object": {}, "object": {},
} }
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"): models.SiteSettings.objects.create()
models.SiteSettings.objects.create()
def test_create_status(self): def test_create_status(self, _):
"""the "it justs works" mode""" """the "it justs works" mode"""
self.assertEqual(models.Status.objects.count(), 1)
datafile = pathlib.Path(__file__).parent.joinpath( datafile = pathlib.Path(__file__).parent.joinpath(
"../../data/ap_quotation.json" "../../data/ap_quotation.json"
) )
status_data = json.loads(datafile.read_bytes()) status_data = json.loads(datafile.read_bytes())
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): models.Edition.objects.create(
models.Edition.objects.create( title="Test Book", remote_id="https://example.com/book/1"
title="Test Book", remote_id="https://example.com/book/1" )
)
activity = self.create_json activity = self.create_json
activity["object"] = status_data activity["object"] = status_data
@ -81,15 +71,13 @@ class InboxCreate(TestCase):
self.assertEqual(status.quote, "quote body") self.assertEqual(status.quote, "quote body")
self.assertEqual(status.content, "commentary") self.assertEqual(status.content, "commentary")
self.assertEqual(status.user, self.local_user) self.assertEqual(status.user, self.local_user)
self.assertEqual(models.Status.objects.count(), 2)
# while we're here, lets ensure we avoid dupes # while we're here, lets ensure we avoid dupes
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
self.assertEqual(models.Status.objects.count(), 2)
def test_create_status_remote_note_with_mention(self):
"""should only create it under the right circumstances"""
self.assertEqual(models.Status.objects.count(), 1) self.assertEqual(models.Status.objects.count(), 1)
def test_create_status_remote_note_with_mention(self, _):
"""should only create it under the right circumstances"""
self.assertFalse( self.assertFalse(
models.Notification.objects.filter(user=self.local_user).exists() models.Notification.objects.filter(user=self.local_user).exists()
) )
@ -110,15 +98,22 @@ class InboxCreate(TestCase):
) )
self.assertEqual(models.Notification.objects.get().notification_type, "MENTION") self.assertEqual(models.Notification.objects.get().notification_type, "MENTION")
def test_create_status_remote_note_with_reply(self): def test_create_status_remote_note_with_reply(self, _):
"""should only create it under the right circumstances""" """should only create it under the right circumstances"""
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
parent_status = models.Status.objects.create(
user=self.local_user,
content="Test status",
remote_id="https://example.com/status/1",
)
self.assertEqual(models.Status.objects.count(), 1) self.assertEqual(models.Status.objects.count(), 1)
self.assertFalse(models.Notification.objects.filter(user=self.local_user)) self.assertFalse(models.Notification.objects.filter(user=self.local_user))
datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_note.json") datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_note.json")
status_data = json.loads(datafile.read_bytes()) status_data = json.loads(datafile.read_bytes())
del status_data["tag"] del status_data["tag"]
status_data["inReplyTo"] = self.status.remote_id status_data["inReplyTo"] = parent_status.remote_id
activity = self.create_json activity = self.create_json
activity["object"] = status_data activity["object"] = status_data
@ -127,16 +122,15 @@ class InboxCreate(TestCase):
self.assertTrue(redis_mock.called) self.assertTrue(redis_mock.called)
status = models.Status.objects.last() status = models.Status.objects.last()
self.assertEqual(status.content, "test content in note") self.assertEqual(status.content, "test content in note")
self.assertEqual(status.reply_parent, self.status) self.assertEqual(status.reply_parent, parent_status)
self.assertTrue(models.Notification.objects.filter(user=self.local_user)) self.assertTrue(models.Notification.objects.filter(user=self.local_user))
self.assertEqual(models.Notification.objects.get().notification_type, "REPLY") self.assertEqual(models.Notification.objects.get().notification_type, "REPLY")
def test_create_rating(self): def test_create_rating(self, _):
"""a remote rating activity""" """a remote rating activity"""
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): book = models.Edition.objects.create(
book = models.Edition.objects.create( title="Test Book", remote_id="https://example.com/book/1"
title="Test Book", remote_id="https://example.com/book/1" )
)
activity = self.create_json activity = self.create_json
activity["object"] = { activity["object"] = {
"id": "https://example.com/user/mouse/reviewrating/12", "id": "https://example.com/user/mouse/reviewrating/12",
@ -149,8 +143,8 @@ class InboxCreate(TestCase):
"id": "https://example.com/user/mouse/reviewrating/12/replies", "id": "https://example.com/user/mouse/reviewrating/12/replies",
"type": "OrderedCollection", "type": "OrderedCollection",
"totalItems": 0, "totalItems": 0,
"first": "https://example.com/user/mouse/reviewrating/12/replies?page=1", "first": "https://example.com/u/mouse/reviewrating/12/replies?page=1",
"last": "https://example.com/user/mouse/reviewrating/12/replies?page=1", "last": "https://example.com/u/mouse/reviewrating/12/replies?page=1",
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
}, },
"inReplyTo": "", "inReplyTo": "",
@ -162,17 +156,14 @@ class InboxCreate(TestCase):
"rating": 3, "rating": 3,
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
} }
with patch("bookwyrm.preview_images.generate_edition_preview_image_task.delay"): with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
with patch( views.inbox.activity_task(activity)
"bookwyrm.activitystreams.ActivityStream.add_status" self.assertTrue(redis_mock.called)
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
rating = models.ReviewRating.objects.first() rating = models.ReviewRating.objects.first()
self.assertEqual(rating.book, book) self.assertEqual(rating.book, book)
self.assertEqual(rating.rating, 3.0) self.assertEqual(rating.rating, 3.0)
def test_create_list(self): def test_create_list(self, _):
"""a new list""" """a new list"""
activity = self.create_json activity = self.create_json
activity["object"] = { activity["object"] = {
@ -196,7 +187,7 @@ class InboxCreate(TestCase):
self.assertEqual(book_list.description, "summary text") self.assertEqual(book_list.description, "summary text")
self.assertEqual(book_list.remote_id, "https://example.com/list/22") self.assertEqual(book_list.remote_id, "https://example.com/list/22")
def test_create_unsupported_type(self): def test_create_unsupported_type(self, _):
"""ignore activities we know we can't handle""" """ignore activities we know we can't handle"""
activity = self.create_json activity = self.create_json
activity["object"] = { activity["object"] = {
@ -206,7 +197,7 @@ class InboxCreate(TestCase):
# just observer how it doesn't throw an error # just observer how it doesn't throw an error
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
def test_create_unknown_type(self): def test_create_unknown_type(self, _):
"""ignore activities we know we've never heard of""" """ignore activities we know we've never heard of"""
activity = self.create_json activity = self.create_json
activity["object"] = { activity["object"] = {

View file

@ -13,7 +13,7 @@ class InboxActivities(TestCase):
def setUp(self): def setUp(self):
"""basic user and book data""" """basic user and book data"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -21,18 +21,18 @@ class InboxActivities(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
self.local_user.remote_id = "https://example.com/user/mouse" self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False) self.local_user.save(broadcast=False, update_fields=["remote_id"])
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.activitystreams.ActivityStream.add_status"): with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
self.status = models.Status.objects.create( self.status = models.Status.objects.create(
user=self.remote_user, user=self.remote_user,
@ -48,8 +48,7 @@ class InboxActivities(TestCase):
"cc": ["https://example.com/user/mouse/followers"], "cc": ["https://example.com/user/mouse/followers"],
"object": {}, "object": {},
} }
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"): models.SiteSettings.objects.create()
models.SiteSettings.objects.create()
def test_delete_status(self): def test_delete_status(self):
"""remove a status""" """remove a status"""
@ -107,7 +106,8 @@ class InboxActivities(TestCase):
self.assertEqual(models.Notification.objects.count(), 1) self.assertEqual(models.Notification.objects.count(), 1)
self.assertEqual(models.Notification.objects.get(), notif) self.assertEqual(models.Notification.objects.get(), notif)
def test_delete_user(self): @patch("bookwyrm.suggested_users.remove_user_task.delay")
def test_delete_user(self, _):
"""delete a user""" """delete a user"""
self.assertTrue(models.User.objects.get(username="rat@example.com").is_active) self.assertTrue(models.User.objects.get(username="rat@example.com").is_active)
activity = { activity = {
@ -119,8 +119,7 @@ class InboxActivities(TestCase):
"object": self.remote_user.remote_id, "object": self.remote_user.remote_id,
} }
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): views.inbox.activity_task(activity)
views.inbox.activity_task(activity)
self.assertFalse(models.User.objects.get(username="rat@example.com").is_active) self.assertFalse(models.User.objects.get(username="rat@example.com").is_active)
def test_delete_user_unknown(self): def test_delete_user_unknown(self):

View file

@ -13,7 +13,7 @@ class InboxRelationships(TestCase):
def setUp(self): def setUp(self):
"""basic user and book data""" """basic user and book data"""
with patch("bookwyrm.preview_images.generate_user_preview_image_task.delay"): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -21,21 +21,20 @@ class InboxRelationships(TestCase):
local=True, local=True,
localname="mouse", localname="mouse",
) )
self.local_user.remote_id = "https://example.com/user/mouse" self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False) self.local_user.save(broadcast=False, update_fields=["remote_id"])
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
"rat@rat.com", "rat@rat.com",
"ratword", "ratword",
local=False, local=False,
remote_id="https://example.com/users/rat", remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox", inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox", outbox="https://example.com/users/rat/outbox",
) )
with patch("bookwyrm.preview_images.generate_site_preview_image_task.delay"): models.SiteSettings.objects.create()
models.SiteSettings.objects.create()
def test_follow(self): def test_follow(self):
"""remote user wants to follow local user""" """remote user wants to follow local user"""
@ -104,7 +103,9 @@ class InboxRelationships(TestCase):
} }
self.local_user.manually_approves_followers = True self.local_user.manually_approves_followers = True
self.local_user.save(broadcast=False) self.local_user.save(
broadcast=False, update_fields=["manually_approves_followers"]
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
@ -126,7 +127,9 @@ class InboxRelationships(TestCase):
def test_undo_follow_request(self): def test_undo_follow_request(self):
"""the requester cancels a follow request""" """the requester cancels a follow request"""
self.local_user.manually_approves_followers = True self.local_user.manually_approves_followers = True
self.local_user.save(broadcast=False) self.local_user.save(
broadcast=False, update_fields=["manually_approves_followers"]
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
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

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