Merge pull request #165 from mouse-reeve/code-cleanup

Code cleanup and tests
This commit is contained in:
Mouse Reeve 2020-05-10 15:11:58 -07:00 committed by GitHub
commit bc5dba97ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 2270 additions and 94 deletions

View file

@ -1,6 +1,6 @@
''' bring activitypub functions into the namespace '''
from .actor import get_actor
from .book import get_book, get_author
from .book import get_book, get_author, get_shelf
from .create import get_create, get_update
from .follow import get_following, get_followers
from .follow import get_follow_request, get_unfollow, get_accept, get_reject

View file

@ -86,3 +86,42 @@ def get_author(author):
if hasattr(author, field):
activity[field] = author.__getattribute__(field)
return activity
def get_shelf(shelf, page=None):
''' serialize shelf object '''
id_slug = shelf.absolute_id
if page:
return get_shelf_page(shelf, page)
count = shelf.books.count()
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': id_slug,
'type': 'OrderedCollection',
'totalItems': count,
'first': '%s?page=1' % id_slug,
}
def get_shelf_page(shelf, page):
''' list of books on a shelf '''
page = int(page)
page_length = 10
start = (page - 1) * page_length
end = start + page_length
shelf_page = shelf.books.all()[start:end]
id_slug = shelf.absolute_id
data = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': '%s?page=%d' % (id_slug, page),
'type': 'OrderedCollectionPage',
'totalItems': shelf.books.count(),
'partOf': id_slug,
'orderedItems': [get_book(b) for b in shelf_page],
}
if end <= shelf.books.count():
# there are still more pages
data['next'] = '%s?page=%d' % (id_slug, page + 1)
if start > 0:
data['prev'] = '%s?page=%d' % (id_slug, page - 1)
return data

View file

@ -12,7 +12,7 @@ def get_remove(*args):
def get_add_remove(user, book, shelf, action='Add'):
''' format an Add or Remove json blob '''
''' format a shelve book json blob '''
uuid = uuid4()
return {
'@context': 'https://www.w3.org/ns/activitystreams',

View file

@ -19,10 +19,13 @@ def get_public_recipients(user, software=None):
# TODO: eventually we may want to handle particular software differently
followers = followers.filter(fedireads_user=(software == 'fedireads'))
# 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)
@ -33,7 +36,9 @@ def get_public_recipients(user, software=None):
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
# TODO: other kinds of privacy
if privacy == 'public':
recipients += get_public_recipients(sender, software=software)
@ -69,7 +74,9 @@ def sign_and_send(sender, activity, destination):
]
message_to_sign = '\n'.join(signature_headers)
# TODO: raise an error if the user doesn't have a private key
if not sender.private_key:
# this shouldn't happen. it would be bad if it happened.
raise ValueError('No private key found for sender')
signer = pkcs1_15.new(RSA.import_key(sender.private_key))
signed_message = signer.sign(SHA256.new(message_to_sign.encode('utf8')))

View file

@ -39,6 +39,7 @@ class AbstractConnector(ABC):
for field in fields:
setattr(self, field, getattr(info, field))
def is_available(self):
''' check if you're allowed to use this connector '''
if self.max_query_count is not None:
@ -266,25 +267,25 @@ def update_from_mappings(obj, data, mappings):
if key == 'id':
continue
if has_attr(obj, key):
try:
hasattr(obj, key)
except ValueError:
obj.__setattr__(key, formatter(value))
return obj
def has_attr(obj, key):
''' helper function to check if a model object has a key '''
try:
return hasattr(obj, key)
except ValueError:
return False
def get_date(date_string):
''' helper function to try to interpret dates '''
if not date_string:
return None
try:
return pytz.utc.localize(parser.parse(date_string))
except ValueError:
pass
try:
return parser.parse(date_string)
except ValueError:
return None

View file

@ -35,7 +35,7 @@ class Connector(AbstractConnector):
def is_work_data(self, data):
return not re.match(r'^OL\d+M$', data['key'])
return bool(re.match(r'^[\/\w]+OL\d+W$', data['key']))
def get_edition_from_work_data(self, data):
@ -86,14 +86,13 @@ class Connector(AbstractConnector):
def format_search_result(self, doc):
key = doc['key']
# build the absolute id from the openlibrary key
key = self.books_url + key
key = self.books_url + doc['key']
author = doc.get('author_name') or ['Unknown']
return SearchResult(
doc.get('title'),
key,
author[0],
', '.join(author),
doc.get('first_publish_year'),
)

View file

@ -101,7 +101,7 @@ class EditionForm(ModelForm):
'updated_date',
'last_sync_date',
'authors',
'authors',# TODO
'parent_work',
'shelves',
'misc_identifiers',

View file

@ -12,6 +12,7 @@ MAX_ENTRIES = 500
def create_job(user, csv_file):
''' check over a csv and creates a database entry for the job'''
job = ImportJob.objects.create(user=user)
for index, entry in enumerate(list(csv.DictReader(csv_file))[:MAX_ENTRIES]):
if not all(x in entry for x in ('ISBN13', 'Title', 'Author')):
@ -19,13 +20,17 @@ def create_job(user, csv_file):
ImportItem(job=job, index=index, data=entry).save()
return job
def start_import(job):
''' initalizes a csv import job '''
result = import_data.delay(job.id)
job.task_id = result.id
job.save()
@app.task
def import_data(job_id):
''' does the actual lookup work in a celery task '''
job = ImportJob.objects.get(id=job_id)
try:
results = []

View file

@ -42,6 +42,9 @@ def shared_inbox(request):
except json.decoder.JSONDecodeError:
return HttpResponseBadRequest()
if not activity.get('object'):
return HttpResponseBadRequest()
try:
verify_signature(request)
except ValueError:
@ -128,9 +131,13 @@ def verify_signature(request):
@app.task
def handle_follow(activity):
''' someone wants to follow a local user '''
# figure out who they want to follow
to_follow = models.User.objects.get(actor=activity['object'])
# figure out who they are
# figure out who they want to follow -- not using get_or_create because
# we only allow you to follow local users
try:
to_follow = models.User.objects.get(actor=activity['object'])
except models.User.DoesNotExist:
return False
# figure out who the actor is
user = get_or_create_remote_user(activity['actor'])
try:
request = models.UserFollowRequest.objects.create(
@ -165,14 +172,11 @@ def handle_follow(activity):
def handle_unfollow(activity):
''' unfollow a local user '''
obj = activity['object']
if not obj['type'] == 'Follow':
#idk how to undo other things
return HttpResponseNotFound()
try:
requester = get_or_create_remote_user(obj['actor'])
to_unfollow = models.User.objects.get(actor=obj['object'])
except models.User.DoesNotExist:
return HttpResponseNotFound()
return False
to_unfollow.followers.remove(requester)
@ -209,7 +213,7 @@ def handle_follow_reject(activity):
)
request.delete()
except models.UserFollowRequest.DoesNotExist:
pass
return False
@app.task
@ -217,46 +221,37 @@ def handle_create(activity):
''' someone did something, good on them '''
user = get_or_create_remote_user(activity['actor'])
if not 'object' in activity:
return False
if user.local:
# we really oughtn't even be sending in this case
return True
if activity['object'].get('fedireadsType') and \
'inReplyToBook' in activity['object']:
try:
if activity['object']['fedireadsType'] == 'Review':
builder = status_builder.create_review_from_activity
elif activity['object']['fedireadsType'] == 'Quotation':
builder = status_builder.create_quotation_from_activity
else:
builder = status_builder.create_comment_from_activity
if activity['object']['fedireadsType'] == 'Review':
builder = status_builder.create_review_from_activity
elif activity['object']['fedireadsType'] == 'Quotation':
builder = status_builder.create_quotation_from_activity
else:
builder = status_builder.create_comment_from_activity
# create the status, it'll throw a valueerror if anything is missing
builder(user, activity['object'])
except ValueError:
return False
# create the status, it'll throw a ValueError if anything is missing
builder(user, activity['object'])
elif activity['object'].get('inReplyTo'):
# only create the status if it's in reply to a status we already know
if not status_builder.get_status(activity['object']['inReplyTo']):
return True
try:
status = status_builder.create_status_from_activity(
user,
activity['object']
status = status_builder.create_status_from_activity(
user,
activity['object']
)
if status and status.reply_parent:
status_builder.create_notification(
status.reply_parent.user,
'REPLY',
related_user=status.user,
related_status=status,
)
if status and status.reply_parent:
status_builder.create_notification(
status.reply_parent.user,
'REPLY',
related_user=status.user,
related_status=status,
)
except ValueError:
return False
return True
@ -268,7 +263,7 @@ def handle_favorite(activity):
status = models.Status.objects.get(id=status_id)
liker = get_or_create_remote_user(activity['actor'])
except (models.Status.DoesNotExist, models.User.DoesNotExist):
return
return False
if not liker.local:
status_builder.create_favorite_from_activity(liker, activity)
@ -287,7 +282,7 @@ def handle_unfavorite(activity):
favorite_id = activity['object']['id']
fav = status_builder.get_favorite(favorite_id)
if not fav:
return HttpResponseNotFound()
return False
fav.delete()
@ -300,7 +295,7 @@ def handle_boost(activity):
status = models.Status.objects.get(id=status_id)
booster = get_or_create_remote_user(activity['actor'])
except (models.Status.DoesNotExist, models.User.DoesNotExist):
return HttpResponseNotFound()
return False
if not booster.local:
status_builder.create_boost_from_activity(booster, activity)
@ -315,10 +310,10 @@ def handle_boost(activity):
@app.task
def handle_tag(activity):
''' someone is tagging or shelving a book '''
''' someone is tagging a book '''
user = get_or_create_remote_user(activity['actor'])
if not user.local:
book = activity['target']['id'].split('/')[-1]
book = activity['target']['id']
status_builder.create_tag(user, book, activity['object']['name'])

View file

@ -3,7 +3,6 @@ from django.db import models
from fedireads.settings import DOMAIN
# TODO maybe this should be in /models?
class FedireadsModel(models.Model):
''' fields and functions for every model '''
created_date = models.DateTimeField(auto_now_add=True)
@ -12,6 +11,9 @@ class FedireadsModel(models.Model):
@property
def absolute_id(self):
''' constructs the absolute reference to any db object '''
if self.remote_id:
return self.remote_id
base_path = 'https://%s' % DOMAIN
if hasattr(self, 'user'):
base_path = self.user.absolute_id

View file

@ -3,9 +3,10 @@ from django.utils import timezone
from django.db import models
from model_utils.managers import InheritanceManager
from fedireads import activitypub
from fedireads.settings import DOMAIN
from fedireads.utils.fields import JSONField, ArrayField
from fedireads.utils.models import FedireadsModel
from .base_model import FedireadsModel
from fedireads.connectors.settings import CONNECTORS
@ -110,6 +111,10 @@ class Book(FedireadsModel):
self.title,
)
@property
def activitypub_serialize(self):
return activitypub.get_book(self)
class Work(Book):
''' a work (an abstract concept of a book that manifests in an edition) '''
@ -165,3 +170,7 @@ class Author(FedireadsModel):
models.CharField(max_length=255), blank=True, default=list
)
bio = models.TextField(null=True, blank=True)
@property
def activitypub_serialize(self):
return activitypub.get_author(self)

View file

@ -1,3 +1,4 @@
''' track progress of goodreads imports '''
import re
import dateutil.parser
@ -5,7 +6,7 @@ from django.db import models
from django.utils import timezone
from fedireads import books_manager
from fedireads.models import Edition, ReadThrough, User, Book
from fedireads.models import ReadThrough, User, Book
from fedireads.utils.fields import JSONField
# Mapping goodreads -> fedireads shelf titles.
@ -32,6 +33,7 @@ def construct_search_term(title, author):
return ' '.join([title, author])
class ImportJob(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
created_date = models.DateTimeField(default=timezone.now)

View file

@ -1,7 +1,8 @@
''' puttin' books on shelves '''
from django.db import models
from fedireads.utils.models import FedireadsModel
from fedireads import activitypub
from .base_model import FedireadsModel
class Shelf(FedireadsModel):

View file

@ -6,7 +6,8 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from model_utils.managers import InheritanceManager
from fedireads.utils.models import FedireadsModel
from fedireads import activitypub
from .base_model import FedireadsModel
class Status(FedireadsModel):
@ -48,6 +49,11 @@ class Status(FedireadsModel):
return '%s/%s/%d' % (base_path, model_name, self.id)
@property
def activitypub_serialize(self):
return activitypub.get_status(self)
class Comment(Status):
''' like a review but without a rating and transient '''
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
@ -58,6 +64,11 @@ class Comment(Status):
super().save(*args, **kwargs)
@property
def activitypub_serialize(self):
return activitypub.get_comment(self)
class Quotation(Status):
''' like a review but without a rating and transient '''
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
@ -69,6 +80,11 @@ class Quotation(Status):
super().save(*args, **kwargs)
@property
def activitypub_serialize(self):
return activitypub.get_quotation(self)
class Review(Status):
''' a book review '''
name = models.CharField(max_length=255, null=True)
@ -86,19 +102,17 @@ class Review(Status):
super().save(*args, **kwargs)
@property
def activitypub_serialize(self):
return activitypub.get_review(self)
class Favorite(FedireadsModel):
''' fav'ing a post '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
status = models.ForeignKey('Status', on_delete=models.PROTECT)
remote_id = models.CharField(max_length=255, unique=True, null=True)
@property
def absolute_id(self):
''' constructs the absolute reference to any db object '''
if self.remote_id:
return self.remote_id
return super().absolute_id
class Meta:
unique_together = ('user', 'status')

View file

@ -7,7 +7,7 @@ from django.dispatch import receiver
from fedireads.models.shelf import Shelf
from fedireads.settings import DOMAIN
from fedireads.utils.models import FedireadsModel
from .base_model import FedireadsModel
class User(AbstractUser):
@ -73,6 +73,10 @@ class User(AbstractUser):
username = self.localname or self.username
return 'https://%s/%s/%s' % (DOMAIN, model_name, username)
@property
def activitypub_serialize(self):
return activitypub.get_actor(self)
class UserRelationship(FedireadsModel):
''' many-to-many through table for followers '''

View file

@ -19,19 +19,19 @@ class InputHtmlParser(HTMLParser):
self.output.append(('tag', self.get_starttag_text()))
self.tag_stack.append(tag)
else:
self.output.append(('data', ' '))
self.output.append(('data', ''))
def handle_endtag(self, tag):
''' keep the close tag '''
if not self.allow_html or tag not in self.whitelist:
self.output.append(('data', ' '))
self.output.append(('data', ''))
return
if not self.tag_stack or self.tag_stack[-1] != tag:
# the end tag doesn't match the most recent start tag
self.allow_html = False
self.output.append(('data', ' '))
self.output.append(('data', ''))
return
self.tag_stack = self.tag_stack[:-1]
@ -45,6 +45,8 @@ class InputHtmlParser(HTMLParser):
def get_output(self):
''' convert the output from a list of tuples to a string '''
if self.tag_stack:
self.allow_html = False
if not self.allow_html:
return ''.join(v for (k, v) in self.output if k == 'data')
return ''.join(v for (k, v) in self.output)

View file

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

View file

@ -0,0 +1,42 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Document",
"book_type": "Edition",
"name": "Jonathan Strange and Mr Norrell",
"url": "https://example.com/book/122",
"authors": [
"https://example.com/author/25"
],
"published_date": "2017-05-10T00:00:00+00:00",
"work": {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Document",
"book_type": "Work",
"name": "Jonathan Strange and Mr Norrell",
"url": "https://example.com/book/121",
"authors": [
"https://example.com/author/25"
],
"title": "Jonathan Strange and Mr Norrell",
"attachment": [
{
"type": "Document",
"mediaType": "image/jpg",
"url": "https://example.com/images/covers/8775540-M.jpg",
"name": "Cover of \"Jonathan Strange and Mr Norrell\""
}
]
},
"title": "Jonathan Strange and Mr Norrell",
"subtitle": "Bloomsbury Modern Classics",
"isbn_13": "9781408891469",
"physical_format": "paperback",
"attachment": [
{
"type": "Document",
"mediaType": "image/jpg",
"url": "https://example.com/images/covers/9155821-M.jpg",
"name": "Cover of \"Jonathan Strange and Mr Norrell\""
}
]
}

View file

@ -0,0 +1 @@
[{"title": "Jonathan Strange and Mr Norrell", "key": "https://example.com/book/122", "author": "Susanna Clarke", "year": 2017}]

View file

@ -0,0 +1,44 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Document",
"book_type": "Work",
"name": "Jonathan Strange and Mr Norrell",
"url": "https://example.com/book/121",
"authors": [
"https://example.com/author/25"
],
"editions": [
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Document",
"book_type": "Edition",
"name": "Jonathan Strange and Mr Norrell",
"url": "https://example.com/book/122",
"authors": [
"https://example.com/author/25"
],
"published_date": "2017-05-10T00:00:00+00:00",
"title": "Jonathan Strange and Mr Norrell",
"subtitle": "Bloomsbury Modern Classics",
"isbn_13": "9781408891469",
"physical_format": "paperback",
"attachment": [
{
"type": "Document",
"mediaType": "image/jpg",
"url": "https://example.com/images/covers/9155821-M.jpg",
"name": "Cover of \"Jonathan Strange and Mr Norrell\""
}
]
}
],
"title": "Jonathan Strange and Mr Norrell",
"attachment": [
{
"type": "Document",
"mediaType": "image/jpg",
"url": "https://example.com/images/covers/8775540-M.jpg",
"name": "Cover of \"Jonathan Strange and Mr Norrell\""
}
]
}

View file

@ -0,0 +1,83 @@
{
"identifiers": {
"librarything": [
"10014"
],
"goodreads": [
"535197",
"1102517",
"518848"
]
},
"lc_classifications": [
"PZ7.N647 Sab 1995"
],
"latest_revision": 7,
"ocaid": "sabriel00nixg",
"ia_box_id": [
"IA107202"
],
"edition_name": "1st American ed.",
"title": "Sabriel",
"languages": [
{
"key": "/languages/eng"
}
],
"subjects": [
"Fantasy."
],
"publish_country": "nyu",
"by_statement": "Garth Nix.",
"type": {
"key": "/type/edition"
},
"revision": 7,
"publishers": [
"Harper Trophy"
],
"description": {
"type": "/type/text",
"value": "Sabriel, daughter of the necromancer Abhorsen, must journey into the mysterious and magical Old Kingdom to rescue her father from the Land of the Dead."
},
"last_modified": {
"type": "/type/datetime",
"value": "2017-10-08T21:20:07.665236"
},
"key": "/books/OL22951843M",
"authors": [
{
"key": "/authors/OL382982A"
}
],
"publish_places": [
"New York"
],
"pagination": "491 p. :",
"created": {
"type": "/type/datetime",
"value": "2009-02-12T16:29:58.929717"
},
"dewey_decimal_class": [
"[Fic]"
],
"notes": {
"type": "/type/text",
"value": "Originally published: Australia : HarperCollins, 1995."
},
"number_of_pages": 491,
"lccn": [
"96001295"
],
"isbn_10": [
"0060273224",
"0060273232",
"0064471837"
],
"publish_date": "1996",
"works": [
{
"key": "/works/OL15832982W"
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,132 @@
{
"start": 0,
"num_found": 2,
"numFound": 2,
"docs": [
{
"title_suggest": "This Is How You Lose the Time War",
"edition_key": [
"OL27901088M"
],
"isbn": [
"9781534431003",
"1534431004"
],
"has_fulltext": false,
"text": [
"OL27901088M",
"9781534431003",
"1534431004",
"Amal El-Mohtar",
"Max Gladstone",
"OL7313207A",
"OL7129451A",
"epistolary",
"science fiction",
"time-traveling",
"LGBT",
"This Is How You Lose the Time War",
"/works/OL20639540W",
"Simon and Schuster",
"Atlantis",
"London",
"The whole of time and space"
],
"author_name": [
"Amal El-Mohtar",
"Max Gladstone"
],
"seed": [
"/books/OL27901088M",
"/works/OL20639540W",
"/subjects/science_fiction",
"/subjects/time-traveling",
"/subjects/epistolary",
"/subjects/lgbt",
"/subjects/place:london",
"/subjects/place:atlantis",
"/subjects/time:the_whole_of_time_and_space",
"/authors/OL7313207A",
"/authors/OL7129451A"
],
"author_key": [
"OL7313207A",
"OL7129451A"
],
"availability": {
"status": "error"
},
"subject": [
"epistolary",
"science fiction",
"time-traveling",
"LGBT"
],
"title": "This Is How You Lose the Time War",
"publish_date": [
"July 16, 2019"
],
"type": "work",
"ebook_count_i": 0,
"publish_place": [
"New York, USA"
],
"edition_count": 1,
"key": "/works/OL20639540W",
"publisher": [
"Simon and Schuster"
],
"language": [
"eng"
],
"last_modified_i": 1579909341,
"cover_edition_key": "OL27901088M",
"publish_year": [
2019
],
"first_publish_year": 2019,
"place": [
"Atlantis",
"London"
],
"time": [
"The whole of time and space"
]
},
{
"title_suggest": "This is How You Lose the Time War",
"cover_i": 8665647,
"has_fulltext": false,
"title": "This is How You Lose the Time War",
"last_modified_i": 1561998020,
"edition_count": 0,
"author_name": [
"Amal El-Mohtar",
"Max Gladstone"
],
"seed": [
"/works/OL19859295W",
"/authors/OL7313207A",
"/authors/OL7129451A"
],
"key": "/works/OL19859295W",
"text": [
"Amal El-Mohtar",
"Max Gladstone",
"OL7313207A",
"OL7129451A",
"This is How You Lose the Time War",
"/works/OL19859295W"
],
"author_key": [
"OL7313207A",
"OL7129451A"
],
"type": "work",
"availability": {
"status": "error"
},
"ebook_count_i": 0
}
]
}

View file

@ -0,0 +1,63 @@
{
"first_publish_date": "1995",
"key": "/works/OL15832982W",
"description": {
"type": "/type/text",
"value": "First in the Old Kingdom/Abhorsen series."
},
"created": {
"type": "/type/datetime",
"value": "2011-07-07T16:30:28.384311"
},
"title": "Sabriel",
"covers": [
6796986,
3843137
],
"first_sentence": {
"type": "/type/text",
"value": "THE RABBIT HAD been run over minutes before."
},
"excerpts": [
{
"excerpt": "THE RABBIT HAD been run over minutes before."
}
],
"lc_classifications": [
"PZ7.N647 Sab 1995"
],
"latest_revision": 5,
"last_modified": {
"type": "/type/datetime",
"value": "2019-07-22T13:57:34.579651"
},
"authors": [
{
"type": {
"key": "/type/author_role"
},
"author": {
"key": "/authors/OL382982A"
}
}
],
"dewey_number": [
"[Fic]"
],
"subjects": [
"Fantasy",
"Science Fiction & Fantasy",
"Fantasy fiction",
"Fiction",
"Juvenile Fiction",
"Juvenile fiction",
"Magical thinking"
],
"type": {
"key": "/type/work"
},
"subject_times": [
"Life and Death."
],
"revision": 5
}

View file

@ -0,0 +1,44 @@
''' testing models '''
from django.test import TestCase
from fedireads import models, settings
class Book(TestCase):
''' not too much going on in the books model but here we are '''
def setUp(self):
work = models.Work.objects.create(title='Example Work')
models.Edition.objects.create(title='Example Edition', parent_work=work)
def test_absolute_id(self):
''' editions and works use the same absolute id syntax '''
book = models.Edition.objects.first()
expected_id = 'https://%s/book/%d' % (settings.DOMAIN, book.id)
self.assertEqual(book.absolute_id, expected_id)
def test_create_book(self):
''' you shouldn't be able to create Books (only editions and works) '''
self.assertRaises(
ValueError,
models.Book.objects.create,
title='Invalid Book'
)
def test_default_edition(self):
''' a work should always be able to produce a deafult edition '''
default_edition = models.Work.objects.first().default_edition
self.assertIsInstance(default_edition, models.Edition)
class Shelf(TestCase):
def setUp(self):
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
models.Shelf.objects.create(
name='Test Shelf', identifier='test-shelf', user=user)
def test_absolute_id(self):
''' editions and works use the same absolute id syntax '''
shelf = models.Shelf.objects.get(identifier='test-shelf')
expected_id = 'https://%s/user/mouse/shelf/test-shelf' % settings.DOMAIN
self.assertEqual(shelf.absolute_id, expected_id)

View file

@ -0,0 +1,56 @@
from django.test import TestCase
from fedireads import models
from fedireads import status as status_builder
class Comment(TestCase):
''' we have hecka ways to create statuses '''
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
self.book = models.Edition.objects.create(title='Example Edition')
def test_create_comment(self):
comment = status_builder.create_comment(
self.user, self.book, 'commentary')
self.assertEqual(comment.content, 'commentary')
def test_comment_from_activity(self):
activity = {
"id": "https://example.com/user/mouse/comment/6",
"url": "https://example.com/user/mouse/comment/6",
"inReplyTo": None,
"published": "2020-05-08T23:45:44.768012+00:00",
"attributedTo": "https://example.com/user/mouse",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://example.com/user/mouse/followers"
],
"sensitive": False,
"content": "commentary",
"type": "Note",
"attachment": [],
"replies": {
"id": "https://example.com/user/mouse/comment/6/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://example.com/user/mouse/comment/6/replies?only_other_accounts=true&page=true",
"partOf": "https://example.com/user/mouse/comment/6/replies",
"items": []
}
},
"inReplyToBook": self.book.absolute_id,
"fedireadsType": "Comment"
}
comment = status_builder.create_comment_from_activity(
self.user, activity)
self.assertEqual(comment.content, 'commentary')
self.assertEqual(comment.book, self.book)
self.assertEqual(
comment.published_date, '2020-05-08T23:45:44.768012+00:00')

View file

@ -0,0 +1,65 @@
''' testing book data connectors '''
from dateutil import parser
from django.test import TestCase
import json
import pathlib
from fedireads import models
from fedireads.connectors.fedireads_connector import Connector
from fedireads.connectors.abstract_connector import SearchResult, get_date
class FedireadsConnector(TestCase):
def setUp(self):
models.Connector.objects.create(
identifier='example.com',
connector_file='fedireads_connector',
base_url='https://example.com',
books_url='https://example.com',
covers_url='https://example.com/images/covers',
search_url='https://example.com/search?q=',
key_name='remote_id',
)
self.connector = Connector('example.com')
work_file = pathlib.Path(__file__).parent.joinpath(
'data/fr_work.json')
edition_file = pathlib.Path(__file__).parent.joinpath(
'data/fr_edition.json')
self.work_data = json.loads(work_file.read_bytes())
self.edition_data = json.loads(edition_file.read_bytes())
def test_is_work_data(self):
self.assertEqual(self.connector.is_work_data(self.work_data), True)
self.assertEqual(self.connector.is_work_data(self.edition_data), False)
def test_get_edition_from_work_data(self):
edition = self.connector.get_edition_from_work_data(self.work_data)
self.assertEqual(edition['url'], 'https://example.com/book/122')
def test_get_work_from_edition_data(self):
work = self.connector.get_work_from_edition_date(self.edition_data)
self.assertEqual(work['url'], 'https://example.com/book/121')
def test_format_search_result(self):
datafile = pathlib.Path(__file__).parent.joinpath('data/fr_search.json')
search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_search_data(search_data)
self.assertIsInstance(results, list)
result = self.connector.format_search_result(results[0])
self.assertIsInstance(result, SearchResult)
self.assertEqual(result.title, 'Jonathan Strange and Mr Norrell')
self.assertEqual(result.key, 'https://example.com/book/122')
self.assertEqual(result.author, 'Susanna Clarke')
self.assertEqual(result.year, 2017)
def test_get_date(self):
date = get_date(self.edition_data['published_date'])
expected = parser.parse("2017-05-10T00:00:00+00:00")
self.assertEqual(date, expected)

View file

@ -0,0 +1,79 @@
''' testing book data connectors '''
from dateutil import parser
from django.test import TestCase
import json
import pathlib
import pytz
from fedireads import models
from fedireads.connectors.openlibrary import Connector
from fedireads.connectors.openlibrary import get_languages, get_description
from fedireads.connectors.openlibrary import pick_default_edition
from fedireads.connectors.abstract_connector import SearchResult, get_date
class Openlibrary(TestCase):
def setUp(self):
models.Connector.objects.create(
identifier='openlibrary.org',
name='OpenLibrary',
connector_file='openlibrary',
base_url='https://openlibrary.org',
books_url='https://openlibrary.org',
covers_url='https://covers.openlibrary.org',
search_url='https://openlibrary.org/search?q=',
key_name='openlibrary_key',
)
self.connector = Connector('openlibrary.org')
work_file = pathlib.Path(__file__).parent.joinpath(
'data/ol_work.json')
edition_file = pathlib.Path(__file__).parent.joinpath(
'data/ol_edition.json')
edition_list_file = pathlib.Path(__file__).parent.joinpath(
'data/ol_edition_list.json')
self.work_data = json.loads(work_file.read_bytes())
self.edition_data = json.loads(edition_file.read_bytes())
self.edition_list_data = json.loads(edition_list_file.read_bytes())
def test_is_work_data(self):
self.assertEqual(self.connector.is_work_data(self.work_data), True)
self.assertEqual(self.connector.is_work_data(self.edition_data), False)
def test_pick_default_edition(self):
edition = pick_default_edition(self.edition_list_data['entries'])
self.assertEqual(edition['key'], '/books/OL9952943M')
def test_format_search_result(self):
''' translate json from openlibrary into SearchResult '''
datafile = pathlib.Path(__file__).parent.joinpath('data/ol_search.json')
search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_search_data(search_data)
self.assertIsInstance(results, list)
result = self.connector.format_search_result(results[0])
self.assertIsInstance(result, SearchResult)
self.assertEqual(result.title, 'This Is How You Lose the Time War')
self.assertEqual(result.key, 'https://openlibrary.org/works/OL20639540W')
self.assertEqual(result.author, 'Amal El-Mohtar, Max Gladstone')
self.assertEqual(result.year, 2019)
def test_get_description(self):
description = get_description(self.work_data['description'])
expected = 'First in the Old Kingdom/Abhorsen series.'
self.assertEqual(description, expected)
def test_get_date(self):
date = get_date(self.work_data['first_publish_date'])
expected = pytz.utc.localize(parser.parse('1995'))
self.assertEqual(date, expected)
def test_get_languages(self):
languages = get_languages(self.edition_data['languages'])
self.assertEqual(languages, ['English'])

View file

@ -0,0 +1,112 @@
''' testing models '''
import datetime
from django.test import TestCase
from fedireads import models
class ImportJob(TestCase):
''' this is a fancy one!!! '''
def setUp(self):
''' data is from a goodreads export of The Raven Tower '''
read_data = {
'Book Id': 39395857,
'Title': 'The Raven Tower',
'Author': 'Ann Leckie',
'Author l-f': 'Leckie, Ann',
'Additional Authors': '',
'ISBN': '="0356506991"',
'ISBN13': '="9780356506999"',
'My Rating': 0,
'Average Rating': 4.06,
'Publisher': 'Orbit',
'Binding': 'Hardcover',
'Number of Pages': 416,
'Year Published': 2019,
'Original Publication Year': 2019,
'Date Read': '2019/04/09',
'Date Added': '2019/04/09',
'Bookshelves': '',
'Bookshelves with positions': '',
'Exclusive Shelf': 'read',
'My Review': '',
'Spoiler': '',
'Private Notes': '',
'Read Count': 1,
'Recommended For': '',
'Recommended By': '',
'Owned Copies': 0,
'Original Purchase Date': '',
'Original Purchase Location': '',
'Condition': '',
'Condition Description': '',
'BCID': ''
}
currently_reading_data = read_data.copy()
currently_reading_data['Exclusive Shelf'] = 'currently-reading'
currently_reading_data['Date Read'] = ''
unknown_read_data = currently_reading_data.copy()
unknown_read_data['Exclusive Shelf'] = 'read'
unknown_read_data['Date Read'] = ''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
job = models.ImportJob.objects.create(user=user)
models.ImportItem.objects.create(
job=job, index=1, data=currently_reading_data)
models.ImportItem.objects.create(
job=job, index=2, data=read_data)
models.ImportItem.objects.create(
job=job, index=3, data=unknown_read_data)
def test_isbn(self):
''' it unquotes the isbn13 field from data '''
expected = '9780356506999'
item = models.ImportItem.objects.get(index=1)
self.assertEqual(item.isbn, expected)
def test_shelf(self):
''' converts to the local shelf typology '''
expected = 'reading'
item = models.ImportItem.objects.get(index=1)
self.assertEqual(item.shelf, expected)
def test_date_added(self):
''' converts to the local shelf typology '''
expected = datetime.datetime(2019, 4, 9, 0, 0)
item = models.ImportItem.objects.get(index=1)
self.assertEqual(item.date_added, expected)
def test_date_read(self):
''' converts to the local shelf typology '''
expected = datetime.datetime(2019, 4, 9, 0, 0)
item = models.ImportItem.objects.get(index=2)
self.assertEqual(item.date_read, expected)
def test_currently_reading_reads(self):
expected = [models.ReadThrough(
start_date=datetime.datetime(2019, 4, 9, 0, 0))]
actual = models.ImportItem.objects.get(index=1)
self.assertEqual(actual.reads[0].start_date, expected[0].start_date)
self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date)
def test_read_reads(self):
expected = [models.ReadThrough(
finish_date=datetime.datetime(2019, 4, 9, 0, 0))]
actual = models.ImportItem.objects.get(index=2)
self.assertEqual(actual.reads[0].start_date, expected[0].start_date)
self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date)
def test_unread_reads(self):
expected = []
actual = models.ImportItem.objects.get(index=3)
self.assertEqual(actual.reads, expected)

View file

@ -0,0 +1,66 @@
from django.test import TestCase
from fedireads import models
from fedireads import status as status_builder
class Quotation(TestCase):
''' we have hecka ways to create statuses '''
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
self.book = models.Edition.objects.create(title='Example Edition')
def test_create_quotation(self):
quotation = status_builder.create_quotation(
self.user, self.book, 'commentary', 'a quote')
self.assertEqual(quotation.quote, 'a quote')
self.assertEqual(quotation.content, 'commentary')
def test_quotation_from_activity(self):
activity = {
'id': 'https://example.com/user/mouse/quotation/13',
'url': 'https://example.com/user/mouse/quotation/13',
'inReplyTo': None,
'published': '2020-05-10T02:38:31.150343+00:00',
'attributedTo': 'https://example.com/user/mouse',
'to': [
'https://www.w3.org/ns/activitystreams#Public'
],
'cc': [
'https://example.com/user/mouse/followers'
],
'sensitive': False,
'content': 'commentary',
'type': 'Note',
'attachment': [
{
'type': 'Document',
'mediaType': 'image//images/covers/2b4e4712-5a4d-4ac1-9df4-634cc9c7aff3jpg',
'url': 'https://example.com/images/covers/2b4e4712-5a4d-4ac1-9df4-634cc9c7aff3jpg',
'name': 'Cover of \'This Is How You Lose the Time War\''
}
],
'replies': {
'id': 'https://example.com/user/mouse/quotation/13/replies',
'type': 'Collection',
'first': {
'type': 'CollectionPage',
'next': 'https://example.com/user/mouse/quotation/13/replies?only_other_accounts=true&page=true',
'partOf': 'https://example.com/user/mouse/quotation/13/replies',
'items': []
}
},
'inReplyToBook': self.book.absolute_id,
'fedireadsType': 'Quotation',
'quote': 'quote body'
}
quotation = status_builder.create_quotation_from_activity(
self.user, activity)
self.assertEqual(quotation.content, 'commentary')
self.assertEqual(quotation.quote, 'quote body')
self.assertEqual(quotation.book, self.book)
self.assertEqual(
quotation.published_date, '2020-05-10T02:38:31.150343+00:00')

View file

@ -0,0 +1,81 @@
from django.test import TestCase
from fedireads import models
from fedireads import status as status_builder
class Review(TestCase):
''' we have hecka ways to create statuses '''
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
self.book = models.Edition.objects.create(title='Example Edition')
def test_create_review(self):
review = status_builder.create_review(
self.user, self.book, 'review name', 'content', 5)
self.assertEqual(review.name, 'review name')
self.assertEqual(review.content, 'content')
self.assertEqual(review.rating, 5)
review = status_builder.create_review(
self.user, self.book, '<div>review</div> name', '<b>content', 5)
self.assertEqual(review.name, 'review name')
self.assertEqual(review.content, 'content')
self.assertEqual(review.rating, 5)
def test_review_rating(self):
review = status_builder.create_review(
self.user, self.book, 'review name', 'content', -1)
self.assertEqual(review.name, 'review name')
self.assertEqual(review.content, 'content')
self.assertEqual(review.rating, None)
review = status_builder.create_review(
self.user, self.book, 'review name', 'content', 6)
self.assertEqual(review.name, 'review name')
self.assertEqual(review.content, 'content')
self.assertEqual(review.rating, None)
def test_review_from_activity(self):
activity = {
'id': 'https://example.com/user/mouse/review/9',
'url': 'https://example.com/user/mouse/review/9',
'inReplyTo': None,
'published': '2020-05-04T00:00:00.000000+00:00',
'attributedTo': 'https://example.com/user/mouse',
'to': [
'https://www.w3.org/ns/activitystreams#Public'
],
'cc': [
'https://example.com/user/mouse/followers'
],
'sensitive': False,
'content': 'review content',
'type': 'Article',
'attachment': [],
'replies': {
'id': 'https://example.com/user/mouse/review/9/replies',
'type': 'Collection',
'first': {
'type': 'CollectionPage',
'next': 'https://example.com/user/mouse/review/9/replies?only_other_accounts=true&page=true',
'partOf': 'https://example.com/user/mouse/review/9/replies',
'items': []
}
},
'inReplyToBook': self.book.absolute_id,
'fedireadsType': 'Review',
'name': 'review title',
'rating': 3
}
review = status_builder.create_review_from_activity(
self.user, activity)
self.assertEqual(review.content, 'review content')
self.assertEqual(review.name, 'review title')
self.assertEqual(review.rating, 3)
self.assertEqual(review.book, self.book)
self.assertEqual(
review.published_date, '2020-05-04T00:00:00.000000+00:00')

View file

@ -0,0 +1,50 @@
from django.test import TestCase
from fedireads.sanitize_html import InputHtmlParser
class Sanitizer(TestCase):
def test_no_html(self):
input_text = 'no html '
parser = InputHtmlParser()
parser.feed(input_text)
output = parser.get_output()
self.assertEqual(input_text, output)
def test_valid_html(self):
input_text = '<b>yes </b> <i>html</i>'
parser = InputHtmlParser()
parser.feed(input_text)
output = parser.get_output()
self.assertEqual(input_text, output)
def test_valid_html_attrs(self):
input_text = '<a href="fish.com">yes </a> <i>html</i>'
parser = InputHtmlParser()
parser.feed(input_text)
output = parser.get_output()
self.assertEqual(input_text, output)
def test_invalid_html(self):
input_text = '<b>yes <i>html</i>'
parser = InputHtmlParser()
parser.feed(input_text)
output = parser.get_output()
self.assertEqual('yes html', output)
input_text = 'yes <i></b>html </i>'
parser = InputHtmlParser()
parser.feed(input_text)
output = parser.get_output()
self.assertEqual('yes html ', output)
def test_disallowed_html(self):
input_text = '<div> yes <i>html</i></div>'
parser = InputHtmlParser()
parser.feed(input_text)
output = parser.get_output()
self.assertEqual(' yes <i>html</i>', output)

View file

@ -0,0 +1,64 @@
from django.test import TestCase
from fedireads import models
from fedireads import status as status_builder
class Status(TestCase):
''' we have hecka ways to create statuses '''
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
def test_create_status(self):
content = 'statuses are usually <i>replies</i>'
status = status_builder.create_status(
self.user, content)
self.assertEqual(status.content, content)
reply = status_builder.create_status(
self.user, content, reply_parent=status)
self.assertEqual(reply.content, content)
self.assertEqual(reply.reply_parent, status)
def test_create_status_from_activity(self):
book = models.Edition.objects.create(title='Example Edition')
review = status_builder.create_review(
self.user, book, 'review name', 'content', 5)
activity = {
'id': 'https://example.com/user/mouse/status/12',
'url': 'https://example.com/user/mouse/status/12',
'inReplyTo': review.absolute_id,
'published': '2020-05-10T02:15:59.635557+00:00',
'attributedTo': 'https://example.com/user/mouse',
'to': [
'https://www.w3.org/ns/activitystreams#Public'
],
'cc': [
'https://example.com/user/mouse/followers'
],
'sensitive': False,
'content': 'reply to status',
'type': 'Note',
'attachment': [],
'replies': {
'id': 'https://example.com/user/mouse/status/12/replies',
'type': 'Collection',
'first': {
'type': 'CollectionPage',
'next': 'https://example.com/user/mouse/status/12/replies?only_other_accounts=true&page=true',
'partOf': 'https://example.com/user/mouse/status/12/replies',
'items': []
}
}
}
status = status_builder.create_status_from_activity(
self.user, activity)
self.assertEqual(status.reply_parent, review)
self.assertEqual(status.content, 'reply to status')
self.assertEqual(
status.published_date,
'2020-05-10T02:15:59.635557+00:00'
)

View file

@ -0,0 +1,60 @@
''' testing models '''
from django.test import TestCase
from fedireads import models, settings
class Status(TestCase):
def setUp(self):
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
book = models.Edition.objects.create(title='Example Edition')
models.Status.objects.create(user=user, content='Blah blah')
models.Comment.objects.create(user=user, content='content', book=book)
models.Quotation.objects.create(
user=user, content='content', book=book, quote='blah')
models.Review.objects.create(
user=user, content='content', book=book, rating=3)
def test_status(self):
status = models.Status.objects.first()
self.assertEqual(status.status_type, 'Note')
self.assertEqual(status.activity_type, 'Note')
expected_id = 'https://%s/user/mouse/status/%d' % \
(settings.DOMAIN, status.id)
self.assertEqual(status.absolute_id, expected_id)
def test_comment(self):
comment = models.Comment.objects.first()
self.assertEqual(comment.status_type, 'Comment')
self.assertEqual(comment.activity_type, 'Note')
expected_id = 'https://%s/user/mouse/comment/%d' % \
(settings.DOMAIN, comment.id)
self.assertEqual(comment.absolute_id, expected_id)
def test_quotation(self):
quotation = models.Quotation.objects.first()
self.assertEqual(quotation.status_type, 'Quotation')
self.assertEqual(quotation.activity_type, 'Note')
expected_id = 'https://%s/user/mouse/quotation/%d' % \
(settings.DOMAIN, quotation.id)
self.assertEqual(quotation.absolute_id, expected_id)
def test_review(self):
review = models.Review.objects.first()
self.assertEqual(review.status_type, 'Review')
self.assertEqual(review.activity_type, 'Article')
expected_id = 'https://%s/user/mouse/review/%d' % \
(settings.DOMAIN, review.id)
self.assertEqual(review.absolute_id, expected_id)
class Tag(TestCase):
def test_tag(self):
book = models.Edition.objects.create(title='Example Edition')
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
tag = models.Tag.objects.create(user=user, book=book, name='t/est tag')
self.assertEqual(tag.identifier, 't%2Fest+tag')

View file

@ -0,0 +1,35 @@
''' testing models '''
from django.test import TestCase
from fedireads import models
from fedireads.settings import DOMAIN
class User(TestCase):
def setUp(self):
models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
def test_computed_fields(self):
''' username instead of id here '''
user = models.User.objects.get(localname='mouse')
expected_id = 'https://%s/user/mouse' % DOMAIN
self.assertEqual(user.absolute_id, expected_id)
self.assertEqual(user.username, 'mouse@%s' % DOMAIN)
self.assertEqual(user.localname, 'mouse')
self.assertEqual(user.actor, 'https://%s/user/mouse' % DOMAIN)
self.assertEqual(user.shared_inbox, 'https://%s/inbox' % DOMAIN)
self.assertEqual(user.inbox, '%s/inbox' % expected_id)
self.assertEqual(user.outbox, '%s/outbox' % expected_id)
self.assertIsNotNone(user.private_key)
self.assertIsNotNone(user.public_key)
def test_user_shelves(self):
user = models.User.objects.get(localname='mouse')
shelves = models.Shelf.objects.filter(user=user).all()
self.assertEqual(len(shelves), 3)
names = [s.name for s in shelves]
self.assertEqual(names, ['To Read', 'Currently Reading', 'Read'])
ids = [s.identifier for s in shelves]
self.assertEqual(ids, ['to-read', 'reading', 'read'])

View file

@ -10,7 +10,7 @@ username_regex = r'(?P<username>[\w\-_]+@[\w\-\_\.]+)'
localname_regex = r'(?P<username>[\w\-_]+)'
user_path = r'^user/%s' % username_regex
local_user_path = r'^user/%s' % localname_regex
status_path = r'%s/(status|review|comment)/(?P<status_id>\d+)' % local_user_path
status_path = r'%s/(status|review|comment|quotation)/(?P<status_id>\d+)' % local_user_path
book_path = r'^book/(?P<book_id>\d+)'
handler404 = 'fedireads.views.not_found_page'
@ -65,8 +65,8 @@ urlpatterns = [
re_path(r'^author/(?P<author_id>[\w\-]+)(.json)?/?$', views.author_page),
re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page),
re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % username_regex, views.shelf_page),
re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % localname_regex, views.shelf_page),
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % user_path, views.shelf_page),
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % local_user_path, views.shelf_page),
re_path(r'^search/?$', views.search),

View file

@ -6,7 +6,6 @@ from django.db.models import Avg, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound,\
JsonResponse
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.views.decorators.csrf import csrf_exempt
@ -239,7 +238,7 @@ def user_page(request, username, subpage=None):
if is_api_request(request):
# we have a json request
return JsonResponse(activitypub.get_actor(user))
return JsonResponse(user.activitypub_serialize)
# otherwise we're at a UI view
# TODO: change display with privacy and authentication considerations
@ -330,7 +329,7 @@ def status_page(request, username, status_id):
return HttpResponseNotFound()
if is_api_request(request):
return JsonResponse(activitypub.get_status(status))
return JsonResponse(status.activitypub_serialize)
data = {
'status': status,
@ -390,7 +389,7 @@ def edit_profile_page(request):
def book_page(request, book_id, tab='friends'):
''' info about a book '''
book = get_or_create_book(book_id)
book = models.Book.objects.select_subclasses().get(id=book_id)
if is_api_request(request):
return JsonResponse(activitypub.get_book(book))
@ -531,7 +530,8 @@ def shelf_page(request, username, shelf_identifier):
shelf = models.Shelf.objects.get(user=user, identifier=shelf_identifier)
if is_api_request(request):
return activitypub.get_shelf(shelf)
page = request.GET.get('page')
return JsonResponse(activitypub.get_shelf(shelf, page=page))
data = {
'shelf': shelf,

View file

@ -53,23 +53,23 @@ def nodeinfo(request):
status_count = models.Status.objects.filter(user__local=True).count()
user_count = models.User.objects.count()
return JsonResponse({
"version": "2.0",
"software": {
"name": "fedireads",
"version": "0.0.1"
'version': '2.0',
'software': {
'name': 'fedireads',
'version': '0.0.1'
},
"protocols": [
"activitypub"
'protocols': [
'activitypub'
],
"usage": {
"users": {
"total": user_count,
"activeMonth": user_count, # TODO
"activeHalfyear": user_count, # TODO
'usage': {
'users': {
'total': user_count,
'activeMonth': user_count, # TODO
'activeHalfyear': user_count, # TODO
},
"localPosts": status_count,
'localPosts': status_count,
},
"openRegistrations": True,
'openRegistrations': True,
})