mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-01-05 14:58:43 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
5d7bd6a92b
99 changed files with 2298 additions and 1637 deletions
45
README.md
45
README.md
|
@ -3,7 +3,8 @@
|
|||
Social reading and reviewing, decentralized with ActivityPub
|
||||
|
||||
## Contents
|
||||
- [The overall idea](#the-overall-idea)
|
||||
- [Joining BookWyrm](#joining-bookwyrm)
|
||||
- [The overall idea](#the-overall-idea)
|
||||
- [What it is and isn't](#what-it-is-and-isnt)
|
||||
- [The role of federation](#the-role-of-federation)
|
||||
- [Features](#features)
|
||||
|
@ -13,42 +14,46 @@ Social reading and reviewing, decentralized with ActivityPub
|
|||
- [Book data](#book-data)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Joining BookWyrm
|
||||
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://github.com/mouse-reeve/bookwyrm/blob/main/instances.md) list.
|
||||
|
||||
I, the maintianer of this project, run https://bookwyrm.social, and I generally give out invite codes to those who ask by [email](mailto:mousereeve@riseup.net), [Mastodon direct message](https://friend.camp/@tripofmice), or [Twitter direct message](https://twitter.com/tripofmice).
|
||||
|
||||
## The overall idea
|
||||
### What it is and isn't
|
||||
BookWyrm is a platform for social reading! You can use it to track what you're reading, review books, and follow your friends. It isn't primarily meant for cataloguing or as a datasource for books, but it does do both of those things to some degree.
|
||||
|
||||
### The role of federation
|
||||
BookWyrm is built on [ActivityPub](http://activitypub.rocks/). With ActivityPub, it inter-operates with different instances of BookWyrm, and other ActivityPub compliant services, like Mastodon and Pixelfed. This means you can run an instance for your book club, and still follow your friend who posts on a server devoted to 20th century Russian speculative fiction. It also means that your friend on mastodon can read and comment on a book review that you post on your BookWyrm instance.
|
||||
BookWyrm is built on [ActivityPub](http://activitypub.rocks/). With ActivityPub, it inter-operates with different instances of BookWyrm, and other ActivityPub compliant services, like Mastodon. This means you can run an instance for your book club, and still follow your friend who posts on a server devoted to 20th century Russian speculative fiction. It also means that your friend on mastodon can read and comment on a book review that you post on your BookWyrm instance.
|
||||
|
||||
Federation makes it possible to have small, self-determining communities, in contrast to the monolithic service you find on GoodReads or Twitter. An instance can be focused on a particular type of literature, be just for use by people who are in a book club together, or anything else that brings people together. Each community can choose which other instances they want to federate with, and moderate and run their community autonomously. Check out https://runyourown.social/ to get a sense of the philosophy and logistics behind small, high-trust social networks.
|
||||
Federation makes it possible to have small, self-determining communities, in contrast to the monolithic service you find on GoodReads or Twitter. An instance can be focused on a particular interest, be just for a group of friends, or anything else that brings people together. Each community can choose which other instances they want to federate with, and moderate and run their community autonomously. Check out https://runyourown.social/ to get a sense of the philosophy and logistics behind small, high-trust social networks.
|
||||
|
||||
### Features
|
||||
Since the project is still in its early stages, not everything here is fully implemented. There is plenty of room for suggestions and ideas. Open an [issue](https://github.com/mouse-reeve/bookwyrm/issues) to get the conversation going!
|
||||
Since the project is still in its early stages, the features are growing every day, and there is plenty of room for suggestions and ideas. Open an [issue](https://github.com/mouse-reeve/bookwyrm/issues) to get the conversation going!
|
||||
- Posting about books
|
||||
- Compose reviews, with or without ratings, which are aggregated in the book page
|
||||
- Compose other kinds of statuses about books, such as:
|
||||
- Comments on a book
|
||||
- Quotes or excerpts
|
||||
- Recommenations of other books
|
||||
- Reply to statuses
|
||||
- Aggregate reviews of a book across connected BookWyrm instances
|
||||
- Differentiate local and federated reviews and rating
|
||||
- View aggregate reviews of a book across connected BookWyrm instances
|
||||
- Differentiate local and federated reviews and rating in your activity feed
|
||||
- Track reading activity
|
||||
- Shelve books on default "to-read," "currently reading," and "read" shelves
|
||||
- Create custom shelves
|
||||
- Store started reading/finished reading dates
|
||||
- Store started reading/finished reading dates, as well as progress updates along the way
|
||||
- Update followers about reading activity (optionally, and with granular privacy controls)
|
||||
- Create lists of books which can be open to submissions from anyone, curated, or only edited by the creator
|
||||
- Federation with ActivityPub
|
||||
- Broadcast and receive user statuses and activity
|
||||
- Broadcast copies of books that can be used as canonical data sources
|
||||
- Share book data between instances to create a networked database of metadata
|
||||
- Identify shared books across instances and aggregate related content
|
||||
- Follow and interact with users across BookWyrm instances
|
||||
- Inter-operate with non-BookWyrm ActivityPub services
|
||||
- Inter-operate with non-BookWyrm ActivityPub services (currently, Mastodon is supported)
|
||||
- Granular privacy controls
|
||||
- Local-only, followers-only, and public posting
|
||||
- Private, followers-only, and public privacy levels for posting, shelves, and lists
|
||||
- Option for users to manually approve followers
|
||||
- Allow blocking and flagging for moderation
|
||||
- Control which instances you want to federate with
|
||||
|
||||
## Setting up the developer environment
|
||||
|
||||
|
@ -88,6 +93,7 @@ This project is still young and isn't, at the momoment, very stable, so please p
|
|||
`cp .env.example .env`
|
||||
- Add your domain, email address, mailgun credentials
|
||||
- Set a secure redis password and secret key
|
||||
- Set a secure database password for postgres
|
||||
- Update your nginx configuration in `nginx/default.conf`
|
||||
- Replace `your-domain.com` with your domain name
|
||||
- Run the application (this should also set up a Certbot ssl cert for your domain)
|
||||
|
@ -99,6 +105,7 @@ This project is still young and isn't, at the momoment, very stable, so please p
|
|||
`docker-compose up -d`
|
||||
- Initialize the database
|
||||
`./bw-dev initdb`
|
||||
- Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U <databasename>` and saves the backup to a safe locationgi
|
||||
- Congrats! You did it, go to your domain and enjoy the fruits of your labors
|
||||
### Configure your instance
|
||||
- Register a user account in the applcation UI
|
||||
|
@ -114,21 +121,11 @@ This project is still young and isn't, at the momoment, very stable, so please p
|
|||
user.is_superuser = True
|
||||
user.save()
|
||||
```
|
||||
- Go to the admin panel (`/admin/bookwyrm/sitesettings/1/change` on your domain) and set your instance name, description, code of conduct, and toggle whether registration is open on your instance
|
||||
|
||||
|
||||
## Project structure
|
||||
All the url routing is in `bookwyrm/urls.py`. This includes the application views (your home page, user page, book page, etc), application endpoints (things that happen when you click buttons), and federation api endpoints (inboxes, outboxes, webfinger, etc).
|
||||
|
||||
The application views and actions are in `bookwyrm/views.py`. The internal actions call api handlers which deal with federating content. Outgoing messages (any action done by a user that is federated out), as well as outboxes, live in `bookwyrm/outgoing.py`, and all handlers for incoming messages, as well as inboxes and webfinger, live in `bookwyrm/incoming.py`. Connection to openlibrary.org to get book data is handled in `bookwyrm/connectors/openlibrary.py`. ActivityPub serialization is handled in the `bookwyrm/activitypub/` directory.
|
||||
|
||||
Celery is used for background tasks, which includes receiving incoming ActivityPub activities, ActivityPub broadcasting, and external data import.
|
||||
|
||||
The UI is all django templates because that is the default. You can replace it with a complex javascript framework over my ~dead body~ mild objections.
|
||||
- Go to the site settings (`/settings/site-settings` on your domain) and configure your instance name, description, code of conduct, and toggle whether registration is open on your instance
|
||||
|
||||
|
||||
## Book data
|
||||
The application is set up to get book data from arbitrary outside sources -- right now, it's only able to connect to OpenLibrary, but other connectors could be written. By default, a book is non-canonical copy of an OpenLibrary book, and will be updated with OpenLibrary if the data there changes. However, a book can edited and decoupled from its original data source, or added locally with no external data source.
|
||||
The application is set up to share book and author data between instances, and get book data from arbitrary outside sources. Right now, the only connector is to OpenLibrary, but other connectors could be written.
|
||||
|
||||
There are three concepts in the book data model:
|
||||
- `Book`, an abstract, high-level concept that could mean either a `Work` or an `Edition`. No data is saved as a `Book`, it serves as shared model for `Work` and `Edition`
|
||||
|
|
|
@ -93,7 +93,10 @@ class ActivityObject:
|
|||
with transaction.atomic():
|
||||
# we can't set many to many and reverse fields on an unsaved object
|
||||
try:
|
||||
instance.save()
|
||||
try:
|
||||
instance.save(broadcast=False)
|
||||
except TypeError:
|
||||
instance.save()
|
||||
except IntegrityError as e:
|
||||
raise ActivitySerializerError(e)
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class Note(ActivityObject):
|
|||
''' Note activity '''
|
||||
published: str
|
||||
attributedTo: str
|
||||
content: str
|
||||
content: str = ''
|
||||
to: List[str] = field(default_factory=lambda: [])
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
replies: Dict = field(default_factory=lambda: {})
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
''' send out activitypub messages '''
|
||||
import json
|
||||
from django.utils.http import http_date
|
||||
import requests
|
||||
|
||||
from bookwyrm import models, settings
|
||||
from bookwyrm.activitypub import ActivityEncoder
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.signatures import make_signature, make_digest
|
||||
|
||||
|
||||
def get_public_recipients(user, software=None):
|
||||
''' everybody and their public inboxes '''
|
||||
followers = user.followers.filter(local=False)
|
||||
if software:
|
||||
followers = followers.filter(bookwyrm_user=(software == 'bookwyrm'))
|
||||
|
||||
# we want shared inboxes when available
|
||||
shared = followers.filter(
|
||||
shared_inbox__isnull=False
|
||||
).values_list('shared_inbox', flat=True).distinct()
|
||||
|
||||
# if a user doesn't have a shared inbox, we need their personal inbox
|
||||
# iirc pixelfed doesn't have shared inboxes
|
||||
inboxes = followers.filter(
|
||||
shared_inbox__isnull=True
|
||||
).values_list('inbox', flat=True)
|
||||
|
||||
return list(shared) + list(inboxes)
|
||||
|
||||
|
||||
def broadcast(sender, activity, software=None, \
|
||||
privacy='public', direct_recipients=None):
|
||||
''' send out an event '''
|
||||
# start with parsing the direct recipients
|
||||
recipients = [u.inbox for u in direct_recipients or []]
|
||||
# and then add any other recipients
|
||||
if privacy == 'public':
|
||||
recipients += get_public_recipients(sender, software=software)
|
||||
broadcast_task.delay(
|
||||
sender.id,
|
||||
json.dumps(activity, cls=ActivityEncoder),
|
||||
recipients
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
def broadcast_task(sender_id, activity, recipients):
|
||||
''' the celery task for broadcast '''
|
||||
sender = models.User.objects.get(id=sender_id)
|
||||
errors = []
|
||||
for recipient in recipients:
|
||||
try:
|
||||
sign_and_send(sender, activity, recipient)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
errors.append({
|
||||
'error': str(e),
|
||||
'recipient': recipient,
|
||||
'activity': activity,
|
||||
})
|
||||
return errors
|
||||
|
||||
|
||||
def sign_and_send(sender, data, destination):
|
||||
''' crpyto whatever and http junk '''
|
||||
now = http_date()
|
||||
|
||||
if not sender.key_pair.private_key:
|
||||
# this shouldn't happen. it would be bad if it happened.
|
||||
raise ValueError('No private key found for sender')
|
||||
|
||||
digest = make_digest(data)
|
||||
|
||||
response = requests.post(
|
||||
destination,
|
||||
data=data,
|
||||
headers={
|
||||
'Date': now,
|
||||
'Digest': digest,
|
||||
'Signature': make_signature(sender, destination, now, digest),
|
||||
'Content-Type': 'application/activity+json; charset=utf-8',
|
||||
'User-Agent': settings.USER_AGENT,
|
||||
},
|
||||
)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
return response
|
|
@ -107,7 +107,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
if self.is_work_data(data):
|
||||
try:
|
||||
edition_data = self.get_edition_from_work_data(data)
|
||||
except KeyError:
|
||||
except (KeyError, ConnectorException):
|
||||
# hack: re-use the work data as the edition data
|
||||
# this is why remote ids aren't necessarily unique
|
||||
edition_data = data
|
||||
|
@ -116,7 +116,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
try:
|
||||
work_data = self.get_work_from_edition_data(data)
|
||||
work_data = dict_from_mappings(work_data, self.book_mappings)
|
||||
except KeyError:
|
||||
except (KeyError, ConnectorException):
|
||||
work_data = mapped_data
|
||||
edition_data = data
|
||||
|
||||
|
@ -145,8 +145,9 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
edition.connector = self.connector
|
||||
edition.save()
|
||||
|
||||
work.default_edition = edition
|
||||
work.save()
|
||||
if not work.default_edition:
|
||||
work.default_edition = edition
|
||||
work.save()
|
||||
|
||||
for author in self.get_authors_from_data(edition_data):
|
||||
edition.authors.add(author)
|
||||
|
@ -210,13 +211,20 @@ def get_data(url):
|
|||
'User-Agent': settings.USER_AGENT,
|
||||
},
|
||||
)
|
||||
except (RequestError, SSLError):
|
||||
except (RequestError, SSLError) as e:
|
||||
logger.exception(e)
|
||||
raise ConnectorException()
|
||||
|
||||
if not resp.ok:
|
||||
resp.raise_for_status()
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.exception(e)
|
||||
raise ConnectorException()
|
||||
try:
|
||||
data = resp.json()
|
||||
except ValueError:
|
||||
except ValueError as e:
|
||||
logger.exception(e)
|
||||
raise ConnectorException()
|
||||
|
||||
return data
|
||||
|
|
|
@ -3,9 +3,7 @@ import csv
|
|||
import logging
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from bookwyrm.models import ImportJob, ImportItem
|
||||
from bookwyrm.status import create_notification
|
||||
from bookwyrm.tasks import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -69,7 +67,6 @@ def import_data(job_id):
|
|||
item.fail_reason = 'Could not find a match for book'
|
||||
item.save()
|
||||
finally:
|
||||
create_notification(job.user, 'IMPORT', related_import=job)
|
||||
job.complete = True
|
||||
job.save()
|
||||
|
||||
|
@ -82,7 +79,7 @@ def handle_imported_book(user, item, include_reviews, privacy):
|
|||
return
|
||||
|
||||
existing_shelf = models.ShelfBook.objects.filter(
|
||||
book=item.book, added_by=user).exists()
|
||||
book=item.book, user=user).exists()
|
||||
|
||||
# shelve the book if it hasn't been shelved already
|
||||
if item.shelf and not existing_shelf:
|
||||
|
@ -90,9 +87,8 @@ def handle_imported_book(user, item, include_reviews, privacy):
|
|||
identifier=item.shelf,
|
||||
user=user
|
||||
)
|
||||
shelf_book = models.ShelfBook.objects.create(
|
||||
book=item.book, shelf=desired_shelf, added_by=user)
|
||||
broadcast(user, shelf_book.to_add_activity(user), privacy=privacy)
|
||||
models.ShelfBook.objects.create(
|
||||
book=item.book, shelf=desired_shelf, user=user)
|
||||
|
||||
for read in item.reads:
|
||||
# check for an existing readthrough with the same dates
|
||||
|
@ -114,7 +110,7 @@ def handle_imported_book(user, item, include_reviews, privacy):
|
|||
# we don't know the publication date of the review,
|
||||
# but "now" is a bad guess
|
||||
published_date_guess = item.date_read or item.date_added
|
||||
review = models.Review.objects.create(
|
||||
models.Review.objects.create(
|
||||
user=user,
|
||||
book=item.book,
|
||||
name=review_title,
|
||||
|
@ -123,6 +119,3 @@ def handle_imported_book(user, item, include_reviews, privacy):
|
|||
published_date=published_date_guess,
|
||||
privacy=privacy,
|
||||
)
|
||||
# we don't need to send out pure activities because non-bookwyrm
|
||||
# instances don't need this data
|
||||
broadcast(user, review.to_create_activity(user), privacy=privacy)
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|||
from django.views.decorators.http import require_POST
|
||||
import requests
|
||||
|
||||
from bookwyrm import activitypub, models, views
|
||||
from bookwyrm import activitypub, models
|
||||
from bookwyrm import status as status_builder
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.signatures import Signature
|
||||
|
@ -136,15 +136,8 @@ def handle_follow(activity):
|
|||
)
|
||||
# send the accept normally for a duplicate request
|
||||
|
||||
manually_approves = relationship.user_object.manually_approves_followers
|
||||
|
||||
status_builder.create_notification(
|
||||
relationship.user_object,
|
||||
'FOLLOW_REQUEST' if manually_approves else 'FOLLOW',
|
||||
related_user=relationship.user_subject
|
||||
)
|
||||
if not manually_approves:
|
||||
views.handle_accept(relationship)
|
||||
if not relationship.user_object.manually_approves_followers:
|
||||
relationship.accept()
|
||||
|
||||
|
||||
@app.task
|
||||
|
@ -256,27 +249,6 @@ def handle_create_status(activity):
|
|||
# it was discarded because it's not a bookwyrm type
|
||||
return
|
||||
|
||||
# create a notification if this is a reply
|
||||
notified = []
|
||||
if status.reply_parent and status.reply_parent.user.local:
|
||||
notified.append(status.reply_parent.user)
|
||||
status_builder.create_notification(
|
||||
status.reply_parent.user,
|
||||
'REPLY',
|
||||
related_user=status.user,
|
||||
related_status=status,
|
||||
)
|
||||
if status.mention_users.exists():
|
||||
for mentioned_user in status.mention_users.all():
|
||||
if not mentioned_user.local or mentioned_user in notified:
|
||||
continue
|
||||
status_builder.create_notification(
|
||||
mentioned_user,
|
||||
'MENTION',
|
||||
related_user=status.user,
|
||||
related_status=status,
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_delete_status(activity):
|
||||
|
@ -309,13 +281,6 @@ def handle_favorite(activity):
|
|||
if fav.user.local:
|
||||
return
|
||||
|
||||
status_builder.create_notification(
|
||||
fav.status.user,
|
||||
'FAVORITE',
|
||||
related_user=fav.user,
|
||||
related_status=fav.status,
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_unfavorite(activity):
|
||||
|
@ -332,19 +297,11 @@ def handle_unfavorite(activity):
|
|||
def handle_boost(activity):
|
||||
''' someone gave us a boost! '''
|
||||
try:
|
||||
boost = activitypub.Boost(**activity).to_model(models.Boost)
|
||||
activitypub.Boost(**activity).to_model(models.Boost)
|
||||
except activitypub.ActivitySerializerError:
|
||||
# this probably just means we tried to boost an unknown status
|
||||
return
|
||||
|
||||
if not boost.user.local:
|
||||
status_builder.create_notification(
|
||||
boost.boosted_status.user,
|
||||
'BOOST',
|
||||
related_user=boost.user,
|
||||
related_status=boost.boosted_status,
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
def handle_unboost(activity):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 3.0.7 on 2020-11-30 18:19
|
||||
|
||||
import bookwyrm.models.base_model
|
||||
import bookwyrm.models.activitypub_mixin
|
||||
import bookwyrm.models.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
@ -38,7 +38,7 @@ class Migration(migrations.Migration):
|
|||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(bookwyrm.models.base_model.ActivitypubMixin, models.Model),
|
||||
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 3.0.7 on 2021-01-31 16:14
|
||||
|
||||
import bookwyrm.models.base_model
|
||||
import bookwyrm.models.activitypub_mixin
|
||||
import bookwyrm.models.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
@ -29,7 +29,7 @@ class Migration(migrations.Migration):
|
|||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(bookwyrm.models.base_model.OrderedCollectionMixin, models.Model),
|
||||
bases=(bookwyrm.models.activitypub_mixin.OrderedCollectionMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ListItem',
|
||||
|
@ -50,7 +50,7 @@ class Migration(migrations.Migration):
|
|||
'ordering': ('-created_date',),
|
||||
'unique_together': {('book', 'book_list')},
|
||||
},
|
||||
bases=(bookwyrm.models.base_model.ActivitypubMixin, models.Model),
|
||||
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='list',
|
||||
|
|
23
bookwyrm/migrations/0043_auto_20210204_2223.py
Normal file
23
bookwyrm/migrations/0043_auto_20210204_2223.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.0.7 on 2021-02-04 22:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0042_auto_20210201_2108'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='listitem',
|
||||
old_name='added_by',
|
||||
new_name='user',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='shelfbook',
|
||||
old_name='added_by',
|
||||
new_name='user',
|
||||
),
|
||||
]
|
30
bookwyrm/migrations/0044_auto_20210207_1924.py
Normal file
30
bookwyrm/migrations/0044_auto_20210207_1924.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Generated by Django 3.0.7 on 2021-02-07 19:24
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
import django.db.models.deletion
|
||||
|
||||
def set_user(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
shelfbook = app_registry.get_model('bookwyrm', 'ShelfBook')
|
||||
for item in shelfbook.objects.using(db_alias).filter(user__isnull=True):
|
||||
item.user = item.shelf.user
|
||||
item.save(broadcast=False)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0043_auto_20210204_2223'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_user, lambda x, y: None),
|
||||
migrations.AlterField(
|
||||
model_name='shelfbook',
|
||||
name='user',
|
||||
field=bookwyrm.models.fields.ForeignKey(default=2, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
58
bookwyrm/migrations/0045_auto_20210210_2114.py
Normal file
58
bookwyrm/migrations/0045_auto_20210210_2114.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Generated by Django 3.0.7 on 2021-02-10 21:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0044_auto_20210207_1924'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='notification',
|
||||
name='notification_type_valid',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='notification',
|
||||
name='related_list_item',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ListItem'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='notification_type',
|
||||
field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('MENTION', 'Mention'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request'), ('BOOST', 'Boost'), ('IMPORT', 'Import'), ('ADD', 'Add')], max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='related_book',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.Edition'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='related_import',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.ImportJob'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='related_status',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='related_user',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_user', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='notification',
|
||||
constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'MENTION', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST', 'BOOST', 'IMPORT', 'ADD']), name='notification_type_valid'),
|
||||
),
|
||||
]
|
497
bookwyrm/models/activitypub_mixin.py
Normal file
497
bookwyrm/models/activitypub_mixin.py
Normal file
|
@ -0,0 +1,497 @@
|
|||
''' activitypub model functionality '''
|
||||
from base64 import b64encode
|
||||
from functools import reduce
|
||||
import json
|
||||
import operator
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
import requests
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import pkcs1_15
|
||||
from Crypto.Hash import SHA256
|
||||
from django.apps import apps
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.utils.http import http_date
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import USER_AGENT, PAGE_LENGTH
|
||||
from bookwyrm.signatures import make_signature, make_digest
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.models.fields import ImageField, ManyToManyField
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# I tried to separate these classes into mutliple files but I kept getting
|
||||
# circular import errors so I gave up. I'm sure it could be done though!
|
||||
class ActivitypubMixin:
|
||||
''' add this mixin for models that are AP serializable '''
|
||||
activity_serializer = lambda: {}
|
||||
reverse_unfurl = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
''' collect some info on model fields '''
|
||||
self.image_fields = []
|
||||
self.many_to_many_fields = []
|
||||
self.simple_fields = [] # "simple"
|
||||
# sort model fields by type
|
||||
for field in self._meta.get_fields():
|
||||
if not hasattr(field, 'field_to_activity'):
|
||||
continue
|
||||
|
||||
if isinstance(field, ImageField):
|
||||
self.image_fields.append(field)
|
||||
elif isinstance(field, ManyToManyField):
|
||||
self.many_to_many_fields.append(field)
|
||||
else:
|
||||
self.simple_fields.append(field)
|
||||
|
||||
# a list of allll the serializable fields
|
||||
self.activity_fields = self.image_fields + \
|
||||
self.many_to_many_fields + self.simple_fields
|
||||
|
||||
# these are separate to avoid infinite recursion issues
|
||||
self.deserialize_reverse_fields = self.deserialize_reverse_fields \
|
||||
if hasattr(self, 'deserialize_reverse_fields') else []
|
||||
self.serialize_reverse_fields = self.serialize_reverse_fields \
|
||||
if hasattr(self, 'serialize_reverse_fields') else []
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@classmethod
|
||||
def find_existing_by_remote_id(cls, remote_id):
|
||||
''' look up a remote id in the db '''
|
||||
return cls.find_existing({'id': remote_id})
|
||||
|
||||
@classmethod
|
||||
def find_existing(cls, data):
|
||||
''' compare data to fields that can be used for deduplation.
|
||||
This always includes remote_id, but can also be unique identifiers
|
||||
like an isbn for an edition '''
|
||||
filters = []
|
||||
# grabs all the data from the model to create django queryset filters
|
||||
for field in cls._meta.get_fields():
|
||||
if not hasattr(field, 'deduplication_field') or \
|
||||
not field.deduplication_field:
|
||||
continue
|
||||
|
||||
value = data.get(field.get_activitypub_field())
|
||||
if not value:
|
||||
continue
|
||||
filters.append({field.name: value})
|
||||
|
||||
if hasattr(cls, 'origin_id') and 'id' in data:
|
||||
# kinda janky, but this handles special case for books
|
||||
filters.append({'origin_id': data['id']})
|
||||
|
||||
if not filters:
|
||||
# if there are no deduplication fields, it will match the first
|
||||
# item no matter what. this shouldn't happen but just in case.
|
||||
return None
|
||||
|
||||
objects = cls.objects
|
||||
if hasattr(objects, 'select_subclasses'):
|
||||
objects = objects.select_subclasses()
|
||||
|
||||
# an OR operation on all the match fields, sorry for the dense syntax
|
||||
match = objects.filter(
|
||||
reduce(operator.or_, (Q(**f) for f in filters))
|
||||
)
|
||||
# there OUGHT to be only one match
|
||||
return match.first()
|
||||
|
||||
|
||||
def broadcast(self, activity, sender, software=None):
|
||||
''' send out an activity '''
|
||||
broadcast_task.delay(
|
||||
sender.id,
|
||||
json.dumps(activity, cls=activitypub.ActivityEncoder),
|
||||
self.get_recipients(software=software)
|
||||
)
|
||||
|
||||
|
||||
def get_recipients(self, software=None):
|
||||
''' figure out which inbox urls to post to '''
|
||||
# first we have to figure out who should receive this activity
|
||||
privacy = self.privacy if hasattr(self, 'privacy') else 'public'
|
||||
# is this activity owned by a user (statuses, lists, shelves), or is it
|
||||
# general to the instance (like books)
|
||||
user = self.user if hasattr(self, 'user') else None
|
||||
user_model = apps.get_model('bookwyrm.User', require_ready=True)
|
||||
if not user and isinstance(self, user_model):
|
||||
# or maybe the thing itself is a user
|
||||
user = self
|
||||
# find anyone who's tagged in a status, for example
|
||||
mentions = self.recipients if hasattr(self, 'recipients') else []
|
||||
|
||||
# we always send activities to explicitly mentioned users' inboxes
|
||||
recipients = [u.inbox for u in mentions or []]
|
||||
|
||||
# unless it's a dm, all the followers should receive the activity
|
||||
if privacy != 'direct':
|
||||
# we will send this out to a subset of all remote users
|
||||
queryset = user_model.objects.filter(
|
||||
local=False,
|
||||
)
|
||||
# filter users first by whether they're using the desired software
|
||||
# this lets us send book updates only to other bw servers
|
||||
if software:
|
||||
queryset = queryset.filter(
|
||||
bookwyrm_user=(software == 'bookwyrm')
|
||||
)
|
||||
# if there's a user, we only want to send to the user's followers
|
||||
if user:
|
||||
queryset = queryset.filter(following=user)
|
||||
|
||||
# ideally, we will send to shared inboxes for efficiency
|
||||
shared_inboxes = queryset.filter(
|
||||
shared_inbox__isnull=False
|
||||
).values_list('shared_inbox', flat=True).distinct()
|
||||
# but not everyone has a shared inbox
|
||||
inboxes = queryset.filter(
|
||||
shared_inbox__isnull=True
|
||||
).values_list('inbox', flat=True)
|
||||
recipients += list(shared_inboxes) + list(inboxes)
|
||||
return recipients
|
||||
|
||||
|
||||
def to_activity(self):
|
||||
''' convert from a model to an activity '''
|
||||
activity = generate_activity(self)
|
||||
return self.activity_serializer(**activity).serialize()
|
||||
|
||||
|
||||
class ObjectMixin(ActivitypubMixin):
|
||||
''' add this mixin for object models that are AP serializable '''
|
||||
def save(self, *args, created=None, **kwargs):
|
||||
''' broadcast created/updated/deleted objects as appropriate '''
|
||||
broadcast = kwargs.get('broadcast', True)
|
||||
# this bonus kwarg woul cause an error in the base save method
|
||||
if 'broadcast' in kwargs:
|
||||
del kwargs['broadcast']
|
||||
|
||||
created = created or not bool(self.id)
|
||||
# first off, we want to save normally no matter what
|
||||
super().save(*args, **kwargs)
|
||||
if not broadcast:
|
||||
return
|
||||
|
||||
# this will work for objects owned by a user (lists, shelves)
|
||||
user = self.user if hasattr(self, 'user') else None
|
||||
|
||||
if created:
|
||||
# broadcast Create activities for objects owned by a local user
|
||||
if not user or not user.local:
|
||||
return
|
||||
|
||||
try:
|
||||
software = None
|
||||
# do we have a "pure" activitypub version of this for mastodon?
|
||||
if hasattr(self, 'pure_content'):
|
||||
pure_activity = self.to_create_activity(user, pure=True)
|
||||
self.broadcast(pure_activity, user, software='other')
|
||||
software = 'bookwyrm'
|
||||
# sends to BW only if we just did a pure version for masto
|
||||
activity = self.to_create_activity(user)
|
||||
self.broadcast(activity, user, software=software)
|
||||
except KeyError:
|
||||
# janky as heck, this catches the mutliple inheritence chain
|
||||
# for boosts and ignores this auxilliary broadcast
|
||||
return
|
||||
return
|
||||
|
||||
# --- updating an existing object
|
||||
if not user:
|
||||
# users don't have associated users, they ARE users
|
||||
user_model = apps.get_model('bookwyrm.User', require_ready=True)
|
||||
if isinstance(self, user_model):
|
||||
user = self
|
||||
# book data tracks last editor
|
||||
elif hasattr(self, 'last_edited_by'):
|
||||
user = self.last_edited_by
|
||||
# again, if we don't know the user or they're remote, don't bother
|
||||
if not user or not user.local:
|
||||
return
|
||||
|
||||
# is this a deletion?
|
||||
if hasattr(self, 'deleted') and self.deleted:
|
||||
activity = self.to_delete_activity(user)
|
||||
else:
|
||||
activity = self.to_update_activity(user)
|
||||
self.broadcast(activity, user)
|
||||
|
||||
|
||||
def to_create_activity(self, user, **kwargs):
|
||||
''' returns the object wrapped in a Create activity '''
|
||||
activity_object = self.to_activity(**kwargs)
|
||||
|
||||
signature = None
|
||||
create_id = self.remote_id + '/activity'
|
||||
if 'content' in activity_object and activity_object['content']:
|
||||
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
|
||||
content = activity_object['content']
|
||||
signed_message = signer.sign(SHA256.new(content.encode('utf8')))
|
||||
|
||||
signature = activitypub.Signature(
|
||||
creator='%s#main-key' % user.remote_id,
|
||||
created=activity_object['published'],
|
||||
signatureValue=b64encode(signed_message).decode('utf8')
|
||||
)
|
||||
|
||||
return activitypub.Create(
|
||||
id=create_id,
|
||||
actor=user.remote_id,
|
||||
to=activity_object['to'],
|
||||
cc=activity_object['cc'],
|
||||
object=activity_object,
|
||||
signature=signature,
|
||||
).serialize()
|
||||
|
||||
|
||||
def to_delete_activity(self, user):
|
||||
''' notice of deletion '''
|
||||
return activitypub.Delete(
|
||||
id=self.remote_id + '/activity',
|
||||
actor=user.remote_id,
|
||||
to=['%s/followers' % user.remote_id],
|
||||
cc=['https://www.w3.org/ns/activitystreams#Public'],
|
||||
object=self.to_activity(),
|
||||
).serialize()
|
||||
|
||||
|
||||
def to_update_activity(self, user):
|
||||
''' wrapper for Updates to an activity '''
|
||||
activity_id = '%s#update/%s' % (self.remote_id, uuid4())
|
||||
return activitypub.Update(
|
||||
id=activity_id,
|
||||
actor=user.remote_id,
|
||||
to=['https://www.w3.org/ns/activitystreams#Public'],
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
|
||||
|
||||
class OrderedCollectionPageMixin(ObjectMixin):
|
||||
''' just the paginator utilities, so you don't HAVE to
|
||||
override ActivitypubMixin's to_activity (ie, for outbox) '''
|
||||
@property
|
||||
def collection_remote_id(self):
|
||||
''' this can be overriden if there's a special remote id, ie outbox '''
|
||||
return self.remote_id
|
||||
|
||||
|
||||
def to_ordered_collection(self, queryset, \
|
||||
remote_id=None, page=False, collection_only=False, **kwargs):
|
||||
''' an ordered collection of whatevers '''
|
||||
if not queryset.ordered:
|
||||
raise RuntimeError('queryset must be ordered')
|
||||
|
||||
remote_id = remote_id or self.remote_id
|
||||
if page:
|
||||
return to_ordered_collection_page(
|
||||
queryset, remote_id, **kwargs)
|
||||
|
||||
if collection_only or not hasattr(self, 'activity_serializer'):
|
||||
serializer = activitypub.OrderedCollection
|
||||
activity = {}
|
||||
else:
|
||||
serializer = self.activity_serializer
|
||||
# a dict from the model fields
|
||||
activity = generate_activity(self)
|
||||
|
||||
if remote_id:
|
||||
activity['id'] = remote_id
|
||||
|
||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||
# add computed fields specific to orderd collections
|
||||
activity['totalItems'] = paginated.count
|
||||
activity['first'] = '%s?page=1' % remote_id
|
||||
activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)
|
||||
|
||||
return serializer(**activity).serialize()
|
||||
|
||||
|
||||
class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||
''' extends activitypub models to work as ordered collections '''
|
||||
@property
|
||||
def collection_queryset(self):
|
||||
''' usually an ordered collection model aggregates a different model '''
|
||||
raise NotImplementedError('Model must define collection_queryset')
|
||||
|
||||
activity_serializer = activitypub.OrderedCollection
|
||||
|
||||
def to_activity(self, **kwargs):
|
||||
''' an ordered collection of the specified model queryset '''
|
||||
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
||||
|
||||
|
||||
class CollectionItemMixin(ActivitypubMixin):
|
||||
''' for items that are part of an (Ordered)Collection '''
|
||||
activity_serializer = activitypub.Add
|
||||
object_field = collection_field = None
|
||||
|
||||
def save(self, *args, broadcast=True, **kwargs):
|
||||
''' broadcast updated '''
|
||||
created = not bool(self.id)
|
||||
# first off, we want to save normally no matter what
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# these shouldn't be edited, only created and deleted
|
||||
if not broadcast or not created or not self.user.local:
|
||||
return
|
||||
|
||||
# adding an obj to the collection
|
||||
activity = self.to_add_activity()
|
||||
self.broadcast(activity, self.user)
|
||||
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
''' broadcast a remove activity '''
|
||||
activity = self.to_remove_activity()
|
||||
super().delete(*args, **kwargs)
|
||||
self.broadcast(activity, self.user)
|
||||
|
||||
|
||||
def to_add_activity(self):
|
||||
''' AP for shelving a book'''
|
||||
object_field = getattr(self, self.object_field)
|
||||
collection_field = getattr(self, self.collection_field)
|
||||
return activitypub.Add(
|
||||
id='%s#add' % self.remote_id,
|
||||
actor=self.user.remote_id,
|
||||
object=object_field.to_activity(),
|
||||
target=collection_field.remote_id
|
||||
).serialize()
|
||||
|
||||
def to_remove_activity(self):
|
||||
''' AP for un-shelving a book'''
|
||||
object_field = getattr(self, self.object_field)
|
||||
collection_field = getattr(self, self.collection_field)
|
||||
return activitypub.Remove(
|
||||
id='%s#remove' % self.remote_id,
|
||||
actor=self.user.remote_id,
|
||||
object=object_field.to_activity(),
|
||||
target=collection_field.remote_id
|
||||
).serialize()
|
||||
|
||||
|
||||
class ActivityMixin(ActivitypubMixin):
|
||||
''' add this mixin for models that are AP serializable '''
|
||||
def save(self, *args, broadcast=True, **kwargs):
|
||||
''' broadcast activity '''
|
||||
super().save(*args, **kwargs)
|
||||
user = self.user if hasattr(self, 'user') else self.user_subject
|
||||
if broadcast and user.local:
|
||||
self.broadcast(self.to_activity(), user)
|
||||
|
||||
|
||||
def delete(self, *args, broadcast=True, **kwargs):
|
||||
''' nevermind, undo that activity '''
|
||||
user = self.user if hasattr(self, 'user') else self.user_subject
|
||||
if broadcast and user.local:
|
||||
self.broadcast(self.to_undo_activity(), user)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
def to_undo_activity(self):
|
||||
''' undo an action '''
|
||||
user = self.user if hasattr(self, 'user') else self.user_subject
|
||||
return activitypub.Undo(
|
||||
id='%s#undo' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
|
||||
|
||||
def generate_activity(obj):
|
||||
''' go through the fields on an object '''
|
||||
activity = {}
|
||||
for field in obj.activity_fields:
|
||||
field.set_activity_from_field(activity, obj)
|
||||
|
||||
if hasattr(obj, 'serialize_reverse_fields'):
|
||||
# for example, editions of a work
|
||||
for model_field_name, activity_field_name, sort_field in \
|
||||
obj.serialize_reverse_fields:
|
||||
related_field = getattr(obj, model_field_name)
|
||||
activity[activity_field_name] = \
|
||||
unfurl_related_field(related_field, sort_field)
|
||||
|
||||
if not activity.get('id'):
|
||||
activity['id'] = obj.get_remote_id()
|
||||
return activity
|
||||
|
||||
|
||||
def unfurl_related_field(related_field, sort_field=None):
|
||||
''' load reverse lookups (like public key owner or Status attachment '''
|
||||
if hasattr(related_field, 'all'):
|
||||
return [unfurl_related_field(i) for i in related_field.order_by(
|
||||
sort_field).all()]
|
||||
if related_field.reverse_unfurl:
|
||||
return related_field.field_to_activity()
|
||||
return related_field.remote_id
|
||||
|
||||
|
||||
@app.task
|
||||
def broadcast_task(sender_id, activity, recipients):
|
||||
''' the celery task for broadcast '''
|
||||
user_model = apps.get_model('bookwyrm.User', require_ready=True)
|
||||
sender = user_model.objects.get(id=sender_id)
|
||||
for recipient in recipients:
|
||||
try:
|
||||
sign_and_send(sender, activity, recipient)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.exception(e)
|
||||
|
||||
|
||||
def sign_and_send(sender, data, destination):
|
||||
''' crpyto whatever and http junk '''
|
||||
now = http_date()
|
||||
|
||||
if not sender.key_pair.private_key:
|
||||
# this shouldn't happen. it would be bad if it happened.
|
||||
raise ValueError('No private key found for sender')
|
||||
|
||||
digest = make_digest(data)
|
||||
|
||||
response = requests.post(
|
||||
destination,
|
||||
data=data,
|
||||
headers={
|
||||
'Date': now,
|
||||
'Digest': digest,
|
||||
'Signature': make_signature(sender, destination, now, digest),
|
||||
'Content-Type': 'application/activity+json; charset=utf-8',
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def to_ordered_collection_page(
|
||||
queryset, remote_id, id_only=False, page=1, **kwargs):
|
||||
''' serialize and pagiante a queryset '''
|
||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||
|
||||
activity_page = paginated.page(page)
|
||||
if id_only:
|
||||
items = [s.remote_id for s in activity_page.object_list]
|
||||
else:
|
||||
items = [s.to_activity() for s in activity_page.object_list]
|
||||
|
||||
prev_page = next_page = None
|
||||
if activity_page.has_next():
|
||||
next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())
|
||||
if activity_page.has_previous():
|
||||
prev_page = '%s?page=%d' % \
|
||||
(remote_id, activity_page.previous_page_number())
|
||||
return activitypub.OrderedCollectionPage(
|
||||
id='%s?page=%s' % (remote_id, page),
|
||||
partOf=remote_id,
|
||||
orderedItems=items,
|
||||
next=next_page,
|
||||
prev=prev_page
|
||||
).serialize()
|
|
@ -2,7 +2,7 @@
|
|||
from django.db import models
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .base_model import ActivitypubMixin
|
||||
from .activitypub_mixin import ActivitypubMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
|
|
|
@ -1,20 +1,9 @@
|
|||
''' base model with default fields '''
|
||||
from base64 import b64encode
|
||||
from functools import reduce
|
||||
import operator
|
||||
from uuid import uuid4
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import pkcs1_15
|
||||
from Crypto.Hash import SHA256
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.dispatch import receiver
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN, PAGE_LENGTH
|
||||
from .fields import ImageField, ManyToManyField, RemoteIdField
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .fields import RemoteIdField
|
||||
|
||||
|
||||
class BookWyrmModel(models.Model):
|
||||
|
@ -27,7 +16,7 @@ class BookWyrmModel(models.Model):
|
|||
''' generate a url that resolves to the local object '''
|
||||
base_path = 'https://%s' % DOMAIN
|
||||
if hasattr(self, 'user'):
|
||||
base_path = self.user.remote_id
|
||||
base_path = '%s%s' % (base_path, self.user.local_path)
|
||||
model_name = type(self).__name__.lower()
|
||||
return '%s/%s/%d' % (base_path, model_name, self.id)
|
||||
|
||||
|
@ -49,254 +38,7 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
|||
return
|
||||
if not instance.remote_id:
|
||||
instance.remote_id = instance.get_remote_id()
|
||||
instance.save()
|
||||
|
||||
|
||||
def unfurl_related_field(related_field, sort_field=None):
|
||||
''' load reverse lookups (like public key owner or Status attachment '''
|
||||
if hasattr(related_field, 'all'):
|
||||
return [unfurl_related_field(i) for i in related_field.order_by(
|
||||
sort_field).all()]
|
||||
if related_field.reverse_unfurl:
|
||||
return related_field.field_to_activity()
|
||||
return related_field.remote_id
|
||||
|
||||
|
||||
class ActivitypubMixin:
|
||||
''' add this mixin for models that are AP serializable '''
|
||||
activity_serializer = lambda: {}
|
||||
reverse_unfurl = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
''' collect some info on model fields '''
|
||||
self.image_fields = []
|
||||
self.many_to_many_fields = []
|
||||
self.simple_fields = [] # "simple"
|
||||
for field in self._meta.get_fields():
|
||||
if not hasattr(field, 'field_to_activity'):
|
||||
continue
|
||||
|
||||
if isinstance(field, ImageField):
|
||||
self.image_fields.append(field)
|
||||
elif isinstance(field, ManyToManyField):
|
||||
self.many_to_many_fields.append(field)
|
||||
else:
|
||||
self.simple_fields.append(field)
|
||||
|
||||
self.activity_fields = self.image_fields + \
|
||||
self.many_to_many_fields + self.simple_fields
|
||||
|
||||
self.deserialize_reverse_fields = self.deserialize_reverse_fields \
|
||||
if hasattr(self, 'deserialize_reverse_fields') else []
|
||||
self.serialize_reverse_fields = self.serialize_reverse_fields \
|
||||
if hasattr(self, 'serialize_reverse_fields') else []
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@classmethod
|
||||
def find_existing_by_remote_id(cls, remote_id):
|
||||
''' look up a remote id in the db '''
|
||||
return cls.find_existing({'id': remote_id})
|
||||
|
||||
@classmethod
|
||||
def find_existing(cls, data):
|
||||
''' compare data to fields that can be used for deduplation.
|
||||
This always includes remote_id, but can also be unique identifiers
|
||||
like an isbn for an edition '''
|
||||
filters = []
|
||||
for field in cls._meta.get_fields():
|
||||
if not hasattr(field, 'deduplication_field') or \
|
||||
not field.deduplication_field:
|
||||
continue
|
||||
|
||||
value = data.get(field.get_activitypub_field())
|
||||
if not value:
|
||||
continue
|
||||
filters.append({field.name: value})
|
||||
|
||||
if hasattr(cls, 'origin_id') and 'id' in data:
|
||||
# kinda janky, but this handles special case for books
|
||||
filters.append({'origin_id': data['id']})
|
||||
|
||||
if not filters:
|
||||
# if there are no deduplication fields, it will match the first
|
||||
# item no matter what. this shouldn't happen but just in case.
|
||||
return None
|
||||
|
||||
objects = cls.objects
|
||||
if hasattr(objects, 'select_subclasses'):
|
||||
objects = objects.select_subclasses()
|
||||
|
||||
# an OR operation on all the match fields
|
||||
match = objects.filter(
|
||||
reduce(
|
||||
operator.or_, (Q(**f) for f in filters)
|
||||
)
|
||||
)
|
||||
# there OUGHT to be only one match
|
||||
return match.first()
|
||||
|
||||
|
||||
def to_activity(self):
|
||||
''' convert from a model to an activity '''
|
||||
activity = generate_activity(self)
|
||||
return self.activity_serializer(**activity).serialize()
|
||||
|
||||
|
||||
def to_create_activity(self, user, **kwargs):
|
||||
''' returns the object wrapped in a Create activity '''
|
||||
activity_object = self.to_activity(**kwargs)
|
||||
|
||||
signature = None
|
||||
create_id = self.remote_id + '/activity'
|
||||
if 'content' in activity_object:
|
||||
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
|
||||
content = activity_object['content']
|
||||
signed_message = signer.sign(SHA256.new(content.encode('utf8')))
|
||||
|
||||
signature = activitypub.Signature(
|
||||
creator='%s#main-key' % user.remote_id,
|
||||
created=activity_object['published'],
|
||||
signatureValue=b64encode(signed_message).decode('utf8')
|
||||
)
|
||||
|
||||
return activitypub.Create(
|
||||
id=create_id,
|
||||
actor=user.remote_id,
|
||||
to=activity_object['to'],
|
||||
cc=activity_object['cc'],
|
||||
object=activity_object,
|
||||
signature=signature,
|
||||
).serialize()
|
||||
|
||||
|
||||
def to_delete_activity(self, user):
|
||||
''' notice of deletion '''
|
||||
return activitypub.Delete(
|
||||
id=self.remote_id + '/activity',
|
||||
actor=user.remote_id,
|
||||
to=['%s/followers' % user.remote_id],
|
||||
cc=['https://www.w3.org/ns/activitystreams#Public'],
|
||||
object=self.to_activity(),
|
||||
).serialize()
|
||||
|
||||
|
||||
def to_update_activity(self, user):
|
||||
''' wrapper for Updates to an activity '''
|
||||
activity_id = '%s#update/%s' % (self.remote_id, uuid4())
|
||||
return activitypub.Update(
|
||||
id=activity_id,
|
||||
actor=user.remote_id,
|
||||
to=['https://www.w3.org/ns/activitystreams#Public'],
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
|
||||
|
||||
def to_undo_activity(self, user):
|
||||
''' undo an action '''
|
||||
return activitypub.Undo(
|
||||
id='%s#undo' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
|
||||
|
||||
class OrderedCollectionPageMixin(ActivitypubMixin):
|
||||
''' just the paginator utilities, so you don't HAVE to
|
||||
override ActivitypubMixin's to_activity (ie, for outbox '''
|
||||
@property
|
||||
def collection_remote_id(self):
|
||||
''' this can be overriden if there's a special remote id, ie outbox '''
|
||||
return self.remote_id
|
||||
|
||||
|
||||
def to_ordered_collection(self, queryset, \
|
||||
remote_id=None, page=False, collection_only=False, **kwargs):
|
||||
''' an ordered collection of whatevers '''
|
||||
if not queryset.ordered:
|
||||
raise RuntimeError('queryset must be ordered')
|
||||
|
||||
remote_id = remote_id or self.remote_id
|
||||
if page:
|
||||
return to_ordered_collection_page(
|
||||
queryset, remote_id, **kwargs)
|
||||
|
||||
if collection_only or not hasattr(self, 'activity_serializer'):
|
||||
serializer = activitypub.OrderedCollection
|
||||
activity = {}
|
||||
else:
|
||||
serializer = self.activity_serializer
|
||||
# a dict from the model fields
|
||||
activity = generate_activity(self)
|
||||
|
||||
if remote_id:
|
||||
activity['id'] = remote_id
|
||||
|
||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||
# add computed fields specific to orderd collections
|
||||
activity['totalItems'] = paginated.count
|
||||
activity['first'] = '%s?page=1' % remote_id
|
||||
activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)
|
||||
|
||||
return serializer(**activity).serialize()
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def to_ordered_collection_page(
|
||||
queryset, remote_id, id_only=False, page=1, **kwargs):
|
||||
''' serialize and pagiante a queryset '''
|
||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||
|
||||
activity_page = paginated.page(page)
|
||||
if id_only:
|
||||
items = [s.remote_id for s in activity_page.object_list]
|
||||
else:
|
||||
items = [s.to_activity() for s in activity_page.object_list]
|
||||
|
||||
prev_page = next_page = None
|
||||
if activity_page.has_next():
|
||||
next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())
|
||||
if activity_page.has_previous():
|
||||
prev_page = '%s?page=%d' % \
|
||||
(remote_id, activity_page.previous_page_number())
|
||||
return activitypub.OrderedCollectionPage(
|
||||
id='%s?page=%s' % (remote_id, page),
|
||||
partOf=remote_id,
|
||||
orderedItems=items,
|
||||
next=next_page,
|
||||
prev=prev_page
|
||||
).serialize()
|
||||
|
||||
|
||||
class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||
''' extends activitypub models to work as ordered collections '''
|
||||
@property
|
||||
def collection_queryset(self):
|
||||
''' usually an ordered collection model aggregates a different model '''
|
||||
raise NotImplementedError('Model must define collection_queryset')
|
||||
|
||||
activity_serializer = activitypub.OrderedCollection
|
||||
|
||||
def to_activity(self, **kwargs):
|
||||
''' an ordered collection of the specified model queryset '''
|
||||
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
||||
|
||||
|
||||
def generate_activity(obj):
|
||||
''' go through the fields on an object '''
|
||||
activity = {}
|
||||
for field in obj.activity_fields:
|
||||
field.set_activity_from_field(activity, obj)
|
||||
|
||||
if hasattr(obj, 'serialize_reverse_fields'):
|
||||
# for example, editions of a work
|
||||
for model_field_name, activity_field_name, sort_field in \
|
||||
obj.serialize_reverse_fields:
|
||||
related_field = getattr(obj, model_field_name)
|
||||
activity[activity_field_name] = \
|
||||
unfurl_related_field(related_field, sort_field)
|
||||
|
||||
if not activity.get('id'):
|
||||
activity['id'] = obj.get_remote_id()
|
||||
return activity
|
||||
try:
|
||||
instance.save(broadcast=False)
|
||||
except TypeError:
|
||||
instance.save()
|
||||
|
|
|
@ -7,11 +7,11 @@ from model_utils.managers import InheritanceManager
|
|||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
|
||||
from . import fields
|
||||
|
||||
class BookDataModel(ActivitypubMixin, BookWyrmModel):
|
||||
class BookDataModel(ObjectMixin, BookWyrmModel):
|
||||
''' fields shared between editable book data (books, works, authors) '''
|
||||
origin_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
openlibrary_key = fields.CharField(
|
||||
|
@ -74,6 +74,7 @@ class Book(BookDataModel):
|
|||
|
||||
@property
|
||||
def latest_readthrough(self):
|
||||
''' most recent readthrough activity '''
|
||||
return self.readthrough_set.order_by('-updated_date').first()
|
||||
|
||||
@property
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
''' like/fav/star a status '''
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
||||
from .activitypub_mixin import ActivityMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
class Favorite(ActivitypubMixin, BookWyrmModel):
|
||||
class Favorite(ActivityMixin, BookWyrmModel):
|
||||
''' fav'ing a post '''
|
||||
user = fields.ForeignKey(
|
||||
'User', on_delete=models.PROTECT, activitypub_field='actor')
|
||||
|
@ -18,9 +20,33 @@ class Favorite(ActivitypubMixin, BookWyrmModel):
|
|||
def save(self, *args, **kwargs):
|
||||
''' update user active time '''
|
||||
self.user.last_active_date = timezone.now()
|
||||
self.user.save()
|
||||
self.user.save(broadcast=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if self.status.user.local and self.status.user != self.user:
|
||||
notification_model = apps.get_model(
|
||||
'bookwyrm.Notification', require_ready=True)
|
||||
notification_model.objects.create(
|
||||
user=self.status.user,
|
||||
notification_type='FAVORITE',
|
||||
related_user=self.user,
|
||||
related_status=self.status
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
''' delete and delete notifications '''
|
||||
# check for notification
|
||||
if self.status.user.local:
|
||||
notification_model = apps.get_model(
|
||||
'bookwyrm.Notification', require_ready=True)
|
||||
notification = notification_model.objects.filter(
|
||||
user=self.status.user, related_user=self.user,
|
||||
related_status=self.status, notification_type='FAVORITE'
|
||||
).first()
|
||||
if notification:
|
||||
notification.delete()
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
''' can't fav things twice '''
|
||||
unique_together = ('user', 'status')
|
||||
|
|
|
@ -263,6 +263,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
|||
if formatted is None or formatted is MISSING:
|
||||
return
|
||||
getattr(instance, self.name).set(formatted)
|
||||
instance.save(broadcast=False)
|
||||
|
||||
def field_to_activity(self, value):
|
||||
if self.link_only:
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import re
|
||||
import dateutil.parser
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
@ -50,6 +51,18 @@ class ImportJob(models.Model):
|
|||
)
|
||||
retry = models.BooleanField(default=False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' save and notify '''
|
||||
super().save(*args, **kwargs)
|
||||
if self.complete:
|
||||
notification_model = apps.get_model(
|
||||
'bookwyrm.Notification', require_ready=True)
|
||||
notification_model.objects.create(
|
||||
user=self.user,
|
||||
notification_type='IMPORT',
|
||||
related_import=self,
|
||||
)
|
||||
|
||||
|
||||
class ImportItem(models.Model):
|
||||
''' a single line of a csv being imported '''
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
''' make a list of books!! '''
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
||||
from .base_model import OrderedCollectionMixin
|
||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
|
||||
|
@ -42,20 +43,22 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
|||
@property
|
||||
def collection_queryset(self):
|
||||
''' list of books for this shelf, overrides OrderedCollectionMixin '''
|
||||
return self.books.all().order_by('listitem')
|
||||
return self.books.filter(
|
||||
listitem__approved=True
|
||||
).all().order_by('listitem')
|
||||
|
||||
class Meta:
|
||||
''' default sorting '''
|
||||
ordering = ('-updated_date',)
|
||||
|
||||
|
||||
class ListItem(ActivitypubMixin, BookWyrmModel):
|
||||
class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||
''' ok '''
|
||||
book = fields.ForeignKey(
|
||||
'Edition', on_delete=models.PROTECT, activitypub_field='object')
|
||||
book_list = fields.ForeignKey(
|
||||
'List', on_delete=models.CASCADE, activitypub_field='target')
|
||||
added_by = fields.ForeignKey(
|
||||
user = fields.ForeignKey(
|
||||
'User',
|
||||
on_delete=models.PROTECT,
|
||||
activitypub_field='actor'
|
||||
|
@ -66,24 +69,24 @@ class ListItem(ActivitypubMixin, BookWyrmModel):
|
|||
endorsement = models.ManyToManyField('User', related_name='endorsers')
|
||||
|
||||
activity_serializer = activitypub.AddBook
|
||||
object_field = 'book'
|
||||
collection_field = 'book_list'
|
||||
|
||||
def to_add_activity(self, user):
|
||||
''' AP for shelving a book'''
|
||||
return activitypub.Add(
|
||||
id='%s#add' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.book.to_activity(),
|
||||
target=self.book_list.remote_id,
|
||||
).serialize()
|
||||
def save(self, *args, **kwargs):
|
||||
''' create a notification too '''
|
||||
created = not bool(self.id)
|
||||
super().save(*args, **kwargs)
|
||||
list_owner = self.book_list.user
|
||||
# create a notification if somoene ELSE added to a local user's list
|
||||
if created and list_owner.local and list_owner != self.user:
|
||||
model = apps.get_model('bookwyrm.Notification', require_ready=True)
|
||||
model.objects.create(
|
||||
user=list_owner,
|
||||
related_user=self.user,
|
||||
related_list_item=self,
|
||||
notification_type='ADD',
|
||||
)
|
||||
|
||||
def to_remove_activity(self, user):
|
||||
''' AP for un-shelving a book'''
|
||||
return activitypub.Remove(
|
||||
id='%s#remove' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.book.to_activity(),
|
||||
target=self.book_list.remote_id
|
||||
).serialize()
|
||||
|
||||
class Meta:
|
||||
''' an opinionated constraint! you can't put a book on a list twice '''
|
||||
|
|
|
@ -5,24 +5,41 @@ from .base_model import BookWyrmModel
|
|||
|
||||
NotificationType = models.TextChoices(
|
||||
'NotificationType',
|
||||
'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT')
|
||||
'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT ADD')
|
||||
|
||||
class Notification(BookWyrmModel):
|
||||
''' you've been tagged, liked, followed, etc '''
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
user = models.ForeignKey('User', on_delete=models.CASCADE)
|
||||
related_book = models.ForeignKey(
|
||||
'Edition', on_delete=models.PROTECT, null=True)
|
||||
'Edition', on_delete=models.CASCADE, null=True)
|
||||
related_user = models.ForeignKey(
|
||||
'User',
|
||||
on_delete=models.PROTECT, null=True, related_name='related_user')
|
||||
on_delete=models.CASCADE, null=True, related_name='related_user')
|
||||
related_status = models.ForeignKey(
|
||||
'Status', on_delete=models.PROTECT, null=True)
|
||||
'Status', on_delete=models.CASCADE, null=True)
|
||||
related_import = models.ForeignKey(
|
||||
'ImportJob', on_delete=models.PROTECT, null=True)
|
||||
'ImportJob', on_delete=models.CASCADE, null=True)
|
||||
related_list_item = models.ForeignKey(
|
||||
'ListItem', on_delete=models.CASCADE, null=True)
|
||||
read = models.BooleanField(default=False)
|
||||
notification_type = models.CharField(
|
||||
max_length=255, choices=NotificationType.choices)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' save, but don't make dupes '''
|
||||
# there's probably a better way to do this
|
||||
if self.__class__.objects.filter(
|
||||
user=self.user,
|
||||
related_book=self.related_book,
|
||||
related_user=self.related_user,
|
||||
related_status=self.related_status,
|
||||
related_import=self.related_import,
|
||||
related_list_item=self.related_list_item,
|
||||
notification_type=self.notification_type,
|
||||
).exists():
|
||||
return
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
''' checks if notifcation is in enum list for valid types '''
|
||||
constraints = [
|
||||
|
|
|
@ -31,7 +31,7 @@ class ReadThrough(BookWyrmModel):
|
|||
def save(self, *args, **kwargs):
|
||||
''' update user active time '''
|
||||
self.user.last_active_date = timezone.now()
|
||||
self.user.save()
|
||||
self.user.save(broadcast=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def create_update(self):
|
||||
|
@ -54,5 +54,5 @@ class ProgressUpdate(BookWyrmModel):
|
|||
def save(self, *args, **kwargs):
|
||||
''' update user active time '''
|
||||
self.user.last_active_date = timezone.now()
|
||||
self.user.save()
|
||||
self.user.save(broadcast=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
''' defines relationships between users '''
|
||||
from django.db import models
|
||||
from django.apps import apps
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.dispatch import receiver
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
||||
from .activitypub_mixin import ActivitypubMixin, ActivityMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
|
||||
class UserRelationship(ActivitypubMixin, BookWyrmModel):
|
||||
class UserRelationship(BookWyrmModel):
|
||||
''' many-to-many through table for followers '''
|
||||
user_subject = fields.ForeignKey(
|
||||
'User',
|
||||
|
@ -23,6 +25,16 @@ class UserRelationship(ActivitypubMixin, BookWyrmModel):
|
|||
activitypub_field='object',
|
||||
)
|
||||
|
||||
@property
|
||||
def privacy(self):
|
||||
''' all relationships are handled directly with the participants '''
|
||||
return 'direct'
|
||||
|
||||
@property
|
||||
def recipients(self):
|
||||
''' the remote user needs to recieve direct broadcasts '''
|
||||
return [u for u in [self.user_subject, self.user_object] if not u.local]
|
||||
|
||||
class Meta:
|
||||
''' relationships should be unique '''
|
||||
abstract = True
|
||||
|
@ -37,8 +49,6 @@ class UserRelationship(ActivitypubMixin, BookWyrmModel):
|
|||
)
|
||||
]
|
||||
|
||||
activity_serializer = activitypub.Follow
|
||||
|
||||
def get_remote_id(self, status=None):# pylint: disable=arguments-differ
|
||||
''' use shelf identifier in remote_id '''
|
||||
status = status or 'follows'
|
||||
|
@ -46,55 +56,84 @@ class UserRelationship(ActivitypubMixin, BookWyrmModel):
|
|||
return '%s#%s/%d' % (base_path, status, self.id)
|
||||
|
||||
|
||||
def to_accept_activity(self):
|
||||
''' generate an Accept for this follow request '''
|
||||
return activitypub.Accept(
|
||||
id=self.get_remote_id(status='accepts'),
|
||||
actor=self.user_object.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
|
||||
|
||||
def to_reject_activity(self):
|
||||
''' generate a Reject for this follow request '''
|
||||
return activitypub.Reject(
|
||||
id=self.get_remote_id(status='rejects'),
|
||||
actor=self.user_object.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
|
||||
|
||||
class UserFollows(UserRelationship):
|
||||
class UserFollows(ActivitypubMixin, UserRelationship):
|
||||
''' Following a user '''
|
||||
status = 'follows'
|
||||
activity_serializer = activitypub.Follow
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, follow_request):
|
||||
''' converts a follow request into a follow relationship '''
|
||||
return cls(
|
||||
return cls.objects.create(
|
||||
user_subject=follow_request.user_subject,
|
||||
user_object=follow_request.user_object,
|
||||
remote_id=follow_request.remote_id,
|
||||
)
|
||||
|
||||
|
||||
class UserFollowRequest(UserRelationship):
|
||||
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
''' following a user requires manual or automatic confirmation '''
|
||||
status = 'follow_request'
|
||||
activity_serializer = activitypub.Follow
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' make sure the follow relationship doesn't already exist '''
|
||||
def save(self, *args, broadcast=True, **kwargs):
|
||||
''' make sure the follow or block relationship doesn't already exist '''
|
||||
try:
|
||||
UserFollows.objects.get(
|
||||
user_subject=self.user_subject,
|
||||
user_object=self.user_object
|
||||
)
|
||||
UserBlocks.objects.get(
|
||||
user_subject=self.user_subject,
|
||||
user_object=self.user_object
|
||||
)
|
||||
return None
|
||||
except UserFollows.DoesNotExist:
|
||||
return super().save(*args, **kwargs)
|
||||
except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if broadcast and self.user_subject.local and not self.user_object.local:
|
||||
self.broadcast(self.to_activity(), self.user_subject)
|
||||
|
||||
if self.user_object.local:
|
||||
model = apps.get_model('bookwyrm.Notification', require_ready=True)
|
||||
notification_type = 'FOLLOW_REQUEST' \
|
||||
if self.user_object.manually_approves_followers else 'FOLLOW'
|
||||
model.objects.create(
|
||||
user=self.user_object,
|
||||
related_user=self.user_subject,
|
||||
notification_type=notification_type,
|
||||
)
|
||||
|
||||
|
||||
class UserBlocks(UserRelationship):
|
||||
def accept(self):
|
||||
''' turn this request into the real deal'''
|
||||
user = self.user_object
|
||||
activity = activitypub.Accept(
|
||||
id=self.get_remote_id(status='accepts'),
|
||||
actor=self.user_object.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
with transaction.atomic():
|
||||
UserFollows.from_request(self)
|
||||
self.delete()
|
||||
|
||||
self.broadcast(activity, user)
|
||||
|
||||
|
||||
def reject(self):
|
||||
''' generate a Reject for this follow request '''
|
||||
user = self.user_object
|
||||
activity = activitypub.Reject(
|
||||
id=self.get_remote_id(status='rejects'),
|
||||
actor=self.user_object.remote_id,
|
||||
object=self.to_activity()
|
||||
).serialize()
|
||||
self.delete()
|
||||
self.broadcast(activity, user)
|
||||
|
||||
|
||||
class UserBlocks(ActivityMixin, UserRelationship):
|
||||
''' prevent another user from following you and seeing your posts '''
|
||||
status = 'blocks'
|
||||
activity_serializer = activitypub.Block
|
||||
|
|
|
@ -3,8 +3,8 @@ import re
|
|||
from django.db import models
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
||||
from .base_model import OrderedCollectionMixin
|
||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
|
||||
|
@ -27,12 +27,11 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
''' set the identifier '''
|
||||
saved = super().save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
if not self.identifier:
|
||||
slug = re.sub(r'[^\w]', '', self.name).lower()
|
||||
self.identifier = '%s-%d' % (slug, self.id)
|
||||
return super().save(*args, **kwargs)
|
||||
return saved
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def collection_queryset(self):
|
||||
|
@ -49,39 +48,18 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
unique_together = ('user', 'identifier')
|
||||
|
||||
|
||||
class ShelfBook(ActivitypubMixin, BookWyrmModel):
|
||||
class ShelfBook(CollectionItemMixin, BookWyrmModel):
|
||||
''' many to many join table for books and shelves '''
|
||||
book = fields.ForeignKey(
|
||||
'Edition', on_delete=models.PROTECT, activitypub_field='object')
|
||||
shelf = fields.ForeignKey(
|
||||
'Shelf', on_delete=models.PROTECT, activitypub_field='target')
|
||||
added_by = fields.ForeignKey(
|
||||
'User',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.PROTECT,
|
||||
activitypub_field='actor'
|
||||
)
|
||||
user = fields.ForeignKey(
|
||||
'User', on_delete=models.PROTECT, activitypub_field='actor')
|
||||
|
||||
activity_serializer = activitypub.AddBook
|
||||
|
||||
def to_add_activity(self, user):
|
||||
''' AP for shelving a book'''
|
||||
return activitypub.Add(
|
||||
id='%s#add' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.book.to_activity(),
|
||||
target=self.shelf.remote_id,
|
||||
).serialize()
|
||||
|
||||
def to_remove_activity(self, user):
|
||||
''' AP for un-shelving a book'''
|
||||
return activitypub.Remove(
|
||||
id='%s#remove' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.book.to_activity(),
|
||||
target=self.shelf.to_activity()
|
||||
).serialize()
|
||||
object_field = 'book'
|
||||
collection_field = 'shelf'
|
||||
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -9,10 +9,12 @@ from django.utils import timezone
|
|||
from model_utils.managers import InheritanceManager
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
|
||||
from .activitypub_mixin import ActivitypubMixin, ActivityMixin
|
||||
from .activitypub_mixin import OrderedCollectionPageMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
from .fields import image_serializer
|
||||
from . import fields
|
||||
|
||||
|
||||
class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
''' any post, like a reply to a review, etc '''
|
||||
|
@ -50,6 +52,47 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
serialize_reverse_fields = [('attachments', 'attachment', 'id')]
|
||||
deserialize_reverse_fields = [('attachments', 'attachment')]
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' save and notify '''
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
notification_model = apps.get_model(
|
||||
'bookwyrm.Notification', require_ready=True)
|
||||
|
||||
if self.deleted:
|
||||
notification_model.objects.filter(related_status=self).delete()
|
||||
|
||||
if self.reply_parent and self.reply_parent.user != self.user and \
|
||||
self.reply_parent.user.local:
|
||||
notification_model.objects.create(
|
||||
user=self.reply_parent.user,
|
||||
notification_type='REPLY',
|
||||
related_user=self.user,
|
||||
related_status=self,
|
||||
)
|
||||
for mention_user in self.mention_users.all():
|
||||
# avoid double-notifying about this status
|
||||
if not mention_user.local or \
|
||||
(self.reply_parent and \
|
||||
mention_user == self.reply_parent.user):
|
||||
continue
|
||||
notification_model.objects.create(
|
||||
user=mention_user,
|
||||
notification_type='MENTION',
|
||||
related_user=self.user,
|
||||
related_status=self,
|
||||
)
|
||||
|
||||
@property
|
||||
def recipients(self):
|
||||
''' tagged users who definitely need to get this status in broadcast '''
|
||||
mentions = [u for u in self.mention_users.all() if not u.local]
|
||||
if hasattr(self, 'reply_parent') and self.reply_parent \
|
||||
and not self.reply_parent.user.local:
|
||||
mentions.append(self.reply_parent.user)
|
||||
return list(set(mentions))
|
||||
|
||||
@classmethod
|
||||
def ignore_activity(cls, activity):
|
||||
''' keep notes if they are replies to existing statuses '''
|
||||
|
@ -126,14 +169,6 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
return activity
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' update user active time '''
|
||||
if self.user.local:
|
||||
self.user.last_active_date = timezone.now()
|
||||
self.user.save()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class GeneratedNote(Status):
|
||||
''' these are app-generated messages about user activity '''
|
||||
@property
|
||||
|
@ -223,7 +258,7 @@ class Review(Status):
|
|||
pure_type = 'Article'
|
||||
|
||||
|
||||
class Boost(Status):
|
||||
class Boost(ActivityMixin, Status):
|
||||
''' boost'ing a post '''
|
||||
boosted_status = fields.ForeignKey(
|
||||
'Status',
|
||||
|
@ -231,6 +266,35 @@ class Boost(Status):
|
|||
related_name='boosters',
|
||||
activitypub_field='object',
|
||||
)
|
||||
activity_serializer = activitypub.Boost
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' save and notify '''
|
||||
super().save(*args, **kwargs)
|
||||
if not self.boosted_status.user.local:
|
||||
return
|
||||
|
||||
notification_model = apps.get_model(
|
||||
'bookwyrm.Notification', require_ready=True)
|
||||
notification_model.objects.create(
|
||||
user=self.boosted_status.user,
|
||||
related_status=self.boosted_status,
|
||||
related_user=self.user,
|
||||
notification_type='BOOST',
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
''' delete and un-notify '''
|
||||
notification_model = apps.get_model(
|
||||
'bookwyrm.Notification', require_ready=True)
|
||||
notification_model.objects.filter(
|
||||
user=self.boosted_status.user,
|
||||
related_status=self.boosted_status,
|
||||
related_user=self.user,
|
||||
notification_type='BOOST',
|
||||
).delete()
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
''' the user field is "actor" here instead of "attributedTo" '''
|
||||
|
@ -244,8 +308,6 @@ class Boost(Status):
|
|||
self.image_fields = []
|
||||
self.deserialize_reverse_fields = []
|
||||
|
||||
activity_serializer = activitypub.Boost
|
||||
|
||||
# This constraint can't work as it would cross tables.
|
||||
# class Meta:
|
||||
# unique_together = ('user', 'boosted_status')
|
||||
|
|
|
@ -5,7 +5,8 @@ from django.db import models
|
|||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .base_model import OrderedCollectionMixin, BookWyrmModel
|
||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
|
||||
|
@ -40,7 +41,7 @@ class Tag(OrderedCollectionMixin, BookWyrmModel):
|
|||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class UserTag(BookWyrmModel):
|
||||
class UserTag(CollectionItemMixin, BookWyrmModel):
|
||||
''' an instance of a tag on a book by a user '''
|
||||
user = fields.ForeignKey(
|
||||
'User', on_delete=models.PROTECT, activitypub_field='actor')
|
||||
|
@ -50,25 +51,8 @@ class UserTag(BookWyrmModel):
|
|||
'Tag', on_delete=models.PROTECT, activitypub_field='target')
|
||||
|
||||
activity_serializer = activitypub.AddBook
|
||||
|
||||
def to_add_activity(self, user):
|
||||
''' AP for shelving a book'''
|
||||
return activitypub.Add(
|
||||
id='%s#add' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.book.to_activity(),
|
||||
target=self.remote_id,
|
||||
).serialize()
|
||||
|
||||
def to_remove_activity(self, user):
|
||||
''' AP for un-shelving a book'''
|
||||
return activitypub.Remove(
|
||||
id='%s#remove' % self.remote_id,
|
||||
actor=user.remote_id,
|
||||
object=self.book.to_activity(),
|
||||
target=self.remote_id,
|
||||
).serialize()
|
||||
|
||||
object_field = 'book'
|
||||
collection_field = 'tag'
|
||||
|
||||
class Meta:
|
||||
''' unqiueness constraint '''
|
||||
|
|
|
@ -17,8 +17,8 @@ from bookwyrm.settings import DOMAIN
|
|||
from bookwyrm.signatures import create_key_pair
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.utils import regex
|
||||
from .base_model import OrderedCollectionPageMixin
|
||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
||||
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from .federated_server import FederatedServer
|
||||
from . import fields, Review
|
||||
|
||||
|
@ -211,6 +211,9 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
''' create a key pair '''
|
||||
# no broadcasting happening here
|
||||
if 'broadcast' in kwargs:
|
||||
del kwargs['broadcast']
|
||||
if not self.public_key:
|
||||
self.private_key, self.public_key = create_key_pair()
|
||||
return super().save(*args, **kwargs)
|
||||
|
@ -291,7 +294,7 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
|||
|
||||
instance.key_pair = KeyPair.objects.create(
|
||||
remote_id='%s/#main-key' % instance.remote_id)
|
||||
instance.save()
|
||||
instance.save(broadcast=False)
|
||||
|
||||
shelves = [{
|
||||
'name': 'To Read',
|
||||
|
@ -310,7 +313,7 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
|||
identifier=shelf['identifier'],
|
||||
user=instance,
|
||||
editable=False
|
||||
).save()
|
||||
).save(broadcast=False)
|
||||
|
||||
|
||||
@app.task
|
||||
|
|
|
@ -13,6 +13,13 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- SHELVING --- */
|
||||
.shelf-option:disabled > *::after {
|
||||
font-family: "icomoon";
|
||||
content: "\e918";
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
/* --- TOGGLES --- */
|
||||
.toggle-button[aria-pressed=true], .toggle-button[aria-pressed=true]:hover {
|
||||
background-color: hsl(171, 100%, 41%);
|
||||
|
|
|
@ -46,13 +46,14 @@ function back(e) {
|
|||
history.back();
|
||||
}
|
||||
|
||||
function polling(el) {
|
||||
let delay = 10000 + (Math.random() * 1000);
|
||||
function polling(el, delay) {
|
||||
delay = delay || 10000;
|
||||
delay += (Math.random() * 1000);
|
||||
setTimeout(function() {
|
||||
fetch('/api/updates/' + el.getAttribute('data-poll'))
|
||||
.then(response => response.json())
|
||||
.then(data => updateCountElement(el, data));
|
||||
polling(el);
|
||||
polling(el, delay * 1.25);
|
||||
}, delay, el);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
''' Handle user activity '''
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import models
|
||||
|
@ -19,30 +20,18 @@ def create_generated_note(user, content, mention_books=None, privacy='public'):
|
|||
parser.feed(content)
|
||||
content = parser.get_output()
|
||||
|
||||
status = models.GeneratedNote.objects.create(
|
||||
user=user,
|
||||
content=content,
|
||||
privacy=privacy
|
||||
)
|
||||
|
||||
if mention_books:
|
||||
for book in mention_books:
|
||||
status.mention_books.add(book)
|
||||
with transaction.atomic():
|
||||
# create but don't save
|
||||
status = models.GeneratedNote(
|
||||
user=user,
|
||||
content=content,
|
||||
privacy=privacy
|
||||
)
|
||||
# we have to save it to set the related fields, but hold off on telling
|
||||
# folks about it because it is not ready
|
||||
status.save(broadcast=False)
|
||||
|
||||
if mention_books:
|
||||
status.mention_books.set(mention_books)
|
||||
status.save(created=True)
|
||||
return status
|
||||
|
||||
|
||||
def create_notification(user, notification_type, related_user=None, \
|
||||
related_book=None, related_status=None, related_import=None):
|
||||
''' let a user know when someone interacts with their content '''
|
||||
if user == related_user:
|
||||
# don't create notification when you interact with your own stuff
|
||||
return
|
||||
models.Notification.objects.create(
|
||||
user=user,
|
||||
related_book=related_book,
|
||||
related_user=related_user,
|
||||
related_status=related_status,
|
||||
related_import=related_import,
|
||||
notification_type=notification_type,
|
||||
)
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<div class="column is-narrow">
|
||||
{% include 'snippets/book_cover.html' with book=book size=large %}
|
||||
{% include 'snippets/rate_action.html' with user=request.user book=book %}
|
||||
{% include 'snippets/shelve_button.html' %}
|
||||
{% include 'snippets/shelve_button/shelve_button.html' %}
|
||||
|
||||
{% if request.user.is_authenticated and not book.cover %}
|
||||
<div class="box p-2">
|
||||
|
|
|
@ -8,15 +8,17 @@
|
|||
{% include 'snippets/toggle/toggle_button.html' with label="close" class="delete" nonbutton=True %}
|
||||
</header>
|
||||
{% block modal-form-open %}{% endblock %}
|
||||
{% if not no_body %}
|
||||
<section class="modal-card-body">
|
||||
{% block modal-body %}{% endblock %}
|
||||
</section>
|
||||
{% endif %}
|
||||
<footer class="modal-card-foot">
|
||||
{% block modal-footer %}{% endblock %}
|
||||
</footer>
|
||||
{% block modal-form-close %}{% endblock %}
|
||||
</div>
|
||||
<label class="modal-close is-large" for="{{ controls_text }}-{{ readthrough.id }}" aria-label="close"></label>
|
||||
<label class="modal-close is-large" for="{{ controls_text }}-{{ controls_uid }}" aria-label="close"></label>
|
||||
{% include 'snippets/toggle/toggle_button.html' with label="close" class="modal-close is-large" nonbutton=True %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
{% include 'snippets/shelve_button.html' with book=book %}
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||
{% active_shelf book as active_shelf %}
|
||||
{% if active_shelf.shelf.identifier == 'reading' and book.latest_readthrough %}
|
||||
{% include 'snippets/progress_update.html' with readthrough=book.latest_readthrough %}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
{% include 'snippets/book_titleby.html' with book=item.book %}
|
||||
</td>
|
||||
<td>
|
||||
{% include 'snippets/username.html' with user=item.added_by %}
|
||||
{% include 'snippets/username.html' with user=item.user %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="field has-addons">
|
||||
|
|
|
@ -21,19 +21,19 @@
|
|||
<div class="card">
|
||||
<div class="card-content columns p-0 mb-0">
|
||||
<div class="column is-narrow pt-0 pb-0">
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=item.book size="medium" %}</a>
|
||||
<a href="{{ item.book.local_path }}">{% include 'snippets/book_cover.html' with book=item.book size="medium" %}</a>
|
||||
</div>
|
||||
<div class="column is-flex-direction-column is-align-items-self-start">
|
||||
<span>{% include 'snippets/book_titleby.html' with book=item.book %}</span>
|
||||
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
|
||||
{% include 'snippets/shelve_button.html' with book=item.book %}
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=item.book %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer has-background-white-bis">
|
||||
<div class="card-footer-item">
|
||||
<p>Added by {% include 'snippets/username.html' with user=item.added_by %}</p>
|
||||
<p>Added by {% include 'snippets/username.html' with user=item.user %}</p>
|
||||
</div>
|
||||
{% if list.user == request.user or list.curation == 'open' and item.added_by == request.user %}
|
||||
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
|
||||
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item" value="{{ item.id }}">
|
||||
|
@ -72,6 +72,7 @@
|
|||
<p>No books found{% if query %} matching the query "{{ query }}"{% endif %}</p>
|
||||
{% endif %}
|
||||
{% for book in suggested_books %}
|
||||
{% if book %}
|
||||
<div class="block columns">
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
|
||||
|
@ -85,6 +86,7 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</header>
|
||||
<div class="card-image is-flex">
|
||||
{% for book in list.listitem_set.all|slice:5 %}
|
||||
{% include 'snippets/book_cover.html' with book=book.book size="small" %}
|
||||
<a href="{{ book.book.local_path }}">{% include 'snippets/book_cover.html' with book=book.book size="small" %}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="card-content is-flex-grow-0">
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
<span class="icon icon-heart"></span>
|
||||
{% elif notification.notification_type == 'IMPORT' %}
|
||||
<span class="icon icon-list"></span>
|
||||
{% elif notification.notification_type == 'ADD' %}
|
||||
<span class="icon icon-plus"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="column">
|
||||
|
@ -36,33 +38,34 @@
|
|||
<p>
|
||||
{# DESCRIPTION #}
|
||||
{% if notification.related_user %}
|
||||
{% include 'snippets/avatar.html' with user=notification.related_user %}
|
||||
{% include 'snippets/username.html' with user=notification.related_user %}
|
||||
{% if notification.notification_type == 'FAVORITE' %}
|
||||
favorited your
|
||||
<a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
||||
{% include 'snippets/avatar.html' with user=notification.related_user %}
|
||||
{% include 'snippets/username.html' with user=notification.related_user %}
|
||||
{% if notification.notification_type == 'FAVORITE' %}
|
||||
favorited your
|
||||
<a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
||||
|
||||
{% elif notification.notification_type == 'MENTION' %}
|
||||
mentioned you in a
|
||||
<a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
||||
{% elif notification.notification_type == 'MENTION' %}
|
||||
mentioned you in a
|
||||
<a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
||||
|
||||
{% elif notification.notification_type == 'REPLY' %}
|
||||
<a href="{{ related_status.local_path }}">replied</a>
|
||||
to your
|
||||
<a href="{{ related_status.reply_parent.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
||||
{% elif notification.notification_type == 'FOLLOW' %}
|
||||
followed you
|
||||
{% include 'snippets/follow_button.html' with user=notification.related_user %}
|
||||
{% elif notification.notification_type == 'FOLLOW_REQUEST' %}
|
||||
sent you a follow request
|
||||
<div class="row shrink">
|
||||
{% include 'snippets/follow_request_buttons.html' with user=notification.related_user %}
|
||||
</div>
|
||||
|
||||
{% elif notification.notification_type == 'BOOST' %}
|
||||
boosted your <a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% elif notification.notification_type == 'REPLY' %}
|
||||
<a href="{{ related_status.local_path }}">replied</a>
|
||||
to your
|
||||
<a href="{{ related_status.reply_parent.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
||||
{% elif notification.notification_type == 'FOLLOW' %}
|
||||
followed you
|
||||
{% include 'snippets/follow_button.html' with user=notification.related_user %}
|
||||
{% elif notification.notification_type == 'FOLLOW_REQUEST' %}
|
||||
sent you a follow request
|
||||
<div class="row shrink">
|
||||
{% include 'snippets/follow_request_buttons.html' with user=notification.related_user %}
|
||||
</div>
|
||||
{% elif notification.notification_type == 'BOOST' %}
|
||||
boosted your <a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
|
||||
{% elif notification.notification_type == 'ADD' %}
|
||||
{% if notification.related_list_item.approved %}added{% else %}suggested adding{% endif %} {% include 'snippets/book_titleby.html' with book=notification.related_list_item.book %} to your list "<a href="{{ notification.related_list_item.book_list.local_path }}{% if not notification.related_list_item.approved %}/curate{% endif %}">{{ notification.related_list_item.book_list.name }}</a>"
|
||||
{% endif %}
|
||||
{% elif notification.related_import %}
|
||||
your <a href="/import/{{ notification.related_import.id }}">import</a> completed.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="column is-narrow">
|
||||
<div>
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
|
||||
{% include 'snippets/shelve_button.html' with book=book %}
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<a href="/book/{{ book.id }}">
|
||||
{% include 'snippets/book_cover.html' with book=book %}
|
||||
</a>
|
||||
{% include 'snippets/shelve_button.html' with book=book switch_mode=True %}
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -37,10 +37,10 @@
|
|||
{% for book in books %}
|
||||
<tr class="book-preview">
|
||||
<td>
|
||||
{% include 'snippets/book_cover.html' with book=book size="small" %}
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/book/{{ book.id }}">{{ book.title }}</a>
|
||||
<a href="{{ book.local_path }}">{{ book.title }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ book.authors.first.name }}
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% if request.user.is_authenticated %}
|
||||
|
||||
{% with book.id|uuid as uuid %}
|
||||
{% active_shelf book as active_shelf %}
|
||||
<div class="field has-addons">
|
||||
{% if switch_mode and active_shelf.book != book %}
|
||||
<div class="control">
|
||||
{% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="control">
|
||||
{% if active_shelf.shelf.identifier == 'read' %}
|
||||
<button class="button is-small" disabled>
|
||||
<span>Read</span> <span class="icon icon-check"></span>
|
||||
</button>
|
||||
{% elif active_shelf.shelf.identifier == 'reading' %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with class="is-small" text="I'm done!" controls_text="finish-reading" controls_uid=uuid focus="modal-title-finish-reading" %}
|
||||
{% elif active_shelf.shelf.identifier == 'to-read' %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with class="is-small" text="Start reading" controls_text="start-reading" controls_uid=uuid focus="modal-title-start-reading" %}
|
||||
{% else %}
|
||||
<form name="shelve" action="/shelve/" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
||||
<input type="hidden" name="shelf" value="to-read">
|
||||
<button class="button is-small" type="submit">Want to read</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'snippets/shelve_button_dropdown.html' with class="is-small" button_uuid=uuid%}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'snippets/start_reading_modal.html' with book=active_shelf.book controls_text="start-reading" controls_uid=uuid %}
|
||||
|
||||
{% latest_read_through book request.user as readthrough %}
|
||||
{% include 'snippets/finish_reading_modal.html' with book=active_shelf.book controls_text="finish-reading" controls_uid=uuid readthrough=readthrough %}
|
||||
|
||||
{% endwith %}
|
||||
{% endif %}
|
27
bookwyrm/templates/snippets/shelve_button/shelve_button.html
Normal file
27
bookwyrm/templates/snippets/shelve_button/shelve_button.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% if request.user.is_authenticated %}
|
||||
|
||||
{% with book.id|uuid as uuid %}
|
||||
{% active_shelf book as active_shelf %}
|
||||
<div class="field has-addons">
|
||||
{% if switch_mode and active_shelf.book != book %}
|
||||
<div class="control">
|
||||
{% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="control">
|
||||
{% include 'snippets/shelve_button/shelve_button_options.html' with class="shelf-option is-small" shelves=request.user.shelf_set.all active_shelf=active_shelf button_uuid=uuid %}
|
||||
</div>
|
||||
{% include 'snippets/shelve_button/shelve_button_dropdown.html' with class="is-small" button_uuid=uuid%}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'snippets/shelve_button/want_to_read_modal.html' with book=active_shelf.book controls_text="want-to-read" controls_uid=uuid no_body=True %}
|
||||
|
||||
{% include 'snippets/shelve_button/start_reading_modal.html' with book=active_shelf.book controls_text="start-reading" controls_uid=uuid %}
|
||||
|
||||
{% latest_read_through book request.user as readthrough %}
|
||||
{% include 'snippets/shelve_button/finish_reading_modal.html' with book=active_shelf.book controls_text="finish-reading" controls_uid=uuid readthrough=readthrough %}
|
||||
|
||||
{% endwith %}
|
||||
{% endif %}
|
|
@ -0,0 +1,10 @@
|
|||
{% extends 'components/dropdown.html' %}
|
||||
{% block dropdown-trigger %}
|
||||
<span class="icon icon-arrow-down">
|
||||
<span class="is-sr-only">More shelves</span>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block dropdown-list %}
|
||||
{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small" %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,25 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% for shelf in shelves %}
|
||||
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
|
||||
{% if dropdown %}<li role="menuitem">{% endif %}
|
||||
<div class="{% if dropdown %}dropdown-item pt-0 pb-0{% elif active_shelf.shelf.identifier|next_shelf != shelf.identifier %}hidden{% endif %}">
|
||||
{% if shelf.identifier == 'reading' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with class=class text="Start reading" controls_text="start-reading" controls_uid=button_uuid focus="modal-title-start-reading" disabled=is_current %}
|
||||
{% endif %}{% elif shelf.identifier == 'read' and active_shelf.shelf.identifier == 'read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
|
||||
<button type="button" class="button {{ class }}" disabled><span>Read</span>
|
||||
{% endif %}{% elif shelf.identifier == 'read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with class=class text="Finish reading" controls_text="finish-reading" controls_uid=button_uuid focus="modal-title-finish-reading" disabled=is_current %}
|
||||
{% endif %}{% elif shelf.identifier == 'to-read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with class=class text="Want to read" controls_text="want-to-read" controls_uid=button_uuid focus="modal-title-want-to-read" disabled=is_current %}
|
||||
{% endif %}{% elif shelf.editable %}
|
||||
<form name="shelve" action="/shelve/" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
||||
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>
|
||||
<span>{{ shelf.name }}</span>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if dropdown %}</li>{% endif %}
|
||||
{% endfor %}
|
|
@ -0,0 +1,31 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
|
||||
{% block modal-title %}
|
||||
Want to Read "<em>{{ book.title }}</em>"
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="shelve" action="/shelve/" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
||||
<input type="hidden" name="shelf" value="to-read">
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<div class="columns">
|
||||
<div class="column field">
|
||||
<label for="post_status_want-{{ uuid }}">
|
||||
<input type="checkbox" name="post-status" class="checkbox" id="post_status_want-{{ uuid }}" checked>
|
||||
Post to feed
|
||||
</label>
|
||||
{% include 'snippets/privacy_select.html' %}
|
||||
</div>
|
||||
<div class="column">
|
||||
<button class="button is-success" type="submit">
|
||||
<span>Want to read</span>
|
||||
</button>
|
||||
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="want-to-read" controls_uid=uuid %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
|
@ -1,28 +0,0 @@
|
|||
{% extends 'components/dropdown.html' %}
|
||||
{% block dropdown-trigger %}
|
||||
<span class="icon icon-arrow-down">
|
||||
<span class="is-sr-only">More shelves</span>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block dropdown-list %}
|
||||
{% for shelf in request.user.shelf_set.all %}
|
||||
<li role="menuitem">
|
||||
{% if active_shelf.shelf.identifier != 'reading' and shelf.identifier == 'reading' %}
|
||||
<div class="dropdown-item pt-0 pb-0">
|
||||
{% include 'snippets/toggle/toggle_button.html' with class="is-fullwidth is-small" text="Start reading" controls_text="start-reading" controls_uid=button_uuid focus="modal-title-start-reading" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<form class="dropdown-item pt-0 pb-0" name="shelve" action="/shelve/" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
||||
<button class="button is-fullwidth is-small" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>
|
||||
<span>{{ shelf.name }}</span>
|
||||
{% if shelf in book.shelf_set.all %}<span class="icon icon-check"></span>{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
{% if checkbox %}data-controls-checkbox="{{ checkbox }}{% if controls_uid %}-{{ controls_uid }}{% endif %}"{% endif %}
|
||||
{% if label %}aria-label="{{ label }}"{% endif %}
|
||||
aria-pressed="{% if pressed %}true{% else %}false{% endif %}"
|
||||
{% if disabled %}disabled{% endif %}
|
||||
>
|
||||
|
||||
{% if icon %}
|
||||
|
@ -13,6 +14,6 @@
|
|||
<span class="is-sr-only">{{ text }}</span>
|
||||
</span>
|
||||
{% else %}
|
||||
{{ text }}
|
||||
<span>{{ text }}</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
|
|
|
@ -54,8 +54,13 @@
|
|||
{% endif %}
|
||||
|
||||
<div>
|
||||
<div class="block">
|
||||
<h2 class="title">User Activity</h2>
|
||||
<div class="columns">
|
||||
<h2 class="title column">User Activity</h2>
|
||||
<div class="column is-narrow">
|
||||
<a class="icon icon-rss" target="_blank" href="{{ user.local_path }}/rss">
|
||||
<span class="is-sr-only">RSS feed</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% for activity in activities %}
|
||||
<div class="block" id="feed">
|
||||
|
|
|
@ -75,19 +75,25 @@
|
|||
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
|
||||
<a href="{{ url }}">Activity</a>
|
||||
</li>
|
||||
{% if is_self or user.goal.exists %}
|
||||
{% now 'Y' as year %}
|
||||
{% url 'user-goal' user|username year as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %}>
|
||||
<a href="{{ url }}">Reading Goal</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_self or user.lists.exists %}
|
||||
{% url 'user-lists' user|username as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %}>
|
||||
<a href="{{ url }}">Lists</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user.shelf_set.exists %}
|
||||
{% url 'user-shelves' user|username as url %}
|
||||
<li{% if url in request.path %} class="is-active"{% endif %}>
|
||||
<a href="{{ url }}">Shelves</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
|
|
@ -171,6 +171,17 @@ def get_status_preview_name(obj):
|
|||
return '%s from <em>%s</em>' % (name, obj.book.title)
|
||||
return name
|
||||
|
||||
@register.filter(name='next_shelf')
|
||||
def get_next_shelf(current_shelf):
|
||||
''' shelf you'd use to update reading progress '''
|
||||
if current_shelf == 'to-read':
|
||||
return 'reading'
|
||||
if current_shelf == 'reading':
|
||||
return 'read'
|
||||
if current_shelf == 'read':
|
||||
return 'read'
|
||||
return 'to-read'
|
||||
|
||||
@register.simple_tag(takes_context=False)
|
||||
def related_status(notification):
|
||||
''' for notifications '''
|
||||
|
@ -211,3 +222,9 @@ def active_read_through(book, user):
|
|||
book=book,
|
||||
finish_date__isnull=True
|
||||
).order_by('-start_date').first()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=False)
|
||||
def comparison_bool(str1, str2):
|
||||
''' idk why I need to write a tag for this, it reutrns a bool '''
|
||||
return str1 == str2
|
||||
|
|
|
@ -23,7 +23,7 @@ class BaseActivity(TestCase):
|
|||
'mouse', 'mouse@mouse.mouse', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
self.user.remote_id = 'http://example.com/a/b'
|
||||
self.user.save()
|
||||
self.user.save(broadcast=False)
|
||||
|
||||
datafile = pathlib.Path(__file__).parent.joinpath(
|
||||
'../data/ap_user.json'
|
||||
|
@ -167,16 +167,19 @@ class BaseActivity(TestCase):
|
|||
with self.assertRaises(ValueError):
|
||||
self.user.avatar.file #pylint: disable=pointless-statement
|
||||
|
||||
activity.to_model(models.User, self.user)
|
||||
# this would trigger a broadcast because it's a local user
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
activity.to_model(models.User, self.user)
|
||||
self.assertIsNotNone(self.user.avatar.name)
|
||||
self.assertIsNotNone(self.user.avatar.file)
|
||||
|
||||
def test_to_model_many_to_many(self):
|
||||
''' annoying that these all need special handling '''
|
||||
status = models.Status.objects.create(
|
||||
content='test status',
|
||||
user=self.user,
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
content='test status',
|
||||
user=self.user,
|
||||
)
|
||||
book = models.Edition.objects.create(
|
||||
title='Test Edition', remote_id='http://book.com/book')
|
||||
update_data = activitypub.Note(
|
||||
|
@ -208,10 +211,11 @@ class BaseActivity(TestCase):
|
|||
def test_to_model_one_to_many(self):
|
||||
''' these are reversed relationships, where the secondary object
|
||||
keys the primary object but not vice versa '''
|
||||
status = models.Status.objects.create(
|
||||
content='test status',
|
||||
user=self.user,
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
content='test status',
|
||||
user=self.user,
|
||||
)
|
||||
update_data = activitypub.Note(
|
||||
id=status.remote_id,
|
||||
content=status.content,
|
||||
|
@ -242,10 +246,11 @@ class BaseActivity(TestCase):
|
|||
@responses.activate
|
||||
def test_set_related_field(self):
|
||||
''' celery task to add back-references to created objects '''
|
||||
status = models.Status.objects.create(
|
||||
content='test status',
|
||||
user=self.user,
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
content='test status',
|
||||
user=self.user,
|
||||
)
|
||||
data = {
|
||||
'url': 'http://www.example.com/image.jpg',
|
||||
'name': 'alt text',
|
||||
|
|
383
bookwyrm/tests/models/test_activitypub_mixin.py
Normal file
383
bookwyrm/tests/models/test_activitypub_mixin.py
Normal file
|
@ -0,0 +1,383 @@
|
|||
''' testing model activitypub utilities '''
|
||||
from unittest.mock import patch
|
||||
from collections import namedtuple
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from django import db
|
||||
from django.test import TestCase
|
||||
|
||||
from bookwyrm.activitypub.base_activity import ActivityObject
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models import base_model
|
||||
from bookwyrm.models.activitypub_mixin import ActivitypubMixin
|
||||
from bookwyrm.models.activitypub_mixin import ActivityMixin, ObjectMixin
|
||||
|
||||
class ActivitypubMixins(TestCase):
|
||||
''' functionality shared across models '''
|
||||
def setUp(self):
|
||||
''' shared data '''
|
||||
self.local_user = models.User.objects.create_user(
|
||||
'mouse', 'mouse@mouse.com', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
self.local_user.remote_id = 'http://example.com/a/b'
|
||||
self.local_user.save(broadcast=False)
|
||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||
self.remote_user = models.User.objects.create_user(
|
||||
'rat', 'rat@rat.com', 'ratword',
|
||||
local=False,
|
||||
remote_id='https://example.com/users/rat',
|
||||
inbox='https://example.com/users/rat/inbox',
|
||||
outbox='https://example.com/users/rat/outbox',
|
||||
)
|
||||
|
||||
|
||||
# ActivitypubMixin
|
||||
def test_to_activity(self):
|
||||
''' model to ActivityPub json '''
|
||||
@dataclass(init=False)
|
||||
class TestActivity(ActivityObject):
|
||||
''' real simple mock '''
|
||||
type: str = 'Test'
|
||||
|
||||
class TestModel(ActivitypubMixin, base_model.BookWyrmModel):
|
||||
''' real simple mock model because BookWyrmModel is abstract '''
|
||||
|
||||
instance = TestModel()
|
||||
instance.remote_id = 'https://www.example.com/test'
|
||||
instance.activity_serializer = TestActivity
|
||||
|
||||
activity = instance.to_activity()
|
||||
self.assertIsInstance(activity, dict)
|
||||
self.assertEqual(activity['id'], 'https://www.example.com/test')
|
||||
self.assertEqual(activity['type'], 'Test')
|
||||
|
||||
|
||||
def test_find_existing_by_remote_id(self):
|
||||
''' attempt to match a remote id to an object in the db '''
|
||||
# uses a different remote id scheme
|
||||
# this isn't really part of this test directly but it's helpful to state
|
||||
book = models.Edition.objects.create(
|
||||
title='Test Edition', remote_id='http://book.com/book')
|
||||
|
||||
self.assertEqual(book.origin_id, 'http://book.com/book')
|
||||
self.assertNotEqual(book.remote_id, 'http://book.com/book')
|
||||
|
||||
# uses subclasses
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.Comment.objects.create(
|
||||
user=self.local_user, content='test status', book=book, \
|
||||
remote_id='https://comment.net')
|
||||
|
||||
result = models.User.find_existing_by_remote_id('hi')
|
||||
self.assertIsNone(result)
|
||||
|
||||
result = models.User.find_existing_by_remote_id(
|
||||
'http://example.com/a/b')
|
||||
self.assertEqual(result, self.local_user)
|
||||
|
||||
# test using origin id
|
||||
result = models.Edition.find_existing_by_remote_id(
|
||||
'http://book.com/book')
|
||||
self.assertEqual(result, book)
|
||||
|
||||
# test subclass match
|
||||
result = models.Status.find_existing_by_remote_id(
|
||||
'https://comment.net')
|
||||
|
||||
|
||||
def test_find_existing(self):
|
||||
''' match a blob of data to a model '''
|
||||
book = models.Edition.objects.create(
|
||||
title='Test edition',
|
||||
openlibrary_key='OL1234',
|
||||
)
|
||||
|
||||
result = models.Edition.find_existing(
|
||||
{'openlibraryKey': 'OL1234'})
|
||||
self.assertEqual(result, book)
|
||||
|
||||
|
||||
def test_get_recipients_public_object(self):
|
||||
''' determines the recipients for an object's broadcast '''
|
||||
MockSelf = namedtuple('Self', ('privacy'))
|
||||
mock_self = MockSelf('public')
|
||||
recipients = ActivitypubMixin.get_recipients(mock_self)
|
||||
self.assertEqual(len(recipients), 1)
|
||||
self.assertEqual(recipients[0], self.remote_user.inbox)
|
||||
|
||||
|
||||
def test_get_recipients_public_user_object_no_followers(self):
|
||||
''' determines the recipients for a user's object broadcast '''
|
||||
MockSelf = namedtuple('Self', ('privacy', 'user'))
|
||||
mock_self = MockSelf('public', self.local_user)
|
||||
|
||||
recipients = ActivitypubMixin.get_recipients(mock_self)
|
||||
self.assertEqual(len(recipients), 0)
|
||||
|
||||
|
||||
def test_get_recipients_public_user_object(self):
|
||||
''' determines the recipients for a user's object broadcast '''
|
||||
MockSelf = namedtuple('Self', ('privacy', 'user'))
|
||||
mock_self = MockSelf('public', self.local_user)
|
||||
self.local_user.followers.add(self.remote_user)
|
||||
|
||||
recipients = ActivitypubMixin.get_recipients(mock_self)
|
||||
self.assertEqual(len(recipients), 1)
|
||||
self.assertEqual(recipients[0], self.remote_user.inbox)
|
||||
|
||||
|
||||
def test_get_recipients_public_user_object_with_mention(self):
|
||||
''' determines the recipients for a user's object broadcast '''
|
||||
MockSelf = namedtuple('Self', ('privacy', 'user'))
|
||||
mock_self = MockSelf('public', self.local_user)
|
||||
self.local_user.followers.add(self.remote_user)
|
||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||
another_remote_user = models.User.objects.create_user(
|
||||
'nutria', 'nutria@nutria.com', 'nutriaword',
|
||||
local=False,
|
||||
remote_id='https://example.com/users/nutria',
|
||||
inbox='https://example.com/users/nutria/inbox',
|
||||
outbox='https://example.com/users/nutria/outbox',
|
||||
)
|
||||
MockSelf = namedtuple('Self', ('privacy', 'user', 'recipients'))
|
||||
mock_self = MockSelf('public', self.local_user, [another_remote_user])
|
||||
|
||||
recipients = ActivitypubMixin.get_recipients(mock_self)
|
||||
self.assertEqual(len(recipients), 2)
|
||||
self.assertEqual(recipients[0], another_remote_user.inbox)
|
||||
self.assertEqual(recipients[1], self.remote_user.inbox)
|
||||
|
||||
|
||||
def test_get_recipients_direct(self):
|
||||
''' determines the recipients for a user's object broadcast '''
|
||||
MockSelf = namedtuple('Self', ('privacy', 'user'))
|
||||
mock_self = MockSelf('public', self.local_user)
|
||||
self.local_user.followers.add(self.remote_user)
|
||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||
another_remote_user = models.User.objects.create_user(
|
||||
'nutria', 'nutria@nutria.com', 'nutriaword',
|
||||
local=False,
|
||||
remote_id='https://example.com/users/nutria',
|
||||
inbox='https://example.com/users/nutria/inbox',
|
||||
outbox='https://example.com/users/nutria/outbox',
|
||||
)
|
||||
MockSelf = namedtuple('Self', ('privacy', 'user', 'recipients'))
|
||||
mock_self = MockSelf('direct', self.local_user, [another_remote_user])
|
||||
|
||||
recipients = ActivitypubMixin.get_recipients(mock_self)
|
||||
self.assertEqual(len(recipients), 1)
|
||||
self.assertEqual(recipients[0], another_remote_user.inbox)
|
||||
|
||||
|
||||
def test_get_recipients_combine_inboxes(self):
|
||||
''' should combine users with the same shared_inbox '''
|
||||
self.remote_user.shared_inbox = 'http://example.com/inbox'
|
||||
self.remote_user.save(broadcast=False)
|
||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||
another_remote_user = models.User.objects.create_user(
|
||||
'nutria', 'nutria@nutria.com', 'nutriaword',
|
||||
local=False,
|
||||
remote_id='https://example.com/users/nutria',
|
||||
inbox='https://example.com/users/nutria/inbox',
|
||||
shared_inbox='http://example.com/inbox',
|
||||
outbox='https://example.com/users/nutria/outbox',
|
||||
)
|
||||
MockSelf = namedtuple('Self', ('privacy', 'user'))
|
||||
mock_self = MockSelf('public', self.local_user)
|
||||
self.local_user.followers.add(self.remote_user)
|
||||
self.local_user.followers.add(another_remote_user)
|
||||
|
||||
recipients = ActivitypubMixin.get_recipients(mock_self)
|
||||
self.assertEqual(len(recipients), 1)
|
||||
self.assertEqual(recipients[0], 'http://example.com/inbox')
|
||||
|
||||
|
||||
def test_get_recipients_software(self):
|
||||
''' should differentiate between bookwyrm and other remote users '''
|
||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||
another_remote_user = models.User.objects.create_user(
|
||||
'nutria', 'nutria@nutria.com', 'nutriaword',
|
||||
local=False,
|
||||
remote_id='https://example.com/users/nutria',
|
||||
inbox='https://example.com/users/nutria/inbox',
|
||||
outbox='https://example.com/users/nutria/outbox',
|
||||
bookwyrm_user=False,
|
||||
)
|
||||
MockSelf = namedtuple('Self', ('privacy', 'user'))
|
||||
mock_self = MockSelf('public', self.local_user)
|
||||
self.local_user.followers.add(self.remote_user)
|
||||
self.local_user.followers.add(another_remote_user)
|
||||
|
||||
recipients = ActivitypubMixin.get_recipients(mock_self)
|
||||
self.assertEqual(len(recipients), 2)
|
||||
|
||||
recipients = ActivitypubMixin.get_recipients(
|
||||
mock_self, software='bookwyrm')
|
||||
self.assertEqual(len(recipients), 1)
|
||||
self.assertEqual(recipients[0], self.remote_user.inbox)
|
||||
|
||||
recipients = ActivitypubMixin.get_recipients(
|
||||
mock_self, software='other')
|
||||
self.assertEqual(len(recipients), 1)
|
||||
self.assertEqual(recipients[0], another_remote_user.inbox)
|
||||
|
||||
|
||||
# ObjectMixin
|
||||
def test_object_save_create(self):
|
||||
''' should save uneventufully when broadcast is disabled '''
|
||||
class Success(Exception):
|
||||
''' this means we got to the right method '''
|
||||
|
||||
class ObjectModel(ObjectMixin, base_model.BookWyrmModel):
|
||||
''' real simple mock model because BookWyrmModel is abstract '''
|
||||
user = models.fields.ForeignKey('User', on_delete=db.models.CASCADE)
|
||||
def save(self, *args, **kwargs):
|
||||
with patch('django.db.models.Model.save'):
|
||||
super().save(*args, **kwargs)
|
||||
def broadcast(self, activity, sender, **kwargs):#pylint: disable=arguments-differ
|
||||
''' do something '''
|
||||
raise Success()
|
||||
def to_create_activity(self, user):#pylint: disable=arguments-differ
|
||||
return {}
|
||||
|
||||
with self.assertRaises(Success):
|
||||
ObjectModel(user=self.local_user).save()
|
||||
|
||||
ObjectModel(user=self.remote_user).save()
|
||||
ObjectModel(user=self.local_user).save(broadcast=False)
|
||||
ObjectModel(user=None).save()
|
||||
|
||||
|
||||
def test_object_save_update(self):
|
||||
''' should save uneventufully when broadcast is disabled '''
|
||||
class Success(Exception):
|
||||
''' this means we got to the right method '''
|
||||
|
||||
class UpdateObjectModel(ObjectMixin, base_model.BookWyrmModel):
|
||||
''' real simple mock model because BookWyrmModel is abstract '''
|
||||
user = models.fields.ForeignKey('User', on_delete=db.models.CASCADE)
|
||||
last_edited_by = models.fields.ForeignKey(
|
||||
'User', on_delete=db.models.CASCADE)
|
||||
def save(self, *args, **kwargs):
|
||||
with patch('django.db.models.Model.save'):
|
||||
super().save(*args, **kwargs)
|
||||
def to_update_activity(self, user):
|
||||
raise Success()
|
||||
|
||||
with self.assertRaises(Success):
|
||||
UpdateObjectModel(id=1, user=self.local_user).save()
|
||||
with self.assertRaises(Success):
|
||||
UpdateObjectModel(id=1, last_edited_by=self.local_user).save()
|
||||
|
||||
|
||||
def test_object_save_delete(self):
|
||||
''' should create delete activities when objects are deleted by flag '''
|
||||
class ActivitySuccess(Exception):
|
||||
''' this means we got to the right method '''
|
||||
|
||||
class DeletableObjectModel(ObjectMixin, base_model.BookWyrmModel):
|
||||
''' real simple mock model because BookWyrmModel is abstract '''
|
||||
user = models.fields.ForeignKey('User', on_delete=db.models.CASCADE)
|
||||
deleted = models.fields.BooleanField()
|
||||
def save(self, *args, **kwargs):
|
||||
with patch('django.db.models.Model.save'):
|
||||
super().save(*args, **kwargs)
|
||||
def to_delete_activity(self, user):
|
||||
raise ActivitySuccess()
|
||||
|
||||
with self.assertRaises(ActivitySuccess):
|
||||
DeletableObjectModel(
|
||||
id=1, user=self.local_user, deleted=True).save()
|
||||
|
||||
|
||||
def test_to_create_activity(self):
|
||||
''' wrapper for ActivityPub "create" action '''
|
||||
object_activity = {
|
||||
'to': 'to field', 'cc': 'cc field',
|
||||
'content': 'hi',
|
||||
'published': '2020-12-04T17:52:22.623807+00:00',
|
||||
}
|
||||
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
|
||||
mock_self = MockSelf(
|
||||
'https://example.com/status/1',
|
||||
lambda *args: object_activity
|
||||
)
|
||||
activity = ObjectMixin.to_create_activity(
|
||||
mock_self, self.local_user)
|
||||
self.assertEqual(
|
||||
activity['id'],
|
||||
'https://example.com/status/1/activity'
|
||||
)
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Create')
|
||||
self.assertEqual(activity['to'], 'to field')
|
||||
self.assertEqual(activity['cc'], 'cc field')
|
||||
self.assertEqual(activity['object'], object_activity)
|
||||
self.assertEqual(
|
||||
activity['signature'].creator,
|
||||
'%s#main-key' % self.local_user.remote_id
|
||||
)
|
||||
|
||||
def test_to_delete_activity(self):
|
||||
''' wrapper for Delete activity '''
|
||||
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
|
||||
mock_self = MockSelf(
|
||||
'https://example.com/status/1',
|
||||
lambda *args: {}
|
||||
)
|
||||
activity = ObjectMixin.to_delete_activity(
|
||||
mock_self, self.local_user)
|
||||
self.assertEqual(
|
||||
activity['id'],
|
||||
'https://example.com/status/1/activity'
|
||||
)
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Delete')
|
||||
self.assertEqual(
|
||||
activity['to'],
|
||||
['%s/followers' % self.local_user.remote_id])
|
||||
self.assertEqual(
|
||||
activity['cc'],
|
||||
['https://www.w3.org/ns/activitystreams#Public'])
|
||||
|
||||
|
||||
def test_to_update_activity(self):
|
||||
''' ditto above but for Update '''
|
||||
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
|
||||
mock_self = MockSelf(
|
||||
'https://example.com/status/1',
|
||||
lambda *args: {}
|
||||
)
|
||||
activity = ObjectMixin.to_update_activity(
|
||||
mock_self, self.local_user)
|
||||
self.assertIsNotNone(
|
||||
re.match(
|
||||
r'^https:\/\/example\.com\/status\/1#update\/.*',
|
||||
activity['id']
|
||||
)
|
||||
)
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Update')
|
||||
self.assertEqual(
|
||||
activity['to'],
|
||||
['https://www.w3.org/ns/activitystreams#Public'])
|
||||
self.assertEqual(activity['object'], {})
|
||||
|
||||
|
||||
# Activity mixin
|
||||
def test_to_undo_activity(self):
|
||||
''' and again, for Undo '''
|
||||
MockSelf = namedtuple('Self', ('remote_id', 'to_activity', 'user'))
|
||||
mock_self = MockSelf(
|
||||
'https://example.com/status/1',
|
||||
lambda *args: {},
|
||||
self.local_user,
|
||||
)
|
||||
activity = ActivityMixin.to_undo_activity(mock_self)
|
||||
self.assertEqual(
|
||||
activity['id'],
|
||||
'https://example.com/status/1#undo'
|
||||
)
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Undo')
|
||||
self.assertEqual(activity['object'], {})
|
|
@ -1,13 +1,8 @@
|
|||
''' testing models '''
|
||||
from collections import namedtuple
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from django.test import TestCase
|
||||
|
||||
from bookwyrm.activitypub.base_activity import ActivityObject
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models import base_model
|
||||
from bookwyrm.models.base_model import ActivitypubMixin
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
class BaseModel(TestCase):
|
||||
|
@ -48,173 +43,3 @@ class BaseModel(TestCase):
|
|||
instance.remote_id = None
|
||||
base_model.execute_after_save(None, instance, False)
|
||||
self.assertIsNone(instance.remote_id)
|
||||
|
||||
def test_to_create_activity(self):
|
||||
''' wrapper for ActivityPub "create" action '''
|
||||
user = models.User.objects.create_user(
|
||||
'mouse', 'mouse@mouse.com', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
|
||||
object_activity = {
|
||||
'to': 'to field', 'cc': 'cc field',
|
||||
'content': 'hi',
|
||||
'published': '2020-12-04T17:52:22.623807+00:00',
|
||||
}
|
||||
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
|
||||
mock_self = MockSelf(
|
||||
'https://example.com/status/1',
|
||||
lambda *args: object_activity
|
||||
)
|
||||
activity = ActivitypubMixin.to_create_activity(mock_self, user)
|
||||
self.assertEqual(
|
||||
activity['id'],
|
||||
'https://example.com/status/1/activity'
|
||||
)
|
||||
self.assertEqual(activity['actor'], user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Create')
|
||||
self.assertEqual(activity['to'], 'to field')
|
||||
self.assertEqual(activity['cc'], 'cc field')
|
||||
self.assertEqual(activity['object'], object_activity)
|
||||
self.assertEqual(
|
||||
activity['signature'].creator,
|
||||
'%s#main-key' % user.remote_id
|
||||
)
|
||||
|
||||
def test_to_delete_activity(self):
|
||||
''' wrapper for Delete activity '''
|
||||
user = models.User.objects.create_user(
|
||||
'mouse', 'mouse@mouse.com', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
|
||||
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
|
||||
mock_self = MockSelf(
|
||||
'https://example.com/status/1',
|
||||
lambda *args: {}
|
||||
)
|
||||
activity = ActivitypubMixin.to_delete_activity(mock_self, user)
|
||||
self.assertEqual(
|
||||
activity['id'],
|
||||
'https://example.com/status/1/activity'
|
||||
)
|
||||
self.assertEqual(activity['actor'], user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Delete')
|
||||
self.assertEqual(
|
||||
activity['to'],
|
||||
['%s/followers' % user.remote_id])
|
||||
self.assertEqual(
|
||||
activity['cc'],
|
||||
['https://www.w3.org/ns/activitystreams#Public'])
|
||||
|
||||
def test_to_update_activity(self):
|
||||
''' ditto above but for Update '''
|
||||
user = models.User.objects.create_user(
|
||||
'mouse', 'mouse@mouse.com', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
|
||||
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
|
||||
mock_self = MockSelf(
|
||||
'https://example.com/status/1',
|
||||
lambda *args: {}
|
||||
)
|
||||
activity = ActivitypubMixin.to_update_activity(mock_self, user)
|
||||
self.assertIsNotNone(
|
||||
re.match(
|
||||
r'^https:\/\/example\.com\/status\/1#update\/.*',
|
||||
activity['id']
|
||||
)
|
||||
)
|
||||
self.assertEqual(activity['actor'], user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Update')
|
||||
self.assertEqual(
|
||||
activity['to'],
|
||||
['https://www.w3.org/ns/activitystreams#Public'])
|
||||
self.assertEqual(activity['object'], {})
|
||||
|
||||
def test_to_undo_activity(self):
|
||||
''' and again, for Undo '''
|
||||
user = models.User.objects.create_user(
|
||||
'mouse', 'mouse@mouse.com', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
|
||||
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
|
||||
mock_self = MockSelf(
|
||||
'https://example.com/status/1',
|
||||
lambda *args: {}
|
||||
)
|
||||
activity = ActivitypubMixin.to_undo_activity(mock_self, user)
|
||||
self.assertEqual(
|
||||
activity['id'],
|
||||
'https://example.com/status/1#undo'
|
||||
)
|
||||
self.assertEqual(activity['actor'], user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Undo')
|
||||
self.assertEqual(activity['object'], {})
|
||||
|
||||
|
||||
def test_to_activity(self):
|
||||
''' model to ActivityPub json '''
|
||||
@dataclass(init=False)
|
||||
class TestActivity(ActivityObject):
|
||||
''' real simple mock '''
|
||||
type: str = 'Test'
|
||||
|
||||
class TestModel(ActivitypubMixin, base_model.BookWyrmModel):
|
||||
''' real simple mock model because BookWyrmModel is abstract '''
|
||||
|
||||
instance = TestModel()
|
||||
instance.remote_id = 'https://www.example.com/test'
|
||||
instance.activity_serializer = TestActivity
|
||||
|
||||
activity = instance.to_activity()
|
||||
self.assertIsInstance(activity, dict)
|
||||
self.assertEqual(activity['id'], 'https://www.example.com/test')
|
||||
self.assertEqual(activity['type'], 'Test')
|
||||
|
||||
|
||||
def test_find_existing_by_remote_id(self):
|
||||
''' attempt to match a remote id to an object in the db '''
|
||||
# uses a different remote id scheme
|
||||
# this isn't really part of this test directly but it's helpful to state
|
||||
book = models.Edition.objects.create(
|
||||
title='Test Edition', remote_id='http://book.com/book')
|
||||
user = models.User.objects.create_user(
|
||||
'mouse', 'mouse@mouse.mouse', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
user.remote_id = 'http://example.com/a/b'
|
||||
user.save()
|
||||
|
||||
self.assertEqual(book.origin_id, 'http://book.com/book')
|
||||
self.assertNotEqual(book.remote_id, 'http://book.com/book')
|
||||
|
||||
# uses subclasses
|
||||
models.Comment.objects.create(
|
||||
user=user, content='test status', book=book, \
|
||||
remote_id='https://comment.net')
|
||||
|
||||
result = models.User.find_existing_by_remote_id('hi')
|
||||
self.assertIsNone(result)
|
||||
|
||||
result = models.User.find_existing_by_remote_id(
|
||||
'http://example.com/a/b')
|
||||
self.assertEqual(result, user)
|
||||
|
||||
# test using origin id
|
||||
result = models.Edition.find_existing_by_remote_id(
|
||||
'http://book.com/book')
|
||||
self.assertEqual(result, book)
|
||||
|
||||
# test subclass match
|
||||
result = models.Status.find_existing_by_remote_id(
|
||||
'https://comment.net')
|
||||
|
||||
|
||||
def test_find_existing(self):
|
||||
''' match a blob of data to a model '''
|
||||
book = models.Edition.objects.create(
|
||||
title='Test edition',
|
||||
openlibrary_key='OL1234',
|
||||
)
|
||||
|
||||
result = models.Edition.find_existing(
|
||||
{'openlibraryKey': 'OL1234'})
|
||||
self.assertEqual(result, book)
|
||||
|
|
|
@ -19,7 +19,8 @@ from django.utils import timezone
|
|||
|
||||
from bookwyrm.activitypub.base_activity import ActivityObject
|
||||
from bookwyrm.models import fields, User, Status
|
||||
from bookwyrm.models.base_model import ActivitypubMixin, BookWyrmModel
|
||||
from bookwyrm.models.base_model import BookWyrmModel
|
||||
from bookwyrm.models.activitypub_mixin import ActivitypubMixin
|
||||
|
||||
#pylint: disable=too-many-public-methods
|
||||
class ActivitypubFields(TestCase):
|
||||
|
@ -177,7 +178,8 @@ class ActivitypubFields(TestCase):
|
|||
self.assertEqual(model_instance.privacy_field, 'unlisted')
|
||||
|
||||
|
||||
def test_privacy_field_set_activity_from_field(self):
|
||||
@patch('bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast')
|
||||
def test_privacy_field_set_activity_from_field(self, _):
|
||||
''' translate between to/cc fields and privacy '''
|
||||
user = User.objects.create_user(
|
||||
'rat', 'rat@rat.rat', 'ratword',
|
||||
|
@ -194,13 +196,15 @@ class ActivitypubFields(TestCase):
|
|||
self.assertEqual(activity['to'], [public])
|
||||
self.assertEqual(activity['cc'], [followers])
|
||||
|
||||
model_instance = Status.objects.create(user=user, privacy='unlisted')
|
||||
model_instance = Status.objects.create(
|
||||
user=user, content='hi', privacy='unlisted')
|
||||
activity = {}
|
||||
instance.set_activity_from_field(activity, model_instance)
|
||||
self.assertEqual(activity['to'], [followers])
|
||||
self.assertEqual(activity['cc'], [public])
|
||||
|
||||
model_instance = Status.objects.create(user=user, privacy='followers')
|
||||
model_instance = Status.objects.create(
|
||||
user=user, content='hi', privacy='followers')
|
||||
activity = {}
|
||||
instance.set_activity_from_field(activity, model_instance)
|
||||
self.assertEqual(activity['to'], [followers])
|
||||
|
@ -208,6 +212,7 @@ class ActivitypubFields(TestCase):
|
|||
|
||||
model_instance = Status.objects.create(
|
||||
user=user,
|
||||
content='hi',
|
||||
privacy='direct',
|
||||
)
|
||||
model_instance.mention_users.set([user])
|
||||
|
@ -289,12 +294,13 @@ class ActivitypubFields(TestCase):
|
|||
'mouse', 'mouse@mouse.mouse', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
user.remote_id = 'https://example.com/user/mouse'
|
||||
user.save()
|
||||
user.save(broadcast=False)
|
||||
User.objects.create_user(
|
||||
'rat', 'rat@rat.rat', 'ratword',
|
||||
local=True, localname='rat')
|
||||
|
||||
value = instance.field_from_activity(userdata)
|
||||
with patch('bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast'):
|
||||
value = instance.field_from_activity(userdata)
|
||||
self.assertEqual(value, user)
|
||||
|
||||
|
||||
|
@ -393,7 +399,8 @@ class ActivitypubFields(TestCase):
|
|||
|
||||
|
||||
@responses.activate
|
||||
def test_image_field(self):
|
||||
@patch('bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast')
|
||||
def test_image_field(self, _):
|
||||
''' storing images '''
|
||||
user = User.objects.create_user(
|
||||
'mouse', 'mouse@mouse.mouse', 'mouseword',
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
''' testing models '''
|
||||
from unittest.mock import patch
|
||||
from django.test import TestCase
|
||||
|
||||
from bookwyrm import models, settings
|
||||
|
||||
|
||||
@patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay')
|
||||
class List(TestCase):
|
||||
''' some activitypub oddness ahead '''
|
||||
def setUp(self):
|
||||
|
@ -11,17 +13,18 @@ class List(TestCase):
|
|||
self.user = models.User.objects.create_user(
|
||||
'mouse', 'mouse@mouse.mouse', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
self.list = models.List.objects.create(
|
||||
name='Test List', user=self.user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
self.list = models.List.objects.create(
|
||||
name='Test List', user=self.user)
|
||||
|
||||
def test_remote_id(self):
|
||||
def test_remote_id(self, _):
|
||||
''' shelves use custom remote ids '''
|
||||
expected_id = 'https://%s/list/%d' % \
|
||||
(settings.DOMAIN, self.list.id)
|
||||
self.assertEqual(self.list.get_remote_id(), expected_id)
|
||||
|
||||
|
||||
def test_to_activity(self):
|
||||
def test_to_activity(self, _):
|
||||
''' jsonify it '''
|
||||
activity_json = self.list.to_activity()
|
||||
self.assertIsInstance(activity_json, dict)
|
||||
|
@ -31,24 +34,24 @@ class List(TestCase):
|
|||
self.assertEqual(activity_json['name'], 'Test List')
|
||||
self.assertEqual(activity_json['owner'], self.user.remote_id)
|
||||
|
||||
def test_list_item(self):
|
||||
def test_list_item(self, _):
|
||||
''' a list entry '''
|
||||
work = models.Work.objects.create(title='hello')
|
||||
book = models.Edition.objects.create(title='hi', parent_work=work)
|
||||
item = models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
book=book,
|
||||
added_by=self.user,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.assertTrue(item.approved)
|
||||
|
||||
add_activity = item.to_add_activity(self.user)
|
||||
add_activity = item.to_add_activity()
|
||||
self.assertEqual(add_activity['actor'], self.user.remote_id)
|
||||
self.assertEqual(add_activity['object']['id'], book.remote_id)
|
||||
self.assertEqual(add_activity['target'], self.list.remote_id)
|
||||
|
||||
remove_activity = item.to_remove_activity(self.user)
|
||||
remove_activity = item.to_remove_activity()
|
||||
self.assertEqual(remove_activity['actor'], self.user.remote_id)
|
||||
self.assertEqual(remove_activity['object']['id'], book.remote_id)
|
||||
self.assertEqual(remove_activity['target'], self.list.remote_id)
|
||||
|
|
|
@ -6,7 +6,9 @@ from bookwyrm import models
|
|||
|
||||
|
||||
class Relationship(TestCase):
|
||||
''' following, blocking, stuff like that '''
|
||||
def setUp(self):
|
||||
''' we need some users for this '''
|
||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||
self.remote_user = models.User.objects.create_user(
|
||||
'rat', 'rat@rat.com', 'ratword',
|
||||
|
@ -19,68 +21,31 @@ class Relationship(TestCase):
|
|||
'mouse', 'mouse@mouse.com', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
self.local_user.remote_id = 'http://local.com/user/mouse'
|
||||
self.local_user.save()
|
||||
self.local_user.save(broadcast=False)
|
||||
|
||||
def test_user_follows(self):
|
||||
rel = models.UserFollows.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
rel.remote_id,
|
||||
'http://local.com/user/mouse#follows/%d' % rel.id
|
||||
)
|
||||
''' create a follow relationship '''
|
||||
with patch('bookwyrm.models.activitypub_mixin.ActivityMixin.broadcast'):
|
||||
rel = models.UserFollows.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user
|
||||
)
|
||||
|
||||
activity = rel.to_activity()
|
||||
self.assertEqual(activity['id'], rel.remote_id)
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['object'], self.remote_user.remote_id)
|
||||
|
||||
def test_user_follow_accept_serialization(self):
|
||||
rel = models.UserFollows.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
rel.remote_id,
|
||||
'http://local.com/user/mouse#follows/%d' % rel.id
|
||||
)
|
||||
accept = rel.to_accept_activity()
|
||||
self.assertEqual(accept['type'], 'Accept')
|
||||
self.assertEqual(
|
||||
accept['id'],
|
||||
'http://local.com/user/mouse#accepts/%d' % rel.id
|
||||
)
|
||||
self.assertEqual(accept['actor'], self.remote_user.remote_id)
|
||||
self.assertEqual(accept['object']['id'], rel.remote_id)
|
||||
self.assertEqual(accept['object']['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(accept['object']['object'], self.remote_user.remote_id)
|
||||
|
||||
def test_user_follow_reject_serialization(self):
|
||||
rel = models.UserFollows.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
rel.remote_id,
|
||||
'http://local.com/user/mouse#follows/%d' % rel.id
|
||||
)
|
||||
reject = rel.to_reject_activity()
|
||||
self.assertEqual(reject['type'], 'Reject')
|
||||
self.assertEqual(
|
||||
reject['id'],
|
||||
'http://local.com/user/mouse#rejects/%d' % rel.id
|
||||
)
|
||||
self.assertEqual(reject['actor'], self.remote_user.remote_id)
|
||||
self.assertEqual(reject['object']['id'], rel.remote_id)
|
||||
self.assertEqual(reject['object']['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(reject['object']['object'], self.remote_user.remote_id)
|
||||
|
||||
|
||||
def test_user_follows_from_request(self):
|
||||
''' convert a follow request into a follow '''
|
||||
real_broadcast = models.UserFollowRequest.broadcast
|
||||
def mock_broadcast(_, activity, user):
|
||||
''' introspect what's being sent out '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
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
|
||||
|
@ -99,14 +64,18 @@ class Relationship(TestCase):
|
|||
self.assertEqual(rel.status, 'follows')
|
||||
self.assertEqual(rel.user_subject, self.local_user)
|
||||
self.assertEqual(rel.user_object, self.remote_user)
|
||||
models.UserFollowRequest.broadcast = real_broadcast
|
||||
|
||||
|
||||
|
||||
def test_user_follows_from_request_custom_remote_id(self):
|
||||
request = models.UserFollowRequest.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user,
|
||||
remote_id='http://antoher.server/sdkfhskdjf/23'
|
||||
)
|
||||
''' store a specific remote id for a relationship provided by remote '''
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
request = models.UserFollowRequest.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user,
|
||||
remote_id='http://antoher.server/sdkfhskdjf/23'
|
||||
)
|
||||
self.assertEqual(
|
||||
request.remote_id,
|
||||
'http://antoher.server/sdkfhskdjf/23'
|
||||
|
@ -121,3 +90,67 @@ class Relationship(TestCase):
|
|||
self.assertEqual(rel.status, 'follows')
|
||||
self.assertEqual(rel.user_subject, self.local_user)
|
||||
self.assertEqual(rel.user_object, self.remote_user)
|
||||
|
||||
|
||||
def test_follow_request_activity(self):
|
||||
''' 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(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user,
|
||||
)
|
||||
models.UserFollowRequest.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_follow_request_accept(self):
|
||||
''' 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'], request.remote_id)
|
||||
|
||||
models.UserFollowRequest.broadcast = mock_broadcast
|
||||
request = models.UserFollowRequest.objects.create(
|
||||
user_subject=self.remote_user,
|
||||
user_object=self.local_user,
|
||||
)
|
||||
request.accept()
|
||||
|
||||
self.assertFalse(models.UserFollowRequest.objects.exists())
|
||||
self.assertTrue(models.UserFollows.objects.exists())
|
||||
rel = models.UserFollows.objects.get()
|
||||
self.assertEqual(rel.user_subject, self.remote_user)
|
||||
self.assertEqual(rel.user_object, self.local_user)
|
||||
models.UserFollowRequest.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_follow_request_reject(self):
|
||||
''' 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
|
||||
request = models.UserFollowRequest.objects.create(
|
||||
user_subject=self.remote_user,
|
||||
user_object=self.local_user,
|
||||
)
|
||||
request.reject()
|
||||
|
||||
self.assertFalse(models.UserFollowRequest.objects.exists())
|
||||
self.assertFalse(models.UserFollows.objects.exists())
|
||||
models.UserFollowRequest.broadcast = real_broadcast
|
||||
|
|
|
@ -8,24 +8,113 @@ class Shelf(TestCase):
|
|||
''' some activitypub oddness ahead '''
|
||||
def setUp(self):
|
||||
''' look, a shelf '''
|
||||
self.user = models.User.objects.create_user(
|
||||
self.local_user = models.User.objects.create_user(
|
||||
'mouse', 'mouse@mouse.mouse', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
self.shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf', identifier='test-shelf', user=self.user)
|
||||
work = models.Work.objects.create(title='Test Work')
|
||||
self.book = models.Edition.objects.create(
|
||||
title='test book',
|
||||
parent_work=work)
|
||||
|
||||
def test_remote_id(self):
|
||||
''' shelves use custom remote ids '''
|
||||
real_broadcast = models.Shelf.broadcast
|
||||
def broadcast_mock(_, activity, user, **kwargs):
|
||||
''' nah '''
|
||||
models.Shelf.broadcast = broadcast_mock
|
||||
shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf', identifier='test-shelf',
|
||||
user=self.local_user)
|
||||
expected_id = 'https://%s/user/mouse/shelf/test-shelf' % settings.DOMAIN
|
||||
self.assertEqual(self.shelf.get_remote_id(), expected_id)
|
||||
self.assertEqual(shelf.get_remote_id(), expected_id)
|
||||
models.Shelf.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_to_activity(self):
|
||||
''' jsonify it '''
|
||||
activity_json = self.shelf.to_activity()
|
||||
real_broadcast = models.Shelf.broadcast
|
||||
def empty_mock(_, activity, user, **kwargs):
|
||||
''' nah '''
|
||||
models.Shelf.broadcast = empty_mock
|
||||
shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf', identifier='test-shelf',
|
||||
user=self.local_user)
|
||||
activity_json = shelf.to_activity()
|
||||
self.assertIsInstance(activity_json, dict)
|
||||
self.assertEqual(activity_json['id'], self.shelf.remote_id)
|
||||
self.assertEqual(activity_json['id'], shelf.remote_id)
|
||||
self.assertEqual(activity_json['totalItems'], 0)
|
||||
self.assertEqual(activity_json['type'], 'Shelf')
|
||||
self.assertEqual(activity_json['name'], 'Test Shelf')
|
||||
self.assertEqual(activity_json['owner'], self.user.remote_id)
|
||||
self.assertEqual(activity_json['owner'], self.local_user.remote_id)
|
||||
models.Shelf.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_create_update_shelf(self):
|
||||
''' create and broadcast shelf creation '''
|
||||
real_broadcast = models.Shelf.broadcast
|
||||
def create_mock(_, activity, user, **kwargs):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Create')
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['object']['name'], 'Test Shelf')
|
||||
models.Shelf.broadcast = create_mock
|
||||
|
||||
shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf', identifier='test-shelf', user=self.local_user)
|
||||
|
||||
def update_mock(_, activity, user, **kwargs):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Update')
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['object']['name'], 'arthur russel')
|
||||
models.Shelf.broadcast = update_mock
|
||||
|
||||
shelf.name = 'arthur russel'
|
||||
shelf.save()
|
||||
self.assertEqual(shelf.name, 'arthur russel')
|
||||
models.Shelf.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_shelve(self):
|
||||
''' create and broadcast shelf creation '''
|
||||
real_broadcast = models.Shelf.broadcast
|
||||
real_shelfbook_broadcast = models.ShelfBook.broadcast
|
||||
|
||||
def add_mock(_, activity, user, **kwargs):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Add')
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['object']['id'], self.book.remote_id)
|
||||
self.assertEqual(activity['target'], shelf.remote_id)
|
||||
|
||||
def remove_mock(_, activity, user, **kwargs):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Remove')
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['object']['id'], self.book.remote_id)
|
||||
self.assertEqual(activity['target'], shelf.remote_id)
|
||||
|
||||
def empty_mock(_, activity, user, **kwargs):
|
||||
''' nah '''
|
||||
|
||||
models.Shelf.broadcast = empty_mock
|
||||
shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf', identifier='test-shelf', user=self.local_user)
|
||||
|
||||
models.ShelfBook.broadcast = add_mock
|
||||
shelf_book = models.ShelfBook.objects.create(
|
||||
shelf=shelf,
|
||||
user=self.local_user,
|
||||
book=self.book)
|
||||
self.assertEqual(shelf.books.first(), self.book)
|
||||
|
||||
models.ShelfBook.broadcast = remove_mock
|
||||
shelf_book.delete()
|
||||
self.assertFalse(shelf.books.exists())
|
||||
|
||||
models.ShelfBook.broadcast = real_shelfbook_broadcast
|
||||
models.Shelf.broadcast = real_broadcast
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
''' testing models '''
|
||||
from unittest.mock import patch
|
||||
from io import BytesIO
|
||||
import pathlib
|
||||
|
||||
|
@ -11,6 +12,7 @@ from django.utils import timezone
|
|||
from bookwyrm import models, settings
|
||||
|
||||
|
||||
@patch('bookwyrm.models.Status.broadcast')
|
||||
class Status(TestCase):
|
||||
''' lotta types of statuses '''
|
||||
def setUp(self):
|
||||
|
@ -24,13 +26,14 @@ class Status(TestCase):
|
|||
'../../static/images/default_avi.jpg')
|
||||
image = Image.open(image_file)
|
||||
output = BytesIO()
|
||||
image.save(output, format=image.format)
|
||||
self.book.cover.save(
|
||||
'test.jpg',
|
||||
ContentFile(output.getvalue())
|
||||
)
|
||||
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 '''
|
||||
status = models.Status.objects.create(content='bleh', user=self.user)
|
||||
expected_id = 'https://%s/user/mouse/status/%d' % \
|
||||
|
@ -38,7 +41,7 @@ class Status(TestCase):
|
|||
self.assertEqual(status.remote_id, expected_id)
|
||||
self.assertEqual(status.privacy, 'public')
|
||||
|
||||
def test_replies(self):
|
||||
def test_replies(self, _):
|
||||
''' get a list of replies '''
|
||||
parent = models.Status.objects.create(content='hi', user=self.user)
|
||||
child = models.Status.objects.create(
|
||||
|
@ -54,7 +57,7 @@ class Status(TestCase):
|
|||
# should select subclasses
|
||||
self.assertIsInstance(replies.last(), models.Review)
|
||||
|
||||
def test_status_type(self):
|
||||
def test_status_type(self, _):
|
||||
''' class name '''
|
||||
self.assertEqual(models.Status().status_type, 'Note')
|
||||
self.assertEqual(models.Review().status_type, 'Review')
|
||||
|
@ -62,14 +65,14 @@ class Status(TestCase):
|
|||
self.assertEqual(models.Comment().status_type, 'Comment')
|
||||
self.assertEqual(models.Boost().status_type, 'Boost')
|
||||
|
||||
def test_boostable(self):
|
||||
def test_boostable(self, _):
|
||||
''' can a status be boosted, based on privacy '''
|
||||
self.assertTrue(models.Status(privacy='public').boostable)
|
||||
self.assertTrue(models.Status(privacy='unlisted').boostable)
|
||||
self.assertFalse(models.Status(privacy='followers').boostable)
|
||||
self.assertFalse(models.Status(privacy='direct').boostable)
|
||||
|
||||
def test_to_replies(self):
|
||||
def test_to_replies(self, _):
|
||||
''' activitypub replies collection '''
|
||||
parent = models.Status.objects.create(content='hi', user=self.user)
|
||||
child = models.Status.objects.create(
|
||||
|
@ -83,7 +86,7 @@ class Status(TestCase):
|
|||
self.assertEqual(replies['id'], '%s/replies' % parent.remote_id)
|
||||
self.assertEqual(replies['totalItems'], 2)
|
||||
|
||||
def test_status_to_activity(self):
|
||||
def test_status_to_activity(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.Status.objects.create(
|
||||
content='test content', user=self.user)
|
||||
|
@ -93,7 +96,7 @@ class Status(TestCase):
|
|||
self.assertEqual(activity['content'], 'test content')
|
||||
self.assertEqual(activity['sensitive'], False)
|
||||
|
||||
def test_status_to_activity_tombstone(self):
|
||||
def test_status_to_activity_tombstone(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.Status.objects.create(
|
||||
content='test content', user=self.user,
|
||||
|
@ -103,7 +106,7 @@ class Status(TestCase):
|
|||
self.assertEqual(activity['type'], 'Tombstone')
|
||||
self.assertFalse(hasattr(activity, 'content'))
|
||||
|
||||
def test_status_to_pure_activity(self):
|
||||
def test_status_to_pure_activity(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.Status.objects.create(
|
||||
content='test content', user=self.user)
|
||||
|
@ -114,7 +117,7 @@ class Status(TestCase):
|
|||
self.assertEqual(activity['sensitive'], False)
|
||||
self.assertEqual(activity['attachment'], [])
|
||||
|
||||
def test_generated_note_to_activity(self):
|
||||
def test_generated_note_to_activity(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.GeneratedNote.objects.create(
|
||||
content='test content', user=self.user)
|
||||
|
@ -127,7 +130,7 @@ class Status(TestCase):
|
|||
self.assertEqual(activity['sensitive'], False)
|
||||
self.assertEqual(len(activity['tag']), 2)
|
||||
|
||||
def test_generated_note_to_pure_activity(self):
|
||||
def test_generated_note_to_pure_activity(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.GeneratedNote.objects.create(
|
||||
content='test content', user=self.user)
|
||||
|
@ -149,7 +152,7 @@ class Status(TestCase):
|
|||
self.assertEqual(
|
||||
activity['attachment'][0].name, 'Test Edition cover')
|
||||
|
||||
def test_comment_to_activity(self):
|
||||
def test_comment_to_activity(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.Comment.objects.create(
|
||||
content='test content', user=self.user, book=self.book)
|
||||
|
@ -159,7 +162,7 @@ class Status(TestCase):
|
|||
self.assertEqual(activity['content'], 'test content')
|
||||
self.assertEqual(activity['inReplyToBook'], self.book.remote_id)
|
||||
|
||||
def test_comment_to_pure_activity(self):
|
||||
def test_comment_to_pure_activity(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.Comment.objects.create(
|
||||
content='test content', user=self.user, book=self.book)
|
||||
|
@ -176,7 +179,7 @@ class Status(TestCase):
|
|||
self.assertEqual(
|
||||
activity['attachment'][0].name, 'Test Edition cover')
|
||||
|
||||
def test_quotation_to_activity(self):
|
||||
def test_quotation_to_activity(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.Quotation.objects.create(
|
||||
quote='a sickening sense', content='test content',
|
||||
|
@ -188,7 +191,7 @@ class Status(TestCase):
|
|||
self.assertEqual(activity['content'], 'test content')
|
||||
self.assertEqual(activity['inReplyToBook'], self.book.remote_id)
|
||||
|
||||
def test_quotation_to_pure_activity(self):
|
||||
def test_quotation_to_pure_activity(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.Quotation.objects.create(
|
||||
quote='a sickening sense', content='test content',
|
||||
|
@ -206,7 +209,7 @@ class Status(TestCase):
|
|||
self.assertEqual(
|
||||
activity['attachment'][0].name, 'Test Edition cover')
|
||||
|
||||
def test_review_to_activity(self):
|
||||
def test_review_to_activity(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.Review.objects.create(
|
||||
name='Review name', content='test content', rating=3,
|
||||
|
@ -219,7 +222,7 @@ class Status(TestCase):
|
|||
self.assertEqual(activity['content'], 'test content')
|
||||
self.assertEqual(activity['inReplyToBook'], self.book.remote_id)
|
||||
|
||||
def test_review_to_pure_activity(self):
|
||||
def test_review_to_pure_activity(self, _):
|
||||
''' subclass of the base model version with a "pure" serializer '''
|
||||
status = models.Review.objects.create(
|
||||
name='Review name', content='test content', rating=3,
|
||||
|
@ -237,8 +240,15 @@ class Status(TestCase):
|
|||
self.assertEqual(
|
||||
activity['attachment'][0].name, 'Test Edition cover')
|
||||
|
||||
def test_favorite(self):
|
||||
def test_favorite(self, _):
|
||||
''' fav a status '''
|
||||
real_broadcast = models.Favorite.broadcast
|
||||
def fav_broadcast_mock(_, activity, user):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Like')
|
||||
models.Favorite.broadcast = fav_broadcast_mock
|
||||
|
||||
status = models.Status.objects.create(
|
||||
content='test content', user=self.user)
|
||||
fav = models.Favorite.objects.create(status=status, user=self.user)
|
||||
|
@ -251,8 +261,9 @@ class Status(TestCase):
|
|||
self.assertEqual(activity['type'], 'Like')
|
||||
self.assertEqual(activity['actor'], self.user.remote_id)
|
||||
self.assertEqual(activity['object'], status.remote_id)
|
||||
models.Favorite.broadcast = real_broadcast
|
||||
|
||||
def test_boost(self):
|
||||
def test_boost(self, _):
|
||||
''' boosting, this one's a bit fussy '''
|
||||
status = models.Status.objects.create(
|
||||
content='test content', user=self.user)
|
||||
|
@ -264,7 +275,7 @@ class Status(TestCase):
|
|||
self.assertEqual(activity['type'], 'Announce')
|
||||
self.assertEqual(activity, boost.to_activity(pure=True))
|
||||
|
||||
def test_notification(self):
|
||||
def test_notification(self, _):
|
||||
''' a simple model '''
|
||||
notification = models.Notification.objects.create(
|
||||
user=self.user, notification_type='FAVORITE')
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
from unittest.mock import patch
|
||||
from django.test import TestCase
|
||||
|
||||
from bookwyrm import models, broadcast
|
||||
|
||||
|
||||
class Book(TestCase):
|
||||
def setUp(self):
|
||||
self.user = models.User.objects.create_user(
|
||||
'mouse', 'mouse@mouse.mouse', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
|
||||
local_follower = models.User.objects.create_user(
|
||||
'joe', 'joe@mouse.mouse', 'jeoword',
|
||||
local=True, localname='joe')
|
||||
self.user.followers.add(local_follower)
|
||||
|
||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||
follower = models.User.objects.create_user(
|
||||
'rat', 'rat@mouse.mouse', 'ratword', local=False,
|
||||
remote_id='http://example.com/u/1',
|
||||
outbox='http://example.com/u/1/o',
|
||||
shared_inbox='http://example.com/inbox',
|
||||
inbox='http://example.com/u/1/inbox')
|
||||
self.user.followers.add(follower)
|
||||
|
||||
no_inbox_follower = models.User.objects.create_user(
|
||||
'hamster', 'hamster@mouse.mouse', 'hamword',
|
||||
shared_inbox=None, local=False,
|
||||
remote_id='http://example.com/u/2',
|
||||
outbox='http://example.com/u/2/o',
|
||||
inbox='http://example.com/u/2/inbox')
|
||||
self.user.followers.add(no_inbox_follower)
|
||||
|
||||
non_bw_follower = models.User.objects.create_user(
|
||||
'gerbil', 'gerb@mouse.mouse', 'gerbword',
|
||||
remote_id='http://example.com/u/3',
|
||||
outbox='http://example2.com/u/3/o',
|
||||
inbox='http://example2.com/u/3/inbox',
|
||||
shared_inbox='http://example2.com/inbox',
|
||||
bookwyrm_user=False, local=False)
|
||||
self.user.followers.add(non_bw_follower)
|
||||
|
||||
models.User.objects.create_user(
|
||||
'nutria', 'nutria@mouse.mouse', 'nuword',
|
||||
remote_id='http://example.com/u/4',
|
||||
outbox='http://example.com/u/4/o',
|
||||
shared_inbox='http://example.com/inbox',
|
||||
inbox='http://example.com/u/4/inbox',
|
||||
local=False)
|
||||
|
||||
|
||||
def test_get_public_recipients(self):
|
||||
expected = [
|
||||
'http://example2.com/inbox',
|
||||
'http://example.com/inbox',
|
||||
'http://example.com/u/2/inbox',
|
||||
]
|
||||
|
||||
recipients = broadcast.get_public_recipients(self.user)
|
||||
self.assertEqual(recipients, expected)
|
||||
|
||||
|
||||
def test_get_public_recipients_software(self):
|
||||
expected = [
|
||||
'http://example.com/inbox',
|
||||
'http://example.com/u/2/inbox',
|
||||
]
|
||||
|
||||
recipients = broadcast.get_public_recipients(self.user, software='bookwyrm')
|
||||
self.assertEqual(recipients, expected)
|
||||
|
||||
|
||||
def test_get_public_recipients_software_other(self):
|
||||
expected = [
|
||||
'http://example2.com/inbox',
|
||||
]
|
||||
|
||||
recipients = broadcast.get_public_recipients(self.user, software='mastodon')
|
||||
self.assertEqual(recipients, expected)
|
|
@ -124,7 +124,7 @@ class GoodreadsImport(TestCase):
|
|||
job_id=import_job.id, index=index, data=entry, book=self.book)
|
||||
break
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
goodreads_import.handle_imported_book(
|
||||
self.user, import_item, False, 'public')
|
||||
|
||||
|
@ -144,9 +144,10 @@ class GoodreadsImport(TestCase):
|
|||
|
||||
def test_handle_imported_book_already_shelved(self):
|
||||
''' goodreads import added a book, this adds related connections '''
|
||||
shelf = self.user.shelf_set.filter(identifier='to-read').first()
|
||||
models.ShelfBook.objects.create(
|
||||
shelf=shelf, added_by=self.user, book=self.book)
|
||||
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)
|
||||
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
||||
|
@ -156,7 +157,7 @@ class GoodreadsImport(TestCase):
|
|||
job_id=import_job.id, index=index, data=entry, book=self.book)
|
||||
break
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
goodreads_import.handle_imported_book(
|
||||
self.user, import_item, False, 'public')
|
||||
|
||||
|
@ -185,7 +186,7 @@ class GoodreadsImport(TestCase):
|
|||
job_id=import_job.id, index=index, data=entry, book=self.book)
|
||||
break
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
goodreads_import.handle_imported_book(
|
||||
self.user, import_item, False, 'public')
|
||||
goodreads_import.handle_imported_book(
|
||||
|
@ -214,7 +215,7 @@ class GoodreadsImport(TestCase):
|
|||
import_item = models.ImportItem.objects.create(
|
||||
job_id=import_job.id, index=0, data=entry, book=self.book)
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
goodreads_import.handle_imported_book(
|
||||
self.user, import_item, True, 'unlisted')
|
||||
review = models.Review.objects.get(book=self.book, user=self.user)
|
||||
|
@ -235,7 +236,7 @@ class GoodreadsImport(TestCase):
|
|||
import_item = models.ImportItem.objects.create(
|
||||
job_id=import_job.id, index=0, data=entry, book=self.book)
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
goodreads_import.handle_imported_book(
|
||||
self.user, import_item, False, 'unlisted')
|
||||
self.assertFalse(models.Review.objects.filter(
|
||||
|
|
|
@ -22,7 +22,7 @@ class Incoming(TestCase):
|
|||
'mouse@example.com', 'mouse@mouse.com', 'mouseword',
|
||||
local=True, localname='mouse')
|
||||
self.local_user.remote_id = 'https://example.com/user/mouse'
|
||||
self.local_user.save()
|
||||
self.local_user.save(broadcast=False)
|
||||
with patch('bookwyrm.models.user.set_remote_server.delay'):
|
||||
self.remote_user = models.User.objects.create_user(
|
||||
'rat', 'rat@rat.com', 'ratword',
|
||||
|
@ -31,11 +31,12 @@ class Incoming(TestCase):
|
|||
inbox='https://example.com/users/rat/inbox',
|
||||
outbox='https://example.com/users/rat/outbox',
|
||||
)
|
||||
self.status = models.Status.objects.create(
|
||||
user=self.local_user,
|
||||
content='Test status',
|
||||
remote_id='https://example.com/status/1',
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
self.status = models.Status.objects.create(
|
||||
user=self.local_user,
|
||||
content='Test status',
|
||||
remote_id='https://example.com/status/1',
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
|
||||
|
@ -117,7 +118,7 @@ class Incoming(TestCase):
|
|||
"object": "https://example.com/user/mouse"
|
||||
}
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
incoming.handle_follow(activity)
|
||||
|
||||
# notification created
|
||||
|
@ -145,9 +146,9 @@ class Incoming(TestCase):
|
|||
}
|
||||
|
||||
self.local_user.manually_approves_followers = True
|
||||
self.local_user.save()
|
||||
self.local_user.save(broadcast=False)
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
incoming.handle_follow(activity)
|
||||
|
||||
# notification created
|
||||
|
@ -177,8 +178,9 @@ class Incoming(TestCase):
|
|||
"object": "https://example.com/user/mouse"
|
||||
}
|
||||
}
|
||||
models.UserFollows.objects.create(
|
||||
user_subject=self.remote_user, user_object=self.local_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.UserFollows.objects.create(
|
||||
user_subject=self.remote_user, user_object=self.local_user)
|
||||
self.assertEqual(self.remote_user, self.local_user.followers.first())
|
||||
|
||||
incoming.handle_unfollow(activity)
|
||||
|
@ -200,10 +202,11 @@ class Incoming(TestCase):
|
|||
}
|
||||
}
|
||||
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user
|
||||
)
|
||||
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
|
||||
|
||||
incoming.handle_follow_accept(activity)
|
||||
|
@ -232,10 +235,11 @@ class Incoming(TestCase):
|
|||
}
|
||||
}
|
||||
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user
|
||||
)
|
||||
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
|
||||
|
||||
incoming.handle_follow_reject(activity)
|
||||
|
@ -280,9 +284,10 @@ class Incoming(TestCase):
|
|||
|
||||
def test_handle_update_list(self):
|
||||
''' a new list '''
|
||||
book_list = models.List.objects.create(
|
||||
name='hi', remote_id='https://example.com/list/22',
|
||||
user=self.local_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
book_list = models.List.objects.create(
|
||||
name='hi', remote_id='https://example.com/list/22',
|
||||
user=self.local_user)
|
||||
activity = {
|
||||
'object': {
|
||||
"id": "https://example.com/list/22",
|
||||
|
@ -387,11 +392,14 @@ class Incoming(TestCase):
|
|||
|
||||
def test_handle_delete_status(self):
|
||||
''' remove a status '''
|
||||
self.status.user = self.remote_user
|
||||
self.status.save(broadcast=False)
|
||||
|
||||
self.assertFalse(self.status.deleted)
|
||||
activity = {
|
||||
'type': 'Delete',
|
||||
'id': '%s/activity' % self.status.remote_id,
|
||||
'actor': self.local_user.remote_id,
|
||||
'actor': self.remote_user.remote_id,
|
||||
'object': {'id': self.status.remote_id},
|
||||
}
|
||||
incoming.handle_delete_status(activity)
|
||||
|
@ -403,6 +411,8 @@ class Incoming(TestCase):
|
|||
|
||||
def test_handle_delete_status_notifications(self):
|
||||
''' remove a status with related notifications '''
|
||||
self.status.user = self.remote_user
|
||||
self.status.save(broadcast=False)
|
||||
models.Notification.objects.create(
|
||||
related_status=self.status,
|
||||
user=self.local_user,
|
||||
|
@ -418,7 +428,7 @@ class Incoming(TestCase):
|
|||
activity = {
|
||||
'type': 'Delete',
|
||||
'id': '%s/activity' % self.status.remote_id,
|
||||
'actor': self.local_user.remote_id,
|
||||
'actor': self.remote_user.remote_id,
|
||||
'object': {'id': self.status.remote_id},
|
||||
}
|
||||
incoming.handle_delete_status(activity)
|
||||
|
@ -510,7 +520,6 @@ class Incoming(TestCase):
|
|||
self.assertEqual(models.Boost.objects.count(), 0)
|
||||
|
||||
|
||||
|
||||
def test_handle_unboost(self):
|
||||
''' undo a boost '''
|
||||
activity = {
|
||||
|
@ -539,7 +548,7 @@ class Incoming(TestCase):
|
|||
activity = {
|
||||
"id": "https://bookwyrm.social/shelfbook/6189#add",
|
||||
"type": "Add",
|
||||
"actor": "hhttps://example.com/users/rat",
|
||||
"actor": "https://example.com/users/rat",
|
||||
"object": "https://bookwyrm.social/book/37292",
|
||||
"target": "https://bookwyrm.social/user/mouse/shelf/to-read",
|
||||
"@context": "https://www.w3.org/ns/activitystreams"
|
||||
|
@ -608,9 +617,10 @@ class Incoming(TestCase):
|
|||
def test_handle_blocks(self):
|
||||
''' create a "block" database entry from an activity '''
|
||||
self.local_user.followers.add(self.remote_user)
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user)
|
||||
self.assertTrue(models.UserFollows.objects.exists())
|
||||
self.assertTrue(models.UserFollowRequest.objects.exists())
|
||||
|
||||
|
|
|
@ -35,8 +35,9 @@ class TemplateTags(TestCase):
|
|||
|
||||
def test_get_user_rating(self):
|
||||
''' get a user's most recent rating of a book '''
|
||||
models.Review.objects.create(
|
||||
user=self.user, book=self.book, rating=3)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.Review.objects.create(
|
||||
user=self.user, book=self.book, rating=3)
|
||||
self.assertEqual(
|
||||
bookwyrm_tags.get_user_rating(self.book, self.user), 3)
|
||||
|
||||
|
@ -64,9 +65,9 @@ class TemplateTags(TestCase):
|
|||
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0)
|
||||
|
||||
models.Notification.objects.create(
|
||||
user=self.user, notification_type='FOLLOW')
|
||||
user=self.user, notification_type='FAVORITE')
|
||||
models.Notification.objects.create(
|
||||
user=self.user, notification_type='FOLLOW')
|
||||
user=self.user, notification_type='MENTION')
|
||||
|
||||
models.Notification.objects.create(
|
||||
user=self.remote_user, notification_type='FOLLOW')
|
||||
|
@ -76,14 +77,16 @@ class TemplateTags(TestCase):
|
|||
|
||||
def test_get_replies(self):
|
||||
''' direct replies to a status '''
|
||||
parent = models.Review.objects.create(
|
||||
user=self.user, book=self.book)
|
||||
first_child = models.Status.objects.create(
|
||||
reply_parent=parent, user=self.user)
|
||||
second_child = models.Status.objects.create(
|
||||
reply_parent=parent, user=self.user)
|
||||
third_child = models.Status.objects.create(
|
||||
reply_parent=parent, user=self.user, deleted=True)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
parent = models.Review.objects.create(
|
||||
user=self.user, book=self.book, content='hi')
|
||||
first_child = models.Status.objects.create(
|
||||
reply_parent=parent, user=self.user, content='hi')
|
||||
second_child = models.Status.objects.create(
|
||||
reply_parent=parent, user=self.user, content='hi')
|
||||
third_child = models.Status.objects.create(
|
||||
reply_parent=parent, user=self.user,
|
||||
deleted=True, deleted_date=timezone.now())
|
||||
|
||||
replies = bookwyrm_tags.get_replies(parent)
|
||||
self.assertEqual(len(replies), 2)
|
||||
|
@ -94,10 +97,11 @@ class TemplateTags(TestCase):
|
|||
|
||||
def test_get_parent(self):
|
||||
''' get the reply parent of a status '''
|
||||
parent = models.Review.objects.create(
|
||||
user=self.user, book=self.book)
|
||||
child = models.Status.objects.create(
|
||||
reply_parent=parent, user=self.user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
parent = models.Review.objects.create(
|
||||
user=self.user, book=self.book, content='hi')
|
||||
child = models.Status.objects.create(
|
||||
reply_parent=parent, user=self.user, content='hi')
|
||||
|
||||
result = bookwyrm_tags.get_parent(child)
|
||||
self.assertEqual(result, parent)
|
||||
|
@ -110,10 +114,11 @@ class TemplateTags(TestCase):
|
|||
user=self.remote_user, book=self.book)
|
||||
|
||||
self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status))
|
||||
models.Favorite.objects.create(
|
||||
user=self.user,
|
||||
status=status
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.Favorite.objects.create(
|
||||
user=self.user,
|
||||
status=status
|
||||
)
|
||||
self.assertTrue(bookwyrm_tags.get_user_liked(self.user, status))
|
||||
|
||||
|
||||
|
@ -123,10 +128,11 @@ class TemplateTags(TestCase):
|
|||
user=self.remote_user, book=self.book)
|
||||
|
||||
self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status))
|
||||
models.Boost.objects.create(
|
||||
user=self.user,
|
||||
boosted_status=status
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.Boost.objects.create(
|
||||
user=self.user,
|
||||
boosted_status=status
|
||||
)
|
||||
self.assertTrue(bookwyrm_tags.get_user_boosted(self.user, status))
|
||||
|
||||
|
||||
|
@ -135,9 +141,10 @@ class TemplateTags(TestCase):
|
|||
self.assertFalse(
|
||||
bookwyrm_tags.follow_request_exists(self.user, self.remote_user))
|
||||
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=self.user,
|
||||
user_object=self.remote_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=self.user,
|
||||
user_object=self.remote_user)
|
||||
|
||||
self.assertFalse(
|
||||
bookwyrm_tags.follow_request_exists(self.user, self.remote_user))
|
||||
|
@ -147,12 +154,13 @@ class TemplateTags(TestCase):
|
|||
|
||||
def test_get_boosted(self):
|
||||
''' load a boosted status '''
|
||||
status = models.Review.objects.create(
|
||||
user=self.remote_user, book=self.book)
|
||||
boost = models.Boost.objects.create(
|
||||
user=self.user,
|
||||
boosted_status=status
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Review.objects.create(
|
||||
user=self.remote_user, book=self.book)
|
||||
boost = models.Boost.objects.create(
|
||||
user=self.user,
|
||||
boosted_status=status
|
||||
)
|
||||
boosted = bookwyrm_tags.get_boosted(boost)
|
||||
self.assertIsInstance(boosted, models.Review)
|
||||
self.assertEqual(boosted, status)
|
||||
|
@ -233,29 +241,31 @@ class TemplateTags(TestCase):
|
|||
|
||||
def test_get_status_preview_name(self):
|
||||
''' status context string '''
|
||||
status = models.Status.objects.create(content='hi', user=self.user)
|
||||
result = bookwyrm_tags.get_status_preview_name(status)
|
||||
self.assertEqual(result, 'status')
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(content='hi', user=self.user)
|
||||
result = bookwyrm_tags.get_status_preview_name(status)
|
||||
self.assertEqual(result, 'status')
|
||||
|
||||
status = models.Review.objects.create(
|
||||
content='hi', user=self.user, book=self.book)
|
||||
result = bookwyrm_tags.get_status_preview_name(status)
|
||||
self.assertEqual(result, 'review of <em>Test Book</em>')
|
||||
status = models.Review.objects.create(
|
||||
content='hi', user=self.user, book=self.book)
|
||||
result = bookwyrm_tags.get_status_preview_name(status)
|
||||
self.assertEqual(result, 'review of <em>Test Book</em>')
|
||||
|
||||
status = models.Comment.objects.create(
|
||||
content='hi', user=self.user, book=self.book)
|
||||
result = bookwyrm_tags.get_status_preview_name(status)
|
||||
self.assertEqual(result, 'comment on <em>Test Book</em>')
|
||||
status = models.Comment.objects.create(
|
||||
content='hi', user=self.user, book=self.book)
|
||||
result = bookwyrm_tags.get_status_preview_name(status)
|
||||
self.assertEqual(result, 'comment on <em>Test Book</em>')
|
||||
|
||||
status = models.Quotation.objects.create(
|
||||
content='hi', user=self.user, book=self.book)
|
||||
status = models.Quotation.objects.create(
|
||||
content='hi', user=self.user, book=self.book)
|
||||
result = bookwyrm_tags.get_status_preview_name(status)
|
||||
self.assertEqual(result, 'quotation from <em>Test Book</em>')
|
||||
|
||||
|
||||
def test_related_status(self):
|
||||
''' gets the subclass model for a notification status '''
|
||||
status = models.Status.objects.create(content='hi', user=self.user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(content='hi', user=self.user)
|
||||
notification = models.Notification.objects.create(
|
||||
user=self.user, notification_type='MENTION',
|
||||
related_status=status)
|
||||
|
|
|
@ -84,7 +84,7 @@ class AuthorViews(TestCase):
|
|||
request = self.factory.post('', form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, author.id)
|
||||
author.refresh_from_db()
|
||||
self.assertEqual(author.name, 'New Name')
|
||||
|
|
|
@ -40,15 +40,16 @@ class BlockViews(TestCase):
|
|||
''' create a "block" database entry from an activity '''
|
||||
view = views.Block.as_view()
|
||||
self.local_user.followers.add(self.remote_user)
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=self.local_user,
|
||||
user_object=self.remote_user)
|
||||
self.assertTrue(models.UserFollows.objects.exists())
|
||||
self.assertTrue(models.UserFollowRequest.objects.exists())
|
||||
|
||||
request = self.factory.post('')
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, self.remote_user.id)
|
||||
block = models.UserBlocks.objects.get()
|
||||
self.assertEqual(block.user_subject, self.local_user)
|
||||
|
@ -63,7 +64,7 @@ class BlockViews(TestCase):
|
|||
request = self.factory.post('')
|
||||
request.user = self.local_user
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.block.unblock(request, self.remote_user.id)
|
||||
|
||||
self.assertFalse(models.UserBlocks.objects.exists())
|
||||
|
|
|
@ -77,7 +77,7 @@ class BookViews(TestCase):
|
|||
form.data['last_edited_by'] = self.local_user.id
|
||||
request = self.factory.post('', form.data)
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, self.book.id)
|
||||
self.book.refresh_from_db()
|
||||
self.assertEqual(self.book.title, 'New Title')
|
||||
|
@ -90,9 +90,14 @@ class BookViews(TestCase):
|
|||
title='first ed', parent_work=work)
|
||||
edition2 = models.Edition.objects.create(
|
||||
title='second ed', parent_work=work)
|
||||
shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf', user=self.local_user)
|
||||
shelf.books.add(edition1)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf', user=self.local_user)
|
||||
models.ShelfBook.objects.create(
|
||||
book=edition1,
|
||||
user=self.local_user,
|
||||
shelf=shelf,
|
||||
)
|
||||
models.ReadThrough.objects.create(
|
||||
user=self.local_user, book=edition1)
|
||||
|
||||
|
@ -102,7 +107,7 @@ class BookViews(TestCase):
|
|||
'edition': edition2.id
|
||||
})
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.switch_edition(request)
|
||||
|
||||
self.assertEqual(models.ShelfBook.objects.get().book, edition2)
|
||||
|
|
|
@ -18,6 +18,7 @@ class FeedMessageViews(TestCase):
|
|||
'mouse@local.com', 'mouse@mouse.mouse', 'password',
|
||||
local=True, localname='mouse')
|
||||
self.book = models.Edition.objects.create(
|
||||
parent_work=models.Work.objects.create(title='hi'),
|
||||
title='Example Edition',
|
||||
remote_id='https://example.com/book/1',
|
||||
)
|
||||
|
@ -38,8 +39,9 @@ class FeedMessageViews(TestCase):
|
|||
def test_status_page(self):
|
||||
''' there are so many views, this just makes sure it LOADS '''
|
||||
view = views.Status.as_view()
|
||||
status = models.Status.objects.create(
|
||||
content='hi', user=self.local_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
content='hi', user=self.local_user)
|
||||
request = self.factory.get('')
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.views.feed.is_api_request') as is_api:
|
||||
|
@ -59,8 +61,9 @@ class FeedMessageViews(TestCase):
|
|||
def test_replies_page(self):
|
||||
''' there are so many views, this just makes sure it LOADS '''
|
||||
view = views.Replies.as_view()
|
||||
status = models.Status.objects.create(
|
||||
content='hi', user=self.local_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
content='hi', user=self.local_user)
|
||||
request = self.factory.get('')
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.views.feed.is_api_request') as is_api:
|
||||
|
@ -90,11 +93,12 @@ class FeedMessageViews(TestCase):
|
|||
|
||||
def test_get_suggested_book(self):
|
||||
''' gets books the ~*~ algorithm ~*~ thinks you want to post about '''
|
||||
models.ShelfBook.objects.create(
|
||||
book=self.book,
|
||||
added_by=self.local_user,
|
||||
shelf=self.local_user.shelf_set.get(identifier='reading')
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.ShelfBook.objects.create(
|
||||
book=self.book,
|
||||
user=self.local_user,
|
||||
shelf=self.local_user.shelf_set.get(identifier='reading')
|
||||
)
|
||||
suggestions = views.feed.get_suggested_books(self.local_user)
|
||||
self.assertEqual(suggestions[0]['name'], 'Currently Reading')
|
||||
self.assertEqual(suggestions[0]['books'][0], self.book)
|
||||
|
|
|
@ -46,7 +46,7 @@ class BookViews(TestCase):
|
|||
request.user = self.local_user
|
||||
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.follow(request)
|
||||
|
||||
rel = models.UserFollowRequest.objects.get()
|
||||
|
@ -62,7 +62,7 @@ class BookViews(TestCase):
|
|||
request.user = self.local_user
|
||||
self.remote_user.followers.add(self.local_user)
|
||||
self.assertEqual(self.remote_user.followers.count(), 1)
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.unfollow(request)
|
||||
|
||||
self.assertEqual(self.remote_user.followers.count(), 0)
|
||||
|
@ -77,7 +77,7 @@ class BookViews(TestCase):
|
|||
user_object=self.local_user
|
||||
)
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.accept_follow_request(request)
|
||||
# request should be deleted
|
||||
self.assertEqual(
|
||||
|
@ -96,7 +96,7 @@ class BookViews(TestCase):
|
|||
user_object=self.local_user
|
||||
)
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.delete_follow_request(request)
|
||||
# request should be deleted
|
||||
self.assertEqual(
|
||||
|
|
|
@ -100,7 +100,7 @@ class GoalViews(TestCase):
|
|||
'post-status': True
|
||||
})
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, self.local_user.localname, 2020)
|
||||
|
||||
goal = models.AnnualGoal.objects.get()
|
||||
|
|
|
@ -38,11 +38,12 @@ class ViewsHelpers(TestCase):
|
|||
)
|
||||
self.userdata = json.loads(datafile.read_bytes())
|
||||
del self.userdata['icon']
|
||||
self.shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf',
|
||||
identifier='test-shelf',
|
||||
user=self.local_user
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
self.shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf',
|
||||
identifier='test-shelf',
|
||||
user=self.local_user
|
||||
)
|
||||
|
||||
|
||||
def test_get_edition(self):
|
||||
|
@ -83,22 +84,23 @@ class ViewsHelpers(TestCase):
|
|||
rat = models.User.objects.create_user(
|
||||
'rat', 'rat@rat.rat', 'password', local=True)
|
||||
|
||||
public_status = models.Comment.objects.create(
|
||||
content='public status', book=self.book, user=self.local_user)
|
||||
direct_status = models.Status.objects.create(
|
||||
content='direct', user=self.local_user, privacy='direct')
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
public_status = models.Comment.objects.create(
|
||||
content='public status', book=self.book, user=self.local_user)
|
||||
direct_status = models.Status.objects.create(
|
||||
content='direct', user=self.local_user, privacy='direct')
|
||||
|
||||
rat_public = models.Status.objects.create(
|
||||
content='blah blah', user=rat)
|
||||
rat_unlisted = models.Status.objects.create(
|
||||
content='blah blah', user=rat, privacy='unlisted')
|
||||
remote_status = models.Status.objects.create(
|
||||
content='blah blah', user=self.remote_user)
|
||||
followers_status = models.Status.objects.create(
|
||||
content='blah', user=rat, privacy='followers')
|
||||
rat_mention = models.Status.objects.create(
|
||||
content='blah blah blah', user=rat, privacy='followers')
|
||||
rat_mention.mention_users.set([self.local_user])
|
||||
rat_public = models.Status.objects.create(
|
||||
content='blah blah', user=rat)
|
||||
rat_unlisted = models.Status.objects.create(
|
||||
content='blah blah', user=rat, privacy='unlisted')
|
||||
remote_status = models.Status.objects.create(
|
||||
content='blah blah', user=self.remote_user)
|
||||
followers_status = models.Status.objects.create(
|
||||
content='blah', user=rat, privacy='followers')
|
||||
rat_mention = models.Status.objects.create(
|
||||
content='blah blah blah', user=rat, privacy='followers')
|
||||
rat_mention.mention_users.set([self.local_user])
|
||||
|
||||
statuses = views.helpers.get_activity_feed(
|
||||
self.local_user,
|
||||
|
@ -159,14 +161,15 @@ class ViewsHelpers(TestCase):
|
|||
rat = models.User.objects.create_user(
|
||||
'rat', 'rat@rat.rat', 'password', local=True)
|
||||
|
||||
public_status = models.Comment.objects.create(
|
||||
content='public status', book=self.book, user=self.local_user)
|
||||
rat_public = models.Status.objects.create(
|
||||
content='blah blah', user=rat)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
public_status = models.Comment.objects.create(
|
||||
content='public status', book=self.book, user=self.local_user)
|
||||
rat_public = models.Status.objects.create(
|
||||
content='blah blah', user=rat)
|
||||
|
||||
statuses = views.helpers.get_activity_feed(
|
||||
self.local_user, ['public'])
|
||||
self.assertEqual(len(statuses), 2)
|
||||
statuses = views.helpers.get_activity_feed(
|
||||
self.local_user, ['public'])
|
||||
self.assertEqual(len(statuses), 2)
|
||||
|
||||
# block relationship
|
||||
rat.blocks.add(self.local_user)
|
||||
|
@ -240,7 +243,7 @@ class ViewsHelpers(TestCase):
|
|||
def test_handle_reading_status_to_read(self):
|
||||
''' posts shelve activities '''
|
||||
shelf = self.local_user.shelf_set.get(identifier='to-read')
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.helpers.handle_reading_status(
|
||||
self.local_user, shelf, self.book, 'public')
|
||||
status = models.GeneratedNote.objects.get()
|
||||
|
@ -251,7 +254,7 @@ class ViewsHelpers(TestCase):
|
|||
def test_handle_reading_status_reading(self):
|
||||
''' posts shelve activities '''
|
||||
shelf = self.local_user.shelf_set.get(identifier='reading')
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.helpers.handle_reading_status(
|
||||
self.local_user, shelf, self.book, 'public')
|
||||
status = models.GeneratedNote.objects.get()
|
||||
|
@ -262,7 +265,7 @@ class ViewsHelpers(TestCase):
|
|||
def test_handle_reading_status_read(self):
|
||||
''' posts shelve activities '''
|
||||
shelf = self.local_user.shelf_set.get(identifier='read')
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.helpers.handle_reading_status(
|
||||
self.local_user, shelf, self.book, 'public')
|
||||
status = models.GeneratedNote.objects.get()
|
||||
|
@ -272,7 +275,7 @@ class ViewsHelpers(TestCase):
|
|||
|
||||
def test_handle_reading_status_other(self):
|
||||
''' posts shelve activities '''
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.helpers.handle_reading_status(
|
||||
self.local_user, self.shelf, self.book, 'public')
|
||||
self.assertFalse(models.GeneratedNote.objects.exists())
|
||||
|
|
|
@ -38,10 +38,10 @@ class InteractionViews(TestCase):
|
|||
view = views.Favorite.as_view()
|
||||
request = self.factory.post('')
|
||||
request.user = self.remote_user
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
view(request, status.id)
|
||||
fav = models.Favorite.objects.get()
|
||||
self.assertEqual(fav.status, status)
|
||||
|
@ -58,15 +58,15 @@ class InteractionViews(TestCase):
|
|||
view = views.Unfavorite.as_view()
|
||||
request = self.factory.post('')
|
||||
request.user = self.remote_user
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
views.Favorite.as_view()(request, status.id)
|
||||
|
||||
self.assertEqual(models.Favorite.objects.count(), 1)
|
||||
self.assertEqual(models.Notification.objects.count(), 1)
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, status.id)
|
||||
self.assertEqual(models.Favorite.objects.count(), 0)
|
||||
self.assertEqual(models.Notification.objects.count(), 0)
|
||||
|
@ -77,10 +77,10 @@ class InteractionViews(TestCase):
|
|||
view = views.Boost.as_view()
|
||||
request = self.factory.post('')
|
||||
request.user = self.remote_user
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
view(request, status.id)
|
||||
|
||||
boost = models.Boost.objects.get()
|
||||
|
@ -99,10 +99,10 @@ class InteractionViews(TestCase):
|
|||
view = views.Boost.as_view()
|
||||
request = self.factory.post('')
|
||||
request.user = self.local_user
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi', privacy='unlisted')
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi', privacy='unlisted')
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
view(request, status.id)
|
||||
|
||||
boost = models.Boost.objects.get()
|
||||
|
@ -113,10 +113,10 @@ class InteractionViews(TestCase):
|
|||
view = views.Boost.as_view()
|
||||
request = self.factory.post('')
|
||||
request.user = self.local_user
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi', privacy='followers')
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi', privacy='followers')
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
view(request, status.id)
|
||||
self.assertFalse(models.Boost.objects.exists())
|
||||
|
||||
|
@ -125,10 +125,10 @@ class InteractionViews(TestCase):
|
|||
view = views.Boost.as_view()
|
||||
request = self.factory.post('')
|
||||
request.user = self.local_user
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
view(request, status.id)
|
||||
view(request, status.id)
|
||||
self.assertEqual(models.Boost.objects.count(), 1)
|
||||
|
@ -139,14 +139,14 @@ class InteractionViews(TestCase):
|
|||
view = views.Unboost.as_view()
|
||||
request = self.factory.post('')
|
||||
request.user = self.remote_user
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
views.Boost.as_view()(request, status.id)
|
||||
|
||||
self.assertEqual(models.Boost.objects.count(), 1)
|
||||
self.assertEqual(models.Notification.objects.count(), 1)
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, status.id)
|
||||
self.assertEqual(models.Boost.objects.count(), 0)
|
||||
self.assertEqual(models.Notification.objects.count(), 0)
|
||||
|
|
|
@ -10,7 +10,6 @@ from bookwyrm import models, views
|
|||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
|
||||
|
||||
@patch('bookwyrm.broadcast.broadcast_task.delay')
|
||||
class ListViews(TestCase):
|
||||
''' tag views'''
|
||||
def setUp(self):
|
||||
|
@ -32,19 +31,21 @@ class ListViews(TestCase):
|
|||
remote_id='https://example.com/book/1',
|
||||
parent_work=work,
|
||||
)
|
||||
self.list = models.List.objects.create(
|
||||
name='Test List', user=self.local_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
self.list = models.List.objects.create(
|
||||
name='Test List', user=self.local_user)
|
||||
self.anonymous_user = AnonymousUser
|
||||
self.anonymous_user.is_authenticated = False
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
|
||||
def test_lists_page(self, _):
|
||||
def test_lists_page(self):
|
||||
''' there are so many views, this just makes sure it LOADS '''
|
||||
view = views.Lists.as_view()
|
||||
models.List.objects.create(name='Public list', user=self.local_user)
|
||||
models.List.objects.create(
|
||||
name='Private list', privacy='private', user=self.local_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.List.objects.create(name='Public list', user=self.local_user)
|
||||
models.List.objects.create(
|
||||
name='Private list', privacy='private', user=self.local_user)
|
||||
request = self.factory.get('')
|
||||
request.user = self.local_user
|
||||
|
||||
|
@ -61,8 +62,16 @@ class ListViews(TestCase):
|
|||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
|
||||
def test_lists_create(self, _):
|
||||
def test_lists_create(self):
|
||||
''' create list view '''
|
||||
real_broadcast = models.List.broadcast
|
||||
def mock_broadcast(_, activity, user, **kwargs):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Create')
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
models.List.broadcast = mock_broadcast
|
||||
|
||||
view = views.Lists.as_view()
|
||||
request = self.factory.post('', {
|
||||
'name': 'A list',
|
||||
|
@ -78,9 +87,10 @@ class ListViews(TestCase):
|
|||
self.assertEqual(new_list.description, 'wow')
|
||||
self.assertEqual(new_list.privacy, 'unlisted')
|
||||
self.assertEqual(new_list.curation, 'open')
|
||||
models.List.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_list_page(self, _):
|
||||
def test_list_page(self):
|
||||
''' there are so many views, this just makes sure it LOADS '''
|
||||
view = views.List.as_view()
|
||||
request = self.factory.get('')
|
||||
|
@ -116,8 +126,17 @@ class ListViews(TestCase):
|
|||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
|
||||
def test_list_edit(self, _):
|
||||
def test_list_edit(self):
|
||||
''' edit a list '''
|
||||
real_broadcast = models.List.broadcast
|
||||
def mock_broadcast(_, activity, user, **kwargs):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Update')
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['object']['id'], self.list.remote_id)
|
||||
models.List.broadcast = mock_broadcast
|
||||
|
||||
view = views.List.as_view()
|
||||
request = self.factory.post('', {
|
||||
'name': 'New Name',
|
||||
|
@ -136,14 +155,16 @@ class ListViews(TestCase):
|
|||
self.assertEqual(self.list.description, 'wow')
|
||||
self.assertEqual(self.list.privacy, 'direct')
|
||||
self.assertEqual(self.list.curation, 'curated')
|
||||
models.List.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_curate_page(self, _):
|
||||
def test_curate_page(self):
|
||||
''' there are so many views, this just makes sure it LOADS '''
|
||||
view = views.Curate.as_view()
|
||||
models.List.objects.create(name='Public list', user=self.local_user)
|
||||
models.List.objects.create(
|
||||
name='Private list', privacy='private', user=self.local_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.List.objects.create(name='Public list', user=self.local_user)
|
||||
models.List.objects.create(
|
||||
name='Private list', privacy='private', user=self.local_user)
|
||||
request = self.factory.get('')
|
||||
request.user = self.local_user
|
||||
|
||||
|
@ -157,15 +178,25 @@ class ListViews(TestCase):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
|
||||
def test_curate_approve(self, _):
|
||||
def test_curate_approve(self):
|
||||
''' approve a pending item '''
|
||||
real_broadcast = models.List.broadcast
|
||||
def mock_broadcast(_, activity, user, **kwargs):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Add')
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['target'], self.list.remote_id)
|
||||
models.ListItem.broadcast = mock_broadcast
|
||||
|
||||
view = views.Curate.as_view()
|
||||
pending = models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
added_by=self.local_user,
|
||||
book=self.book,
|
||||
approved=False
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
pending = models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
approved=False
|
||||
)
|
||||
|
||||
request = self.factory.post('', {
|
||||
'item': pending.id,
|
||||
|
@ -178,17 +209,19 @@ class ListViews(TestCase):
|
|||
self.assertEqual(self.list.books.count(), 1)
|
||||
self.assertEqual(self.list.listitem_set.first(), pending)
|
||||
self.assertTrue(pending.approved)
|
||||
models.ListItem.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_curate_reject(self, _):
|
||||
def test_curate_reject(self):
|
||||
''' approve a pending item '''
|
||||
view = views.Curate.as_view()
|
||||
pending = models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
added_by=self.local_user,
|
||||
book=self.book,
|
||||
approved=False
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
pending = models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
approved=False
|
||||
)
|
||||
|
||||
request = self.factory.post('', {
|
||||
'item': pending.id,
|
||||
|
@ -196,13 +229,22 @@ class ListViews(TestCase):
|
|||
})
|
||||
request.user = self.local_user
|
||||
|
||||
view(request, self.list.id)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, self.list.id)
|
||||
self.assertFalse(self.list.books.exists())
|
||||
self.assertFalse(models.ListItem.objects.exists())
|
||||
|
||||
|
||||
def test_add_book(self, _):
|
||||
def test_add_book(self):
|
||||
''' put a book on a list '''
|
||||
real_broadcast = models.List.broadcast
|
||||
def mock_broadcast(_, activity, user):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Add')
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['target'], self.list.remote_id)
|
||||
models.ListItem.broadcast = mock_broadcast
|
||||
request = self.factory.post('', {
|
||||
'book': self.book.id,
|
||||
})
|
||||
|
@ -211,14 +253,23 @@ class ListViews(TestCase):
|
|||
views.list.add_book(request, self.list.id)
|
||||
item = self.list.listitem_set.get()
|
||||
self.assertEqual(item.book, self.book)
|
||||
self.assertEqual(item.added_by, self.local_user)
|
||||
self.assertEqual(item.user, self.local_user)
|
||||
self.assertTrue(item.approved)
|
||||
models.ListItem.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_add_book_outsider(self, _):
|
||||
def test_add_book_outsider(self):
|
||||
''' put a book on a list '''
|
||||
real_broadcast = models.List.broadcast
|
||||
def mock_broadcast(_, activity, user):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.rat.remote_id)
|
||||
self.assertEqual(activity['type'], 'Add')
|
||||
self.assertEqual(activity['actor'], self.rat.remote_id)
|
||||
self.assertEqual(activity['target'], self.list.remote_id)
|
||||
models.ListItem.broadcast = mock_broadcast
|
||||
self.list.curation = 'open'
|
||||
self.list.save()
|
||||
self.list.save(broadcast=False)
|
||||
request = self.factory.post('', {
|
||||
'book': self.book.id,
|
||||
})
|
||||
|
@ -227,14 +278,24 @@ class ListViews(TestCase):
|
|||
views.list.add_book(request, self.list.id)
|
||||
item = self.list.listitem_set.get()
|
||||
self.assertEqual(item.book, self.book)
|
||||
self.assertEqual(item.added_by, self.rat)
|
||||
self.assertEqual(item.user, self.rat)
|
||||
self.assertTrue(item.approved)
|
||||
models.ListItem.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_add_book_pending(self, _):
|
||||
''' put a book on a list '''
|
||||
def test_add_book_pending(self):
|
||||
''' put a book on a list awaiting approval '''
|
||||
real_broadcast = models.List.broadcast
|
||||
def mock_broadcast(_, activity, user):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.rat.remote_id)
|
||||
self.assertEqual(activity['type'], 'Add')
|
||||
self.assertEqual(activity['actor'], self.rat.remote_id)
|
||||
self.assertEqual(activity['target'], self.list.remote_id)
|
||||
self.assertEqual(activity['object']['id'], self.book.remote_id)
|
||||
models.ListItem.broadcast = mock_broadcast
|
||||
self.list.curation = 'curated'
|
||||
self.list.save()
|
||||
self.list.save(broadcast=False)
|
||||
request = self.factory.post('', {
|
||||
'book': self.book.id,
|
||||
})
|
||||
|
@ -243,14 +304,24 @@ class ListViews(TestCase):
|
|||
views.list.add_book(request, self.list.id)
|
||||
item = self.list.listitem_set.get()
|
||||
self.assertEqual(item.book, self.book)
|
||||
self.assertEqual(item.added_by, self.rat)
|
||||
self.assertEqual(item.user, self.rat)
|
||||
self.assertFalse(item.approved)
|
||||
models.ListItem.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_add_book_self_curated(self, _):
|
||||
''' put a book on a list '''
|
||||
def test_add_book_self_curated(self):
|
||||
''' put a book on a list automatically approved '''
|
||||
real_broadcast = models.ListItem.broadcast
|
||||
def mock_broadcast(_, activity, user):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Add')
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['target'], self.list.remote_id)
|
||||
models.ListItem.broadcast = mock_broadcast
|
||||
|
||||
self.list.curation = 'curated'
|
||||
self.list.save()
|
||||
self.list.save(broadcast=False)
|
||||
request = self.factory.post('', {
|
||||
'book': self.book.id,
|
||||
})
|
||||
|
@ -259,18 +330,30 @@ class ListViews(TestCase):
|
|||
views.list.add_book(request, self.list.id)
|
||||
item = self.list.listitem_set.get()
|
||||
self.assertEqual(item.book, self.book)
|
||||
self.assertEqual(item.added_by, self.local_user)
|
||||
self.assertEqual(item.user, self.local_user)
|
||||
self.assertTrue(item.approved)
|
||||
models.ListItem.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_remove_book(self, _):
|
||||
def test_remove_book(self):
|
||||
''' take an item off a list '''
|
||||
item = models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
added_by=self.local_user,
|
||||
book=self.book,
|
||||
)
|
||||
real_broadcast = models.ListItem.broadcast
|
||||
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
item = models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
)
|
||||
self.assertTrue(self.list.listitem_set.exists())
|
||||
|
||||
def mock_broadcast(_, activity, user):
|
||||
''' ok '''
|
||||
self.assertEqual(user.remote_id, self.local_user.remote_id)
|
||||
self.assertEqual(activity['type'], 'Remove')
|
||||
self.assertEqual(activity['actor'], self.local_user.remote_id)
|
||||
self.assertEqual(activity['target'], self.list.remote_id)
|
||||
models.ListItem.broadcast = mock_broadcast
|
||||
request = self.factory.post('', {
|
||||
'item': item.id,
|
||||
})
|
||||
|
@ -279,15 +362,17 @@ class ListViews(TestCase):
|
|||
views.list.remove_book(request, self.list.id)
|
||||
|
||||
self.assertFalse(self.list.listitem_set.exists())
|
||||
models.ListItem.broadcast = real_broadcast
|
||||
|
||||
|
||||
def test_remove_book_unauthorized(self, _):
|
||||
def test_remove_book_unauthorized(self):
|
||||
''' take an item off a list '''
|
||||
item = models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
added_by=self.local_user,
|
||||
book=self.book,
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
item = models.ListItem.objects.create(
|
||||
book_list=self.list,
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
)
|
||||
self.assertTrue(self.list.listitem_set.exists())
|
||||
request = self.factory.post('', {
|
||||
'item': item.id,
|
||||
|
|
|
@ -30,7 +30,7 @@ class NotificationViews(TestCase):
|
|||
def test_clear_notifications(self):
|
||||
''' erase notifications '''
|
||||
models.Notification.objects.create(
|
||||
user=self.local_user, notification_type='MENTION')
|
||||
user=self.local_user, notification_type='FAVORITE')
|
||||
models.Notification.objects.create(
|
||||
user=self.local_user, notification_type='MENTION', read=True)
|
||||
self.assertEqual(models.Notification.objects.count(), 2)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
''' sending out activities '''
|
||||
from unittest.mock import patch
|
||||
import json
|
||||
|
||||
from django.http import JsonResponse
|
||||
|
@ -49,14 +50,16 @@ class OutboxView(TestCase):
|
|||
|
||||
def test_outbox_privacy(self):
|
||||
''' don't show dms et cetera in outbox '''
|
||||
models.Status.objects.create(
|
||||
content='PRIVATE!!', user=self.local_user, privacy='direct')
|
||||
models.Status.objects.create(
|
||||
content='bffs ONLY', user=self.local_user, privacy='followers')
|
||||
models.Status.objects.create(
|
||||
content='unlisted status', user=self.local_user, privacy='unlisted')
|
||||
models.Status.objects.create(
|
||||
content='look at this', user=self.local_user, privacy='public')
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.Status.objects.create(
|
||||
content='PRIVATE!!', user=self.local_user, privacy='direct')
|
||||
models.Status.objects.create(
|
||||
content='bffs ONLY', user=self.local_user, privacy='followers')
|
||||
models.Status.objects.create(
|
||||
content='unlisted status', user=self.local_user,
|
||||
privacy='unlisted')
|
||||
models.Status.objects.create(
|
||||
content='look at this', user=self.local_user, privacy='public')
|
||||
|
||||
request = self.factory.get('')
|
||||
result = views.Outbox.as_view()(request, 'mouse')
|
||||
|
@ -67,11 +70,12 @@ class OutboxView(TestCase):
|
|||
|
||||
def test_outbox_filter(self):
|
||||
''' if we only care about reviews, only get reviews '''
|
||||
models.Review.objects.create(
|
||||
content='look at this', name='hi', rating=1,
|
||||
book=self.book, user=self.local_user)
|
||||
models.Status.objects.create(
|
||||
content='look at this', user=self.local_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.Review.objects.create(
|
||||
content='look at this', name='hi', rating=1,
|
||||
book=self.book, user=self.local_user)
|
||||
models.Status.objects.create(
|
||||
content='look at this', user=self.local_user)
|
||||
|
||||
request = self.factory.get('', {'type': 'bleh'})
|
||||
result = views.Outbox.as_view()(request, 'mouse')
|
||||
|
|
|
@ -45,7 +45,7 @@ class ReadingViews(TestCase):
|
|||
'start_date': '2020-01-05',
|
||||
})
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.start_reading(request, self.book.id)
|
||||
|
||||
self.assertEqual(shelf.books.get(), self.book)
|
||||
|
@ -65,8 +65,9 @@ class ReadingViews(TestCase):
|
|||
def test_start_reading_reshelf(self):
|
||||
''' begin a book '''
|
||||
to_read_shelf = self.local_user.shelf_set.get(identifier='to-read')
|
||||
models.ShelfBook.objects.create(
|
||||
shelf=to_read_shelf, book=self.book, added_by=self.local_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.ShelfBook.objects.create(
|
||||
shelf=to_read_shelf, book=self.book, user=self.local_user)
|
||||
shelf = self.local_user.shelf_set.get(identifier='reading')
|
||||
self.assertEqual(to_read_shelf.books.get(), self.book)
|
||||
self.assertFalse(shelf.books.exists())
|
||||
|
@ -74,7 +75,7 @@ class ReadingViews(TestCase):
|
|||
|
||||
request = self.factory.post('')
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.start_reading(request, self.book.id)
|
||||
|
||||
self.assertFalse(to_read_shelf.books.exists())
|
||||
|
@ -98,7 +99,7 @@ class ReadingViews(TestCase):
|
|||
})
|
||||
request.user = self.local_user
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.finish_reading(request, self.book.id)
|
||||
|
||||
self.assertEqual(shelf.books.get(), self.book)
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
''' tests updating reading progress '''
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
from django.test import TestCase, Client
|
||||
from django.utils import timezone
|
||||
from datetime import datetime
|
||||
|
||||
from bookwyrm import models
|
||||
|
||||
@patch('bookwyrm.broadcast.broadcast_task.delay')
|
||||
@patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay')
|
||||
class ReadThrough(TestCase):
|
||||
''' readthrough tests '''
|
||||
def setUp(self):
|
||||
''' basic user and book data '''
|
||||
self.client = Client()
|
||||
|
||||
self.work = models.Work.objects.create(
|
||||
|
@ -25,7 +28,8 @@ class ReadThrough(TestCase):
|
|||
'cinco', 'cinco@example.com', 'seissiete',
|
||||
local=True, localname='cinco')
|
||||
|
||||
self.client.force_login(self.user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_create_basic_readthrough(self, delay_mock):
|
||||
"""A basic readthrough doesn't create a progress update"""
|
||||
|
@ -38,13 +42,15 @@ class ReadThrough(TestCase):
|
|||
readthroughs = self.edition.readthrough_set.all()
|
||||
self.assertEqual(len(readthroughs), 1)
|
||||
self.assertEqual(readthroughs[0].progressupdate_set.count(), 0)
|
||||
self.assertEqual(readthroughs[0].start_date,
|
||||
self.assertEqual(
|
||||
readthroughs[0].start_date,
|
||||
datetime(2020, 11, 27, tzinfo=timezone.utc))
|
||||
self.assertEqual(readthroughs[0].progress, None)
|
||||
self.assertEqual(readthroughs[0].finish_date, None)
|
||||
self.assertEqual(delay_mock.call_count, 1)
|
||||
|
||||
def test_create_progress_readthrough(self, delay_mock):
|
||||
''' a readthrough with progress '''
|
||||
self.assertEqual(self.edition.readthrough_set.count(), 0)
|
||||
|
||||
self.client.post('/start-reading/{}'.format(self.edition.id), {
|
||||
|
@ -54,7 +60,8 @@ class ReadThrough(TestCase):
|
|||
|
||||
readthroughs = self.edition.readthrough_set.all()
|
||||
self.assertEqual(len(readthroughs), 1)
|
||||
self.assertEqual(readthroughs[0].start_date,
|
||||
self.assertEqual(
|
||||
readthroughs[0].start_date,
|
||||
datetime(2020, 11, 27, tzinfo=timezone.utc))
|
||||
self.assertEqual(readthroughs[0].progress, 50)
|
||||
self.assertEqual(readthroughs[0].finish_date, None)
|
||||
|
@ -76,7 +83,9 @@ class ReadThrough(TestCase):
|
|||
self.assertEqual(len(progress_updates), 2)
|
||||
self.assertEqual(progress_updates[1].mode, models.ProgressMode.PAGE)
|
||||
self.assertEqual(progress_updates[1].progress, 100)
|
||||
self.assertEqual(delay_mock.call_count, 1) # Edit doesn't publish anything
|
||||
|
||||
# Edit doesn't publish anything
|
||||
self.assertEqual(delay_mock.call_count, 1)
|
||||
|
||||
self.client.post('/delete-readthrough', {
|
||||
'id': readthroughs[0].id,
|
|
@ -2,16 +2,14 @@
|
|||
|
||||
from unittest.mock import patch
|
||||
from django.test import RequestFactory, TestCase
|
||||
import responses
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.views import rss_feed
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
class RssFeedView(TestCase):
|
||||
''' rss feed behaves as expected '''
|
||||
def setUp(self):
|
||||
|
||||
''' test data '''
|
||||
self.site = models.SiteSettings.objects.create()
|
||||
|
||||
self.user = models.User.objects.create_user(
|
||||
|
@ -24,21 +22,23 @@ class RssFeedView(TestCase):
|
|||
parent_work=work
|
||||
)
|
||||
|
||||
self.review = models.Review.objects.create(
|
||||
name='Review name', content='test content', rating=3,
|
||||
user=self.user, book=self.book)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
self.review = models.Review.objects.create(
|
||||
name='Review name', content='test content', rating=3,
|
||||
user=self.user, book=self.book)
|
||||
|
||||
self.quote = models.Quotation.objects.create(
|
||||
quote='a sickening sense', content='test content',
|
||||
user=self.user, book=self.book)
|
||||
|
||||
self.generatednote = models.GeneratedNote.objects.create(
|
||||
content='test content', user=self.user)
|
||||
self.quote = models.Quotation.objects.create(
|
||||
quote='a sickening sense', content='test content',
|
||||
user=self.user, book=self.book)
|
||||
|
||||
self.generatednote = models.GeneratedNote.objects.create(
|
||||
content='test content', user=self.user)
|
||||
|
||||
self.factory = RequestFactory()
|
||||
|
||||
|
||||
|
||||
def test_rss_feed(self):
|
||||
''' load an rss feed '''
|
||||
view = rss_feed.RssFeed()
|
||||
request = self.factory.get('/user/rss_user/rss')
|
||||
with patch("bookwyrm.models.SiteSettings.objects.get") as site:
|
||||
|
@ -47,6 +47,5 @@ class RssFeedView(TestCase):
|
|||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
self.assertIn(b"Status updates from rss_user", result.content)
|
||||
self.assertIn( b"a sickening sense", result.content)
|
||||
self.assertIn(b"a sickening sense", result.content)
|
||||
self.assertIn(b"Example Edition", result.content)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from bookwyrm import models, views
|
|||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
|
||||
|
||||
@patch('bookwyrm.broadcast.broadcast_task.delay')
|
||||
@patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay')
|
||||
class ShelfViews(TestCase):
|
||||
''' tag views'''
|
||||
def setUp(self):
|
||||
|
@ -25,11 +25,12 @@ class ShelfViews(TestCase):
|
|||
remote_id='https://example.com/book/1',
|
||||
parent_work=self.work
|
||||
)
|
||||
self.shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf',
|
||||
identifier='test-shelf',
|
||||
user=self.local_user
|
||||
)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
self.shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf',
|
||||
identifier='test-shelf',
|
||||
user=self.local_user
|
||||
)
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
|
||||
|
@ -96,7 +97,8 @@ class ShelfViews(TestCase):
|
|||
'name': 'cool name'
|
||||
})
|
||||
request.user = self.local_user
|
||||
view(request, request.user.username, shelf.identifier)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, request.user.username, shelf.identifier)
|
||||
shelf.refresh_from_db()
|
||||
|
||||
self.assertEqual(shelf.name, 'cool name')
|
||||
|
@ -116,7 +118,8 @@ class ShelfViews(TestCase):
|
|||
'name': 'cool name'
|
||||
})
|
||||
request.user = self.local_user
|
||||
view(request, request.user.username, shelf.identifier)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, request.user.username, shelf.identifier)
|
||||
|
||||
self.assertEqual(shelf.name, 'To Read')
|
||||
|
||||
|
@ -128,7 +131,8 @@ class ShelfViews(TestCase):
|
|||
'shelf': self.shelf.identifier
|
||||
})
|
||||
request.user = self.local_user
|
||||
views.shelve(request)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.shelve(request)
|
||||
# make sure the book is on the shelf
|
||||
self.assertEqual(self.shelf.books.get(), self.book)
|
||||
|
||||
|
@ -142,7 +146,8 @@ class ShelfViews(TestCase):
|
|||
})
|
||||
request.user = self.local_user
|
||||
|
||||
views.shelve(request)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.shelve(request)
|
||||
# make sure the book is on the shelf
|
||||
self.assertEqual(shelf.books.get(), self.book)
|
||||
|
||||
|
@ -156,7 +161,8 @@ class ShelfViews(TestCase):
|
|||
})
|
||||
request.user = self.local_user
|
||||
|
||||
views.shelve(request)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.shelve(request)
|
||||
# make sure the book is on the shelf
|
||||
self.assertEqual(shelf.books.get(), self.book)
|
||||
|
||||
|
@ -170,7 +176,7 @@ class ShelfViews(TestCase):
|
|||
})
|
||||
request.user = self.local_user
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.shelve(request)
|
||||
# make sure the book is on the shelf
|
||||
self.assertEqual(shelf.books.get(), self.book)
|
||||
|
@ -178,7 +184,12 @@ class ShelfViews(TestCase):
|
|||
|
||||
def test_handle_unshelve(self, _):
|
||||
''' remove a book from a shelf '''
|
||||
self.shelf.books.add(self.book)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
models.ShelfBook.objects.create(
|
||||
book=self.book,
|
||||
user=self.local_user,
|
||||
shelf=self.shelf
|
||||
)
|
||||
self.shelf.save()
|
||||
self.assertEqual(self.shelf.books.count(), 1)
|
||||
request = self.factory.post('', {
|
||||
|
@ -186,6 +197,6 @@ class ShelfViews(TestCase):
|
|||
'shelf': self.shelf.id
|
||||
})
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
views.unshelve(request)
|
||||
self.assertEqual(self.shelf.books.count(), 0)
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
''' test for app action functionality '''
|
||||
from unittest.mock import patch
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
|
||||
|
@ -47,7 +45,7 @@ class StatusViews(TestCase):
|
|||
})
|
||||
request = self.factory.post('', form.data)
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, 'comment')
|
||||
status = models.Comment.objects.get()
|
||||
self.assertEqual(status.content, '<p>hi</p>')
|
||||
|
@ -59,8 +57,9 @@ class StatusViews(TestCase):
|
|||
view = views.CreateStatus.as_view()
|
||||
user = models.User.objects.create_user(
|
||||
'rat', 'rat@rat.com', 'password', local=True)
|
||||
parent = models.Status.objects.create(
|
||||
content='parent status', user=self.local_user)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
parent = models.Status.objects.create(
|
||||
content='parent status', user=self.local_user)
|
||||
form = forms.ReplyForm({
|
||||
'content': 'hi',
|
||||
'user': user.id,
|
||||
|
@ -69,7 +68,7 @@ class StatusViews(TestCase):
|
|||
})
|
||||
request = self.factory.post('', form.data)
|
||||
request.user = user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, 'reply')
|
||||
status = models.Status.objects.get(user=user)
|
||||
self.assertEqual(status.content, '<p>hi</p>')
|
||||
|
@ -92,7 +91,7 @@ class StatusViews(TestCase):
|
|||
request = self.factory.post('', form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, 'comment')
|
||||
status = models.Status.objects.get()
|
||||
self.assertEqual(list(status.mention_users.all()), [user])
|
||||
|
@ -116,7 +115,7 @@ class StatusViews(TestCase):
|
|||
request = self.factory.post('', form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, 'comment')
|
||||
status = models.Status.objects.get()
|
||||
|
||||
|
@ -128,7 +127,7 @@ class StatusViews(TestCase):
|
|||
})
|
||||
request = self.factory.post('', form.data)
|
||||
request.user = user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, 'reply')
|
||||
|
||||
reply = models.Status.replies(status).first()
|
||||
|
@ -218,15 +217,26 @@ class StatusViews(TestCase):
|
|||
'is rad</p>')
|
||||
|
||||
|
||||
def test_to_markdown_link(self):
|
||||
''' this is mostly handled in other places, but nonetheless '''
|
||||
text = '[hi](http://fish.com) is <marquee>rad</marquee>'
|
||||
result = views.status.to_markdown(text)
|
||||
self.assertEqual(
|
||||
result,
|
||||
'<p><a href="http://fish.com">hi</a> ' \
|
||||
'is rad</p>')
|
||||
|
||||
|
||||
def test_handle_delete_status(self):
|
||||
''' marks a status as deleted '''
|
||||
view = views.DeleteStatus.as_view()
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
self.assertFalse(status.deleted)
|
||||
request = self.factory.post('')
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request, status.id)
|
||||
status.refresh_from_db()
|
||||
self.assertTrue(status.deleted)
|
||||
|
|
|
@ -39,9 +39,10 @@ class TagViews(TestCase):
|
|||
def test_tag_page(self):
|
||||
''' there are so many views, this just makes sure it LOADS '''
|
||||
view = views.Tag.as_view()
|
||||
tag = models.Tag.objects.create(name='hi there')
|
||||
models.UserTag.objects.create(
|
||||
tag=tag, user=self.local_user, book=self.book)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
tag = models.Tag.objects.create(name='hi there')
|
||||
models.UserTag.objects.create(
|
||||
tag=tag, user=self.local_user, book=self.book)
|
||||
request = self.factory.get('')
|
||||
with patch('bookwyrm.views.tag.is_api_request') as is_api:
|
||||
is_api.return_value = False
|
||||
|
@ -68,7 +69,7 @@ class TagViews(TestCase):
|
|||
})
|
||||
request.user = self.local_user
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request)
|
||||
|
||||
tag = models.Tag.objects.get()
|
||||
|
@ -82,9 +83,10 @@ class TagViews(TestCase):
|
|||
def test_untag(self):
|
||||
''' remove a tag from a book '''
|
||||
view = views.RemoveTag.as_view()
|
||||
tag = models.Tag.objects.create(name='A Tag!?')
|
||||
models.UserTag.objects.create(
|
||||
user=self.local_user, book=self.book, tag=tag)
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
tag = models.Tag.objects.create(name='A Tag!?')
|
||||
models.UserTag.objects.create(
|
||||
user=self.local_user, book=self.book, tag=tag)
|
||||
request = self.factory.post(
|
||||
'', {
|
||||
'user': self.local_user.id,
|
||||
|
@ -93,7 +95,7 @@ class TagViews(TestCase):
|
|||
})
|
||||
request.user = self.local_user
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request)
|
||||
|
||||
self.assertTrue(models.Tag.objects.filter(name='A Tag!?').exists())
|
||||
|
|
|
@ -138,7 +138,7 @@ class UserViews(TestCase):
|
|||
request = self.factory.post('', form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
|
||||
view(request)
|
||||
self.assertEqual(self.local_user.name, 'New Name')
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from .error import not_found_page, server_error_page
|
|||
from .federation import Federation
|
||||
from .feed import DirectMessage, Feed, Replies, Status
|
||||
from .follow import follow, unfollow
|
||||
from .follow import accept_follow_request, delete_follow_request, handle_accept
|
||||
from .follow import accept_follow_request, delete_follow_request
|
||||
from .goal import Goal
|
||||
from .import_data import Import, ImportStatus
|
||||
from .interaction import Favorite, Unfavorite, Boost, Unboost
|
||||
|
|
|
@ -46,6 +46,7 @@ class Login(View):
|
|||
# successful login
|
||||
login(request, user)
|
||||
user.last_active_date = timezone.now()
|
||||
user.save(broadcast=False)
|
||||
return redirect(request.GET.get('next', '/'))
|
||||
|
||||
# login errors
|
||||
|
|
|
@ -8,7 +8,6 @@ from django.views import View
|
|||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from .helpers import is_api_request
|
||||
|
||||
|
||||
|
@ -62,5 +61,4 @@ class EditAuthor(View):
|
|||
return TemplateResponse(request, 'edit_author.html', data)
|
||||
author = form.save()
|
||||
|
||||
broadcast(request.user, author.to_update_activity(request.user))
|
||||
return redirect('/author/%s' % author.id)
|
||||
|
|
|
@ -8,7 +8,6 @@ from django.views import View
|
|||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.broadcast import broadcast
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
|
@ -22,15 +21,8 @@ class Block(View):
|
|||
def post(self, request, user_id):
|
||||
''' block a user '''
|
||||
to_block = get_object_or_404(models.User, id=user_id)
|
||||
block = models.UserBlocks.objects.create(
|
||||
models.UserBlocks.objects.create(
|
||||
user_subject=request.user, user_object=to_block)
|
||||
if not to_block.local:
|
||||
broadcast(
|
||||
request.user,
|
||||
block.to_activity(),
|
||||
privacy='direct',
|
||||
direct_recipients=[to_block]
|
||||
)
|
||||
return redirect('/preferences/block')
|
||||
|
||||
|
||||
|
@ -46,13 +38,5 @@ def unblock(request, user_id):
|
|||
)
|
||||
except models.UserBlocks.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if not to_unblock.local:
|
||||
broadcast(
|
||||
request.user,
|
||||
block.to_undo_activity(request.user),
|
||||
privacy='direct',
|
||||
direct_recipients=[to_unblock]
|
||||
)
|
||||
block.delete()
|
||||
return redirect('/preferences/block')
|
||||
|
|
|
@ -12,7 +12,6 @@ from django.views.decorators.http import require_POST
|
|||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from .helpers import is_api_request, get_activity_feed, get_edition
|
||||
|
@ -78,12 +77,12 @@ class Book(View):
|
|||
.order_by('-updated_date')
|
||||
|
||||
user_shelves = models.ShelfBook.objects.filter(
|
||||
added_by=request.user, book=book
|
||||
user=request.user, book=book
|
||||
)
|
||||
|
||||
other_edition_shelves = models.ShelfBook.objects.filter(
|
||||
~Q(book=book),
|
||||
added_by=request.user,
|
||||
user=request.user,
|
||||
book__parent_work=book.parent_work,
|
||||
)
|
||||
|
||||
|
@ -136,7 +135,6 @@ class EditBook(View):
|
|||
return TemplateResponse(request, 'edit_book.html', data)
|
||||
book = form.save()
|
||||
|
||||
broadcast(request.user, book.to_update_activity(request.user))
|
||||
return redirect('/book/%s' % book.id)
|
||||
|
||||
|
||||
|
@ -170,7 +168,6 @@ def upload_cover(request, book_id):
|
|||
book.cover = form.files['cover']
|
||||
book.save()
|
||||
|
||||
broadcast(request.user, book.to_update_activity(request.user))
|
||||
return redirect('/book/%s' % book.id)
|
||||
|
||||
|
||||
|
@ -189,7 +186,6 @@ def add_description(request, book_id):
|
|||
book.description = description
|
||||
book.save()
|
||||
|
||||
broadcast(request.user, book.to_update_activity(request.user))
|
||||
return redirect('/book/%s' % book.id)
|
||||
|
||||
|
||||
|
@ -215,12 +211,14 @@ def switch_edition(request):
|
|||
shelf__user=request.user
|
||||
)
|
||||
for shelfbook in shelfbooks.all():
|
||||
broadcast(request.user, shelfbook.to_remove_activity(request.user))
|
||||
|
||||
shelfbook.book = new_edition
|
||||
shelfbook.save()
|
||||
|
||||
broadcast(request.user, shelfbook.to_add_activity(request.user))
|
||||
with transaction.atomic():
|
||||
models.ShelfBook.objects.create(
|
||||
created_date=shelfbook.created_date,
|
||||
user=shelfbook.user,
|
||||
shelf=shelfbook.shelf,
|
||||
book=new_edition
|
||||
)
|
||||
shelfbook.delete()
|
||||
|
||||
readthroughs = models.ReadThrough.objects.filter(
|
||||
book__parent_work=new_edition.parent_work,
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
''' views for actions you can take in the application '''
|
||||
from django.db import transaction
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from .helpers import get_user_from_username
|
||||
|
||||
@login_required
|
||||
|
@ -19,13 +17,10 @@ def follow(request):
|
|||
except models.User.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
relationship, _ = models.UserFollowRequest.objects.get_or_create(
|
||||
models.UserFollowRequest.objects.get_or_create(
|
||||
user_subject=request.user,
|
||||
user_object=to_follow,
|
||||
)
|
||||
activity = relationship.to_activity()
|
||||
broadcast(
|
||||
request.user, activity, privacy='direct', direct_recipients=[to_follow])
|
||||
return redirect(to_follow.local_path)
|
||||
|
||||
|
||||
|
@ -39,14 +34,10 @@ def unfollow(request):
|
|||
except models.User.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
relationship = models.UserFollows.objects.get(
|
||||
models.UserFollows.objects.get(
|
||||
user_subject=request.user,
|
||||
user_object=to_unfollow
|
||||
)
|
||||
activity = relationship.to_undo_activity(request.user)
|
||||
broadcast(
|
||||
request.user, activity,
|
||||
privacy='direct', direct_recipients=[to_unfollow])
|
||||
|
||||
to_unfollow.followers.remove(request.user)
|
||||
return redirect(to_unfollow.local_path)
|
||||
|
@ -70,24 +61,11 @@ def accept_follow_request(request):
|
|||
except models.UserFollowRequest.DoesNotExist:
|
||||
# Request already dealt with.
|
||||
return redirect(request.user.local_path)
|
||||
handle_accept(follow_request)
|
||||
follow_request.accept()
|
||||
|
||||
return redirect(request.user.local_path)
|
||||
|
||||
|
||||
def handle_accept(follow_request):
|
||||
''' send an acceptance message to a follow request '''
|
||||
user = follow_request.user_subject
|
||||
to_follow = follow_request.user_object
|
||||
with transaction.atomic():
|
||||
relationship = models.UserFollows.from_request(follow_request)
|
||||
follow_request.delete()
|
||||
relationship.save()
|
||||
|
||||
activity = relationship.to_accept_activity()
|
||||
broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def delete_follow_request(request):
|
||||
|
@ -106,8 +84,5 @@ def delete_follow_request(request):
|
|||
except models.UserFollowRequest.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
activity = follow_request.to_reject_activity()
|
||||
follow_request.delete()
|
||||
broadcast(
|
||||
request.user, activity, privacy='direct', direct_recipients=[requester])
|
||||
return redirect('/user/%s' % request.user.localname)
|
||||
|
|
|
@ -7,7 +7,6 @@ from django.utils.decorators import method_decorator
|
|||
from django.views import View
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from bookwyrm.status import create_generated_note
|
||||
from .helpers import get_user_from_username, object_visible_to_user
|
||||
|
||||
|
@ -63,23 +62,10 @@ class Goal(View):
|
|||
|
||||
if request.POST.get('post-status'):
|
||||
# create status, if appropraite
|
||||
status = create_generated_note(
|
||||
create_generated_note(
|
||||
request.user,
|
||||
'set a goal to read %d books in %d' % (goal.goal, goal.year),
|
||||
privacy=goal.privacy
|
||||
)
|
||||
broadcast(
|
||||
request.user,
|
||||
status.to_create_activity(request.user),
|
||||
privacy=status.privacy,
|
||||
software='bookwyrm')
|
||||
|
||||
# re-format the activity for non-bookwyrm servers
|
||||
remote_activity = status.to_create_activity(request.user, pure=True)
|
||||
broadcast(
|
||||
request.user,
|
||||
remote_activity,
|
||||
privacy=status.privacy,
|
||||
software='other')
|
||||
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
|
|
@ -4,7 +4,6 @@ from requests import HTTPError
|
|||
from django.db.models import Q
|
||||
|
||||
from bookwyrm import activitypub, models
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from bookwyrm.connectors import ConnectorException, get_data
|
||||
from bookwyrm.status import create_generated_note
|
||||
from bookwyrm.utils import regex
|
||||
|
@ -199,7 +198,6 @@ def handle_reading_status(user, shelf, book, privacy):
|
|||
)
|
||||
status.save()
|
||||
|
||||
broadcast(user, status.to_create_activity(user))
|
||||
|
||||
def is_blocked(viewer, user):
|
||||
''' is this viewer blocked by the user? '''
|
||||
|
|
|
@ -7,8 +7,6 @@ from django.utils.decorators import method_decorator
|
|||
from django.views import View
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from bookwyrm.status import create_notification
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
|
@ -19,7 +17,7 @@ class Favorite(View):
|
|||
''' create a like '''
|
||||
status = models.Status.objects.get(id=status_id)
|
||||
try:
|
||||
favorite = models.Favorite.objects.create(
|
||||
models.Favorite.objects.create(
|
||||
status=status,
|
||||
user=request.user
|
||||
)
|
||||
|
@ -27,17 +25,6 @@ class Favorite(View):
|
|||
# you already fav'ed that
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
fav_activity = favorite.to_activity()
|
||||
broadcast(
|
||||
request.user, fav_activity, privacy='direct',
|
||||
direct_recipients=[status.user])
|
||||
if status.user.local:
|
||||
create_notification(
|
||||
status.user,
|
||||
'FAVORITE',
|
||||
related_user=request.user,
|
||||
related_status=status
|
||||
)
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
||||
|
||||
|
@ -56,18 +43,7 @@ class Unfavorite(View):
|
|||
# can't find that status, idk
|
||||
return HttpResponseNotFound()
|
||||
|
||||
fav_activity = favorite.to_undo_activity(request.user)
|
||||
favorite.delete()
|
||||
broadcast(request.user, fav_activity, direct_recipients=[status.user])
|
||||
|
||||
# check for notification
|
||||
if status.user.local:
|
||||
notification = models.Notification.objects.filter(
|
||||
user=status.user, related_user=request.user,
|
||||
related_status=status, notification_type='FAVORITE'
|
||||
).first()
|
||||
if notification:
|
||||
notification.delete()
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
||||
|
||||
|
@ -86,22 +62,11 @@ class Boost(View):
|
|||
# you already boosted that.
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
||||
boost = models.Boost.objects.create(
|
||||
models.Boost.objects.create(
|
||||
boosted_status=status,
|
||||
privacy=status.privacy,
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
boost_activity = boost.to_activity()
|
||||
broadcast(request.user, boost_activity)
|
||||
|
||||
if status.user.local:
|
||||
create_notification(
|
||||
status.user,
|
||||
'BOOST',
|
||||
related_user=request.user,
|
||||
related_status=status
|
||||
)
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
||||
|
||||
|
@ -114,17 +79,6 @@ class Unboost(View):
|
|||
boost = models.Boost.objects.filter(
|
||||
boosted_status=status, user=request.user
|
||||
).first()
|
||||
activity = boost.to_undo_activity(request.user)
|
||||
|
||||
boost.delete()
|
||||
broadcast(request.user, activity)
|
||||
|
||||
# delete related notification
|
||||
if status.user.local:
|
||||
notification = models.Notification.objects.filter(
|
||||
user=status.user, related_user=request.user,
|
||||
related_status=status, notification_type='BOOST'
|
||||
).first()
|
||||
if notification:
|
||||
notification.delete()
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
''' book list views'''
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Count, Q
|
||||
from django.http import HttpResponseNotFound, HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -11,7 +12,6 @@ from django.views.decorators.http import require_POST
|
|||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from .helpers import is_api_request, object_visible_to_user, privacy_filter
|
||||
from .helpers import get_user_from_username
|
||||
|
@ -27,9 +27,13 @@ class Lists(View):
|
|||
page = 1
|
||||
|
||||
user = request.user if request.user.is_authenticated else None
|
||||
# hide lists with no approved books
|
||||
lists = models.List.objects.filter(
|
||||
~Q(user=user),
|
||||
books__isnull=False,
|
||||
).annotate(
|
||||
item_count=Count('listitem', filter=Q(listitem__approved=True))
|
||||
).filter(
|
||||
item_count__gt=0
|
||||
).distinct().all()
|
||||
lists = privacy_filter(request.user, lists, ['public', 'followers'])
|
||||
|
||||
|
@ -51,13 +55,6 @@ class Lists(View):
|
|||
return redirect('lists')
|
||||
book_list = form.save()
|
||||
|
||||
# let the world know
|
||||
broadcast(
|
||||
request.user,
|
||||
book_list.to_create_activity(request.user),
|
||||
privacy=book_list.privacy,
|
||||
software='bookwyrm'
|
||||
)
|
||||
return redirect(book_list.local_path)
|
||||
|
||||
class UserLists(View):
|
||||
|
@ -132,19 +129,12 @@ class List(View):
|
|||
@method_decorator(login_required, name='dispatch')
|
||||
# pylint: disable=unused-argument
|
||||
def post(self, request, list_id):
|
||||
''' edit a book_list '''
|
||||
''' edit a list '''
|
||||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
form = forms.ListForm(request.POST, instance=book_list)
|
||||
if not form.is_valid():
|
||||
return redirect('list', book_list.id)
|
||||
book_list = form.save()
|
||||
# let the world know
|
||||
broadcast(
|
||||
request.user,
|
||||
book_list.to_update_activity(request.user),
|
||||
privacy=book_list.privacy,
|
||||
software='bookwyrm'
|
||||
)
|
||||
return redirect(book_list.local_path)
|
||||
|
||||
|
||||
|
@ -178,13 +168,6 @@ class Curate(View):
|
|||
if approved:
|
||||
suggestion.approved = True
|
||||
suggestion.save()
|
||||
# let the world know
|
||||
broadcast(
|
||||
request.user,
|
||||
suggestion.to_add_activity(request.user),
|
||||
privacy=book_list.privacy,
|
||||
software='bookwyrm'
|
||||
)
|
||||
else:
|
||||
suggestion.delete()
|
||||
return redirect('list-curate', book_list.id)
|
||||
|
@ -199,31 +182,28 @@ def add_book(request, list_id):
|
|||
|
||||
book = get_object_or_404(models.Edition, id=request.POST.get('book'))
|
||||
# do you have permission to add to the list?
|
||||
if request.user == book_list.user or book_list.curation == 'open':
|
||||
# go ahead and add it
|
||||
item = models.ListItem.objects.create(
|
||||
book=book,
|
||||
book_list=book_list,
|
||||
added_by=request.user,
|
||||
)
|
||||
# let the world know
|
||||
broadcast(
|
||||
request.user,
|
||||
item.to_add_activity(request.user),
|
||||
privacy=book_list.privacy,
|
||||
software='bookwyrm'
|
||||
)
|
||||
elif book_list.curation == 'curated':
|
||||
# make a pending entry
|
||||
models.ListItem.objects.create(
|
||||
approved=False,
|
||||
book=book,
|
||||
book_list=book_list,
|
||||
added_by=request.user,
|
||||
)
|
||||
else:
|
||||
# you can't add to this list, what were you THINKING
|
||||
return HttpResponseBadRequest()
|
||||
try:
|
||||
if request.user == book_list.user or book_list.curation == 'open':
|
||||
# go ahead and add it
|
||||
models.ListItem.objects.create(
|
||||
book=book,
|
||||
book_list=book_list,
|
||||
user=request.user,
|
||||
)
|
||||
elif book_list.curation == 'curated':
|
||||
# make a pending entry
|
||||
models.ListItem.objects.create(
|
||||
approved=False,
|
||||
book=book,
|
||||
book_list=book_list,
|
||||
user=request.user,
|
||||
)
|
||||
else:
|
||||
# you can't add to this list, what were you THINKING
|
||||
return HttpResponseBadRequest()
|
||||
except IntegrityError:
|
||||
# if the book is already on the list, don't flip out
|
||||
pass
|
||||
|
||||
return redirect('list', list_id)
|
||||
|
||||
|
@ -234,16 +214,8 @@ def remove_book(request, list_id):
|
|||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
item = get_object_or_404(models.ListItem, id=request.POST.get('item'))
|
||||
|
||||
if not book_list.user == request.user and not item.added_by == request.user:
|
||||
if not book_list.user == request.user and not item.user == request.user:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
activity = item.to_remove_activity(request.user)
|
||||
item.delete()
|
||||
# let the world know
|
||||
broadcast(
|
||||
request.user,
|
||||
activity,
|
||||
privacy=book_list.privacy,
|
||||
software='bookwyrm'
|
||||
)
|
||||
return redirect('list', list_id)
|
||||
|
|
|
@ -79,7 +79,7 @@ class PasswordReset(View):
|
|||
return TemplateResponse(request, 'password_reset.html', data)
|
||||
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
user.save(broadcast=False)
|
||||
login(request, user)
|
||||
reset_code.delete()
|
||||
return redirect('/')
|
||||
|
@ -106,6 +106,6 @@ class ChangePassword(View):
|
|||
return redirect('preferences/password')
|
||||
|
||||
request.user.set_password(new_password)
|
||||
request.user.save()
|
||||
request.user.save(broadcast=False)
|
||||
login(request, request.user)
|
||||
return redirect(request.user.local_path)
|
||||
|
|
|
@ -9,7 +9,6 @@ from django.utils import timezone
|
|||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from .helpers import get_edition, handle_reading_status
|
||||
from .shelf import handle_unshelve
|
||||
|
||||
|
@ -44,9 +43,8 @@ def start_reading(request, book_id):
|
|||
except models.Shelf.DoesNotExist:
|
||||
# this just means it isn't currently on the user's shelves
|
||||
pass
|
||||
shelfbook = models.ShelfBook.objects.create(
|
||||
book=book, shelf=shelf, added_by=request.user)
|
||||
broadcast(request.user, shelfbook.to_add_activity(request.user))
|
||||
models.ShelfBook.objects.create(
|
||||
book=book, shelf=shelf, user=request.user)
|
||||
|
||||
# post about it (if you want)
|
||||
if request.POST.get('post-status'):
|
||||
|
@ -82,9 +80,8 @@ def finish_reading(request, book_id):
|
|||
except models.Shelf.DoesNotExist:
|
||||
# this just means it isn't currently on the user's shelves
|
||||
pass
|
||||
shelfbook = models.ShelfBook.objects.create(
|
||||
book=book, shelf=shelf, added_by=request.user)
|
||||
broadcast(request.user, shelfbook.to_add_activity(request.user))
|
||||
models.ShelfBook.objects.create(
|
||||
book=book, shelf=shelf, user=request.user)
|
||||
|
||||
# post about it (if you want)
|
||||
if request.POST.get('post-status'):
|
||||
|
|
|
@ -9,7 +9,6 @@ from django.views.decorators.http import require_POST
|
|||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from .helpers import is_api_request, get_edition, get_user_from_username
|
||||
from .helpers import handle_reading_status
|
||||
|
||||
|
@ -50,7 +49,7 @@ class Shelf(View):
|
|||
return ActivitypubResponse(shelf.to_activity(**request.GET))
|
||||
|
||||
books = models.ShelfBook.objects.filter(
|
||||
added_by=user, shelf=shelf
|
||||
user=user, shelf=shelf
|
||||
).order_by('-updated_date').all()
|
||||
|
||||
data = {
|
||||
|
@ -125,6 +124,8 @@ def shelve(request):
|
|||
identifier=request.POST.get('shelf'),
|
||||
user=request.user
|
||||
).first()
|
||||
if not desired_shelf:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if request.POST.get('reshelve', True):
|
||||
try:
|
||||
|
@ -136,22 +137,18 @@ def shelve(request):
|
|||
except models.Shelf.DoesNotExist:
|
||||
# this just means it isn't currently on the user's shelves
|
||||
pass
|
||||
shelfbook = models.ShelfBook.objects.create(
|
||||
book=book, shelf=desired_shelf, added_by=request.user)
|
||||
broadcast(
|
||||
request.user,
|
||||
shelfbook.to_add_activity(request.user),
|
||||
privacy=shelfbook.shelf.privacy,
|
||||
software='bookwyrm'
|
||||
)
|
||||
models.ShelfBook.objects.create(
|
||||
book=book, shelf=desired_shelf, user=request.user)
|
||||
|
||||
# post about "want to read" shelves
|
||||
if desired_shelf.identifier == 'to-read':
|
||||
if desired_shelf.identifier == 'to-read' and \
|
||||
request.POST.get('post-status'):
|
||||
privacy = request.POST.get('privacy') or desired_shelf.privacy
|
||||
handle_reading_status(
|
||||
request.user,
|
||||
desired_shelf,
|
||||
book,
|
||||
privacy=desired_shelf.privacy,
|
||||
privacy=privacy
|
||||
)
|
||||
|
||||
return redirect('/')
|
||||
|
@ -168,10 +165,8 @@ def unshelve(request):
|
|||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
||||
|
||||
#pylint: disable=unused-argument
|
||||
def handle_unshelve(user, book, shelf):
|
||||
''' unshelve a book '''
|
||||
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
|
||||
activity = row.to_remove_activity(user)
|
||||
row.delete()
|
||||
|
||||
broadcast(user, activity, privacy=shelf.privacy, software='bookwyrm')
|
||||
|
|
|
@ -8,10 +8,9 @@ from django.views import View
|
|||
from markdown import markdown
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from bookwyrm.sanitize_html import InputHtmlParser
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.status import create_notification, delete_status
|
||||
from bookwyrm.status import delete_status
|
||||
from bookwyrm.utils import regex
|
||||
from .helpers import handle_remote_webfinger
|
||||
|
||||
|
@ -35,7 +34,7 @@ class CreateStatus(View):
|
|||
if not status.sensitive and status.content_warning:
|
||||
# the cw text field remains populated when you click "remove"
|
||||
status.content_warning = None
|
||||
status.save()
|
||||
status.save(broadcast=False)
|
||||
|
||||
# inspect the text for user tags
|
||||
content = status.content
|
||||
|
@ -49,32 +48,12 @@ class CreateStatus(View):
|
|||
r'<a href="%s">%s</a>\g<1>' % \
|
||||
(mention_user.remote_id, mention_text),
|
||||
content)
|
||||
|
||||
# add reply parent to mentions and notify
|
||||
# add reply parent to mentions
|
||||
if status.reply_parent:
|
||||
status.mention_users.add(status.reply_parent.user)
|
||||
|
||||
if status.reply_parent.user.local:
|
||||
create_notification(
|
||||
status.reply_parent.user,
|
||||
'REPLY',
|
||||
related_user=request.user,
|
||||
related_status=status
|
||||
)
|
||||
|
||||
# deduplicate mentions
|
||||
status.mention_users.set(set(status.mention_users.all()))
|
||||
# create mention notifications
|
||||
for mention_user in status.mention_users.all():
|
||||
if status.reply_parent and mention_user == status.reply_parent.user:
|
||||
continue
|
||||
if mention_user.local:
|
||||
create_notification(
|
||||
mention_user,
|
||||
'MENTION',
|
||||
related_user=request.user,
|
||||
related_status=status
|
||||
)
|
||||
|
||||
# don't apply formatting to generated notes
|
||||
if not isinstance(status, models.GeneratedNote):
|
||||
|
@ -83,16 +62,7 @@ class CreateStatus(View):
|
|||
if hasattr(status, 'quote'):
|
||||
status.quote = to_markdown(status.quote)
|
||||
|
||||
status.save()
|
||||
|
||||
broadcast(
|
||||
request.user,
|
||||
status.to_create_activity(request.user),
|
||||
software='bookwyrm')
|
||||
|
||||
# re-format the activity for non-bookwyrm servers
|
||||
remote_activity = status.to_create_activity(request.user, pure=True)
|
||||
broadcast(request.user, remote_activity, software='other')
|
||||
status.save(created=True)
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
||||
|
||||
|
@ -108,7 +78,6 @@ class DeleteStatus(View):
|
|||
|
||||
# perform deletion
|
||||
delete_status(status)
|
||||
broadcast(request.user, status.to_delete_activity(request.user))
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
||||
def find_mentions(content):
|
||||
|
@ -137,8 +106,8 @@ def format_links(content):
|
|||
|
||||
def to_markdown(content):
|
||||
''' catch links and convert to markdown '''
|
||||
content = format_links(content)
|
||||
content = markdown(content)
|
||||
content = format_links(content)
|
||||
# sanitize resulting html
|
||||
sanitizer = InputHtmlParser()
|
||||
sanitizer.feed(content)
|
||||
|
|
|
@ -8,7 +8,6 @@ from django.views import View
|
|||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from .helpers import is_api_request
|
||||
|
||||
|
||||
|
@ -45,17 +44,15 @@ class AddTag(View):
|
|||
name = request.POST.get('name')
|
||||
book_id = request.POST.get('book')
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
tag_obj, created = models.Tag.objects.get_or_create(
|
||||
tag_obj, _ = models.Tag.objects.get_or_create(
|
||||
name=name,
|
||||
)
|
||||
user_tag, _ = models.UserTag.objects.get_or_create(
|
||||
models.UserTag.objects.get_or_create(
|
||||
user=request.user,
|
||||
book=book,
|
||||
tag=tag_obj,
|
||||
)
|
||||
|
||||
if created:
|
||||
broadcast(request.user, user_tag.to_add_activity(request.user))
|
||||
return redirect('/book/%s' % book_id)
|
||||
|
||||
|
||||
|
@ -71,8 +68,6 @@ class RemoveTag(View):
|
|||
|
||||
user_tag = get_object_or_404(
|
||||
models.UserTag, tag=tag_obj, book=book, user=request.user)
|
||||
tag_activity = user_tag.to_remove_activity(request.user)
|
||||
user_tag.delete()
|
||||
|
||||
broadcast(request.user, tag_activity)
|
||||
return redirect('/book/%s' % book_id)
|
||||
|
|
|
@ -15,7 +15,6 @@ from django.views import View
|
|||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.broadcast import broadcast
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from .helpers import get_activity_feed, get_user_from_username, is_api_request
|
||||
from .helpers import is_blocked, object_visible_to_user
|
||||
|
@ -176,7 +175,6 @@ class EditUser(View):
|
|||
user.avatar.save(filename, image)
|
||||
user.save()
|
||||
|
||||
broadcast(user, user.to_update_activity(user))
|
||||
return redirect(user.local_path)
|
||||
|
||||
|
||||
|
|
4
instances.md
Normal file
4
instances.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
| name | url | admin contact | open registration |
|
||||
| :--- | :-- | :------------ | :---------------- |
|
||||
| bookwyrm.social | http://bookwyrm.social/ | mousereeve@riseup.net / @tripofmice@friend.camp | ❌ |
|
Loading…
Reference in a new issue