mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-14 12:16:31 +00:00
Merge branch 'main' into book-format-choices
This commit is contained in:
commit
fd0f9324d3
166 changed files with 7987 additions and 4136 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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.
|
|
||||||
|
|
6
.github/workflows/black.yml
vendored
6
.github/workflows/black.yml
vendored
|
@ -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:
|
||||||
|
|
25
.github/workflows/django-tests.yml
vendored
25
.github/workflows/django-tests.yml
vendored
|
@ -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
|
||||||
|
|
6
.github/workflows/pylint.yml
vendored
6
.github/workflows/pylint.yml
vendored
|
@ -1,6 +1,10 @@
|
||||||
name: Pylint
|
name: Pylint
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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():
|
||||||
|
|
25
bookwyrm/management/commands/populate_suggestions.py
Normal file
25
bookwyrm/management/commands/populate_suggestions.py
Normal 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()
|
27
bookwyrm/migrations/0046_user_default_post_privacy.py
Normal file
27
bookwyrm/migrations/0046_user_default_post_privacy.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
126
bookwyrm/migrations/0077_auto_20210623_2155.py
Normal file
126
bookwyrm/migrations/0077_auto_20210623_2155.py
Normal 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;
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
]
|
34
bookwyrm/migrations/0078_add_shelved_date.py
Normal file
34
bookwyrm/migrations/0078_add_shelved_date.py
Normal 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),
|
||||||
|
]
|
13
bookwyrm/migrations/0079_merge_20210804_1746.py
Normal file
13
bookwyrm/migrations/0079_merge_20210804_1746.py
Normal 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 = []
|
|
@ -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
|
||||||
|
|
|
@ -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"]),)
|
||||||
|
|
|
@ -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)"""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
|
|
17
bookwyrm/storage_backends.py
Normal file
17
bookwyrm/storage_backends.py
Normal 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
221
bookwyrm/suggested_users.py
Normal 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)
|
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %} {% endif %}
|
{% else %} {% endif %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'feed/feed_layout.html' %}
|
{% extends 'feed/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
|
110
bookwyrm/templates/feed/layout.html
Normal file
110
bookwyrm/templates/feed/layout.html
Normal 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 %}
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'feed/feed_layout.html' %}
|
{% extends 'feed/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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:" %}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
{% comment %}
|
{% comment %}
|
||||||
@todo The author property needs to be an Organization or a Person. We’ll be using Thing which is the more generic ancestor.
|
@todo The author property needs to be an Organization or a Person. We’ll 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 %}
|
||||||
|
|
|
@ -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 }}">
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 }}"
|
||||||
|
|
|
@ -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
|
||||||
|
|
38
bookwyrm/templates/snippets/suggested_users.html
Normal file
38
bookwyrm/templates/snippets/suggested_users.html
Normal 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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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" %}">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
76
bookwyrm/tests/test_postgres.py
Normal file
76
bookwyrm/tests/test_postgres.py
Normal 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")
|
|
@ -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(
|
||||||
|
|
|
@ -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):
|
||||||
|
|
173
bookwyrm/tests/test_suggested_users.py
Normal file
173
bookwyrm/tests/test_suggested_users.py
Normal 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)
|
|
@ -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"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"] = {
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue