mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-23 01:51:08 +00:00
Reviews
This commit is contained in:
parent
0345a5d9ff
commit
7ee59c5fd5
10 changed files with 407 additions and 256 deletions
|
@ -1,121 +0,0 @@
|
||||||
''' generates activitypub formatted objects '''
|
|
||||||
from uuid import uuid4
|
|
||||||
from fedireads.settings import DOMAIN
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
def outbox_collection(user, size):
|
|
||||||
''' outbox okay cool '''
|
|
||||||
return {
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
'id': '%s/outbox' % user.actor,
|
|
||||||
'type': 'OrderedCollection',
|
|
||||||
'totalItems': size,
|
|
||||||
'first': '%s/outbox?page=true' % user.actor,
|
|
||||||
'last': '%s/outbox?min_id=0&page=true' % user.actor
|
|
||||||
}
|
|
||||||
|
|
||||||
def shelve_activity(user, book, shelf):
|
|
||||||
''' a user puts a book on a shelf.
|
|
||||||
activitypub action type Add
|
|
||||||
https://www.w3.org/ns/activitystreams#Add '''
|
|
||||||
book_title = book.data['title']
|
|
||||||
summary = '%s added %s to %s' % (
|
|
||||||
user.username,
|
|
||||||
book_title,
|
|
||||||
shelf.name
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
'summary': summary,
|
|
||||||
'type': 'Add',
|
|
||||||
'actor': user.actor,
|
|
||||||
'object': {
|
|
||||||
'type': 'Document',
|
|
||||||
'name': book_title,
|
|
||||||
'url': book.openlibary_key
|
|
||||||
},
|
|
||||||
'target': {
|
|
||||||
'type': 'Collection',
|
|
||||||
'name': shelf.name,
|
|
||||||
'id': shelf.activitypub_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def create_activity(user, obj):
|
|
||||||
''' wraps any object we're broadcasting '''
|
|
||||||
uuid = uuid4()
|
|
||||||
return {
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
|
|
||||||
'id': str(uuid),
|
|
||||||
'type': 'Create',
|
|
||||||
'actor': user.actor,
|
|
||||||
|
|
||||||
'to': ['%s/followers' % user.actor],
|
|
||||||
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
|
|
||||||
|
|
||||||
'object': obj,
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def note_object(user, content):
|
|
||||||
''' a lil post '''
|
|
||||||
uuid = uuid4()
|
|
||||||
return {
|
|
||||||
'id': str(uuid),
|
|
||||||
'type': 'Note',
|
|
||||||
'published': datetime.utcnow().isoformat(),
|
|
||||||
'attributedTo': user.actor,
|
|
||||||
'content': content,
|
|
||||||
'to': 'https://www.w3.org/ns/activitystreams#Public'
|
|
||||||
}
|
|
||||||
|
|
||||||
def follow_request(user, follow):
|
|
||||||
''' ask to be friends '''
|
|
||||||
return {
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
'summary': '',
|
|
||||||
'type': 'Follow',
|
|
||||||
'actor': user.actor,
|
|
||||||
'object': follow,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def accept_follow(activity, user):
|
|
||||||
''' say YES! to a user '''
|
|
||||||
uuid = uuid4()
|
|
||||||
return {
|
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
||||||
'id': 'https://%s/%s' % (DOMAIN, uuid),
|
|
||||||
'type': 'Accept',
|
|
||||||
'actor': user.actor,
|
|
||||||
'object': activity,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def actor(user):
|
|
||||||
''' format an actor object from a user '''
|
|
||||||
return {
|
|
||||||
'@context': [
|
|
||||||
'https://www.w3.org/ns/activitystreams',
|
|
||||||
'https://w3id.org/security/v1'
|
|
||||||
],
|
|
||||||
|
|
||||||
'id': user.actor,
|
|
||||||
'type': 'Person',
|
|
||||||
'preferredUsername': user.username,
|
|
||||||
'inbox': inbox(user),
|
|
||||||
'followers': '%s/followers' % user.actor,
|
|
||||||
'publicKey': {
|
|
||||||
'id': '%s/#main-key' % user.actor,
|
|
||||||
'owner': user.actor,
|
|
||||||
'publicKeyPem': user.public_key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def inbox(user):
|
|
||||||
''' describe an inbox '''
|
|
||||||
return '%s/inbox' % (user.actor)
|
|
|
@ -9,10 +9,9 @@ from django.http import HttpResponse, HttpResponseBadRequest, \
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from fedireads.settings import DOMAIN
|
from fedireads.settings import DOMAIN
|
||||||
from fedireads import models
|
from fedireads import models
|
||||||
from fedireads import openlibrary
|
|
||||||
import fedireads.activitypub_templates as templates
|
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
def webfinger(request):
|
def webfinger(request):
|
||||||
''' allow other servers to ask about a user '''
|
''' allow other servers to ask about a user '''
|
||||||
|
@ -23,12 +22,7 @@ def webfinger(request):
|
||||||
user = models.User.objects.filter(full_username=ap_id).first()
|
user = models.User.objects.filter(full_username=ap_id).first()
|
||||||
if not user:
|
if not user:
|
||||||
return HttpResponseNotFound('No account found')
|
return HttpResponseNotFound('No account found')
|
||||||
return JsonResponse(format_webfinger(user))
|
return JsonResponse({
|
||||||
|
|
||||||
|
|
||||||
def format_webfinger(user):
|
|
||||||
''' helper function to create structured webfinger json '''
|
|
||||||
return {
|
|
||||||
'subject': 'acct:%s' % (user.full_username),
|
'subject': 'acct:%s' % (user.full_username),
|
||||||
'links': [
|
'links': [
|
||||||
{
|
{
|
||||||
|
@ -37,21 +31,40 @@ def format_webfinger(user):
|
||||||
'href': user.actor
|
'href': user.actor
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
})
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def get_actor(request, username):
|
def get_actor(request, username):
|
||||||
''' return an activitypub actor object '''
|
''' return an activitypub actor object '''
|
||||||
|
if request.method != 'GET':
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
user = models.User.objects.get(username=username)
|
user = models.User.objects.get(username=username)
|
||||||
return JsonResponse(templates.actor(user))
|
return JsonResponse({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1'
|
||||||
|
],
|
||||||
|
|
||||||
|
'id': user.actor,
|
||||||
|
'type': 'Person',
|
||||||
|
'preferredUsername': user.username,
|
||||||
|
'inbox': format_inbox(user),
|
||||||
|
'followers': '%s/followers' % user.actor,
|
||||||
|
'publicKey': {
|
||||||
|
'id': '%s/#main-key' % user.actor,
|
||||||
|
'owner': user.actor,
|
||||||
|
'publicKeyPem': user.public_key,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def inbox(request, username):
|
def inbox(request, username):
|
||||||
''' incoming activitypub events '''
|
''' incoming activitypub events '''
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
# return a collection of something?
|
# TODO: return a collection of something?
|
||||||
return JsonResponse({})
|
return JsonResponse({})
|
||||||
|
|
||||||
# TODO: RSA key verification
|
# TODO: RSA key verification
|
||||||
|
@ -60,30 +73,47 @@ def inbox(request, username):
|
||||||
activity = json.loads(request.body)
|
activity = json.loads(request.body)
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
return HttpResponseBadRequest
|
return HttpResponseBadRequest
|
||||||
|
|
||||||
|
# TODO: should do some kind of checking if the user accepts
|
||||||
|
# this action from the sender
|
||||||
|
# but this will just throw an error if the user doesn't exist I guess
|
||||||
|
models.User.objects.get(username=username)
|
||||||
|
|
||||||
if activity['type'] == 'Add':
|
if activity['type'] == 'Add':
|
||||||
handle_add(activity)
|
return handle_add(activity)
|
||||||
|
|
||||||
if activity['type'] == 'Follow':
|
if activity['type'] == 'Follow':
|
||||||
response = handle_follow(activity)
|
return handle_incoming_follow(activity)
|
||||||
return JsonResponse(response)
|
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
def handle_add(activity):
|
def handle_add(activity):
|
||||||
''' adding a book to a shelf '''
|
''' receiving an Add activity (to shelve a book) '''
|
||||||
|
# TODO what happens here? If it's a remote over, then I think
|
||||||
|
# I should save both the activity and the ShelfBook entry. But
|
||||||
|
# I'll do that later.
|
||||||
|
uuid = activity['id']
|
||||||
|
models.ShelveActivity.objects.get(uuid=uuid)
|
||||||
|
'''
|
||||||
book_id = activity['object']['url']
|
book_id = activity['object']['url']
|
||||||
book = openlibrary.get_or_create_book(book_id)
|
book = openlibrary.get_or_create_book(book_id)
|
||||||
user_ap_id = activity['actor'].replace('https//:', '')
|
user_ap_id = activity['actor'].replace('https//:', '')
|
||||||
user = models.User.objects.get(actor=user_ap_id)
|
user = models.User.objects.get(actor=user_ap_id)
|
||||||
|
if not user or not user.local:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
shelf = models.Shelf.objects.get(activitypub_id=activity['target']['id'])
|
shelf = models.Shelf.objects.get(activitypub_id=activity['target']['id'])
|
||||||
models.ShelfBook(
|
models.ShelfBook(
|
||||||
shelf=shelf,
|
shelf=shelf,
|
||||||
book=book,
|
book=book,
|
||||||
added_by=user,
|
added_by=user,
|
||||||
).save()
|
).save()
|
||||||
|
'''
|
||||||
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
def handle_follow(activity):
|
def handle_incoming_follow(activity):
|
||||||
'''
|
'''
|
||||||
{
|
{
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
@ -99,42 +129,160 @@ def handle_follow(activity):
|
||||||
# figure out who they are
|
# figure out who they are
|
||||||
user = get_or_create_remote_user(activity)
|
user = get_or_create_remote_user(activity)
|
||||||
following.followers.add(user)
|
following.followers.add(user)
|
||||||
# accept the request
|
# verify uuid and accept the request
|
||||||
return templates.accept_follow(activity, following)
|
models.FollowActivity(
|
||||||
|
uuid=activity['id'],
|
||||||
|
user=user,
|
||||||
|
followed=following,
|
||||||
|
content=activity,
|
||||||
|
activity_type='Follow',
|
||||||
|
)
|
||||||
|
uuid = uuid4()
|
||||||
|
return JsonResponse({
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': 'https://%s/%s' % (DOMAIN, uuid),
|
||||||
|
'type': 'Accept',
|
||||||
|
'actor': user.actor,
|
||||||
|
'object': activity,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def handle_outgoing_follow(user, to_follow):
|
||||||
|
''' someone local wants to follow someone '''
|
||||||
|
uuid = uuid4()
|
||||||
|
activity = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': str(uuid),
|
||||||
|
'summary': '',
|
||||||
|
'type': 'Follow',
|
||||||
|
'actor': user.actor,
|
||||||
|
'object': to_follow,
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast(user, activity, [format_inbox(to_follow)])
|
||||||
|
models.FollowActivity(
|
||||||
|
uuid=uuid,
|
||||||
|
user=user,
|
||||||
|
content=activity,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_shelve(user, book, shelf):
|
||||||
|
''' gettin organized '''
|
||||||
|
# update the database
|
||||||
|
models.ShelfBook(book=book, shelf=shelf, added_by=user).save()
|
||||||
|
|
||||||
|
# send out the activitypub action
|
||||||
|
summary = '%s marked %s as %s' % (
|
||||||
|
user.username,
|
||||||
|
book.data['title'],
|
||||||
|
shelf.name
|
||||||
|
)
|
||||||
|
|
||||||
|
uuid = uuid4()
|
||||||
|
activity = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': str(uuid),
|
||||||
|
'summary': summary,
|
||||||
|
'type': 'Add',
|
||||||
|
'actor': user.actor,
|
||||||
|
'object': {
|
||||||
|
'type': 'Document',
|
||||||
|
'name': book.data['title'],
|
||||||
|
'url': book.openlibrary_key
|
||||||
|
},
|
||||||
|
'target': {
|
||||||
|
'type': 'Collection',
|
||||||
|
'name': shelf.name,
|
||||||
|
'id': shelf.activitypub_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recipients = [format_inbox(u) for u in user.followers.all()]
|
||||||
|
|
||||||
|
models.ShelveActivity(
|
||||||
|
uuid=uuid,
|
||||||
|
user=user,
|
||||||
|
content=activity,
|
||||||
|
activity_type='Add',
|
||||||
|
shelf=shelf,
|
||||||
|
book=book,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
broadcast(user, activity, recipients)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_review(user, book, name, content, rating):
|
||||||
|
''' post a review '''
|
||||||
|
review_uuid = uuid4()
|
||||||
|
obj = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': str(review_uuid),
|
||||||
|
'type': 'Article',
|
||||||
|
'published': datetime.utcnow().isoformat(),
|
||||||
|
'attributedTo': user.actor,
|
||||||
|
'content': content,
|
||||||
|
'inReplyTo': book.activitypub_id,
|
||||||
|
'rating': rating, # fedireads-only custom field
|
||||||
|
'to': 'https://www.w3.org/ns/activitystreams#Public'
|
||||||
|
}
|
||||||
|
recipients = [format_inbox(u) for u in user.followers.all()]
|
||||||
|
create_uuid = uuid4()
|
||||||
|
activity = {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
|
||||||
|
'id': str(create_uuid),
|
||||||
|
'type': 'Create',
|
||||||
|
'actor': user.actor,
|
||||||
|
|
||||||
|
'to': ['%s/followers' % user.actor],
|
||||||
|
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
|
|
||||||
|
'object': obj,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Review(
|
||||||
|
uuid=create_uuid,
|
||||||
|
user=user,
|
||||||
|
content=activity,
|
||||||
|
activity_type='Article',
|
||||||
|
book=book,
|
||||||
|
work=book.works.first(),
|
||||||
|
name=name,
|
||||||
|
rating=rating,
|
||||||
|
review_content=content,
|
||||||
|
).save()
|
||||||
|
broadcast(user, activity, recipients)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def outbox(request, username):
|
def outbox(request, username):
|
||||||
''' outbox for the requested user '''
|
''' outbox for the requested user '''
|
||||||
user = models.User.objects.get(username=username)
|
user = models.User.objects.get(username=username)
|
||||||
size = models.Message.objects.filter(user=user).count()
|
size = models.Review.objects.filter(user=user).count()
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
# list of activities
|
# list of activities
|
||||||
return JsonResponse(templates.outbox_collection(user, size))
|
return JsonResponse({
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'id': '%s/outbox' % user.actor,
|
||||||
|
'type': 'OrderedCollection',
|
||||||
|
'totalItems': size,
|
||||||
|
'first': '%s/outbox?page=true' % user.actor,
|
||||||
|
'last': '%s/outbox?min_id=0&page=true' % user.actor
|
||||||
|
})
|
||||||
|
# TODO: paginated list of messages
|
||||||
|
|
||||||
data = request.body.decode('utf-8')
|
#data = request.body.decode('utf-8')
|
||||||
if data.activity.type == 'Follow':
|
|
||||||
handle_follow(data)
|
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
def broadcast_activity(sender, obj, recipients):
|
def broadcast(sender, action, recipients):
|
||||||
''' sign and send out the actions '''
|
|
||||||
activity = templates.create_activity(sender, obj)
|
|
||||||
|
|
||||||
# store message in database
|
|
||||||
models.Message(user=sender, content=activity).save()
|
|
||||||
|
|
||||||
for recipient in recipients:
|
|
||||||
broadcast(sender, activity, recipient)
|
|
||||||
|
|
||||||
|
|
||||||
def broadcast_follow(sender, action, destination):
|
|
||||||
''' send a follow request '''
|
|
||||||
broadcast(sender, action, destination)
|
|
||||||
|
|
||||||
def broadcast(sender, action, destination):
|
|
||||||
''' send out an event to all followers '''
|
''' send out an event to all followers '''
|
||||||
|
for recipient in recipients:
|
||||||
|
sign_and_send(sender, action, recipient)
|
||||||
|
|
||||||
|
def sign_and_send(sender, action, destination):
|
||||||
|
''' crpyto whatever and http junk '''
|
||||||
inbox_fragment = '/api/u/%s/inbox' % (sender.username)
|
inbox_fragment = '/api/u/%s/inbox' % (sender.username)
|
||||||
now = datetime.utcnow().isoformat()
|
now = datetime.utcnow().isoformat()
|
||||||
message_to_sign = '''(request-target): post %s
|
message_to_sign = '''(request-target): post %s
|
||||||
|
@ -175,3 +323,6 @@ def get_or_create_remote_user(activity):
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def format_inbox(user):
|
||||||
|
''' describe an inbox '''
|
||||||
|
return '%s/inbox' % (user.actor)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 2.0.13 on 2020-01-27 05:42
|
# Generated by Django 3.0.2 on 2020-01-28 02:46
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
|
@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('auth', '0009_alter_user_last_name_max_length'),
|
('auth', '0011_update_proxy_permissions'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -53,11 +53,23 @@ class Migration(migrations.Migration):
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Activity',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.CharField(max_length=255, unique=True)),
|
||||||
|
('content', django.contrib.postgres.fields.jsonb.JSONField(max_length=5000)),
|
||||||
|
('activity_type', models.CharField(max_length=255)),
|
||||||
|
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_date', models.DateTimeField(auto_now=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Author',
|
name='Author',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('openlibary_key', models.CharField(max_length=255)),
|
('openlibrary_key', models.CharField(max_length=255)),
|
||||||
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||||
('added_date', models.DateTimeField(auto_now_add=True)),
|
('added_date', models.DateTimeField(auto_now_add=True)),
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
('updated_date', models.DateTimeField(auto_now=True)),
|
||||||
|
@ -68,7 +80,7 @@ class Migration(migrations.Migration):
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('activitypub_id', models.CharField(max_length=255)),
|
('activitypub_id', models.CharField(max_length=255)),
|
||||||
('openlibary_key', models.CharField(max_length=255)),
|
('openlibrary_key', models.CharField(max_length=255)),
|
||||||
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||||
('added_date', models.DateTimeField(auto_now_add=True)),
|
('added_date', models.DateTimeField(auto_now_add=True)),
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
('updated_date', models.DateTimeField(auto_now=True)),
|
||||||
|
@ -76,16 +88,6 @@ class Migration(migrations.Migration):
|
||||||
('authors', models.ManyToManyField(to='fedireads.Author')),
|
('authors', models.ManyToManyField(to='fedireads.Author')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
|
||||||
name='Message',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('content', django.contrib.postgres.fields.jsonb.JSONField(max_length=5000)),
|
|
||||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Shelf',
|
name='Shelf',
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -99,6 +101,23 @@ class Migration(migrations.Migration):
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
('updated_date', models.DateTimeField(auto_now=True)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Work',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('openlibrary_key', models.CharField(max_length=255)),
|
||||||
|
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||||
|
('added_date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_date', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Note',
|
||||||
|
fields=[
|
||||||
|
('activity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fedireads.Activity')),
|
||||||
|
],
|
||||||
|
bases=('fedireads.activity',),
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ShelfBook',
|
name='ShelfBook',
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -108,16 +127,9 @@ class Migration(migrations.Migration):
|
||||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
|
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
|
||||||
('shelf', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Shelf')),
|
('shelf', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Shelf')),
|
||||||
],
|
],
|
||||||
),
|
options={
|
||||||
migrations.CreateModel(
|
'unique_together': {('book', 'shelf')},
|
||||||
name='Work',
|
},
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('openlibary_key', models.CharField(max_length=255)),
|
|
||||||
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
|
||||||
('added_date', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_date', models.DateTimeField(auto_now=True)),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='shelf',
|
model_name='shelf',
|
||||||
|
@ -139,12 +151,37 @@ class Migration(migrations.Migration):
|
||||||
name='works',
|
name='works',
|
||||||
field=models.ManyToManyField(to='fedireads.Work'),
|
field=models.ManyToManyField(to='fedireads.Work'),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.CreateModel(
|
||||||
name='shelfbook',
|
name='ShelveActivity',
|
||||||
unique_together={('book', 'shelf')},
|
fields=[
|
||||||
|
('activity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fedireads.Activity')),
|
||||||
|
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
|
||||||
|
('shelf', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Shelf')),
|
||||||
|
],
|
||||||
|
bases=('fedireads.activity',),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='shelf',
|
name='shelf',
|
||||||
unique_together={('user', 'name')},
|
unique_together={('user', 'name')},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Review',
|
||||||
|
fields=[
|
||||||
|
('activity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fedireads.Activity')),
|
||||||
|
('name', models.TextField()),
|
||||||
|
('rating', models.IntegerField(default=0)),
|
||||||
|
('review_content', models.TextField()),
|
||||||
|
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
|
||||||
|
('work', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Work')),
|
||||||
|
],
|
||||||
|
bases=('fedireads.activity',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FollowActivity',
|
||||||
|
fields=[
|
||||||
|
('activity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fedireads.Activity')),
|
||||||
|
('followed', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='followed', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
bases=('fedireads.activity',),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,15 +10,21 @@ import re
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
''' a user who wants to read books '''
|
''' a user who wants to read books '''
|
||||||
full_username = models.CharField(max_length=255, blank=True, null=True, unique=True)
|
full_username = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
private_key = models.TextField(blank=True, null=True)
|
private_key = models.TextField(blank=True, null=True)
|
||||||
public_key = models.TextField(blank=True, null=True)
|
public_key = models.TextField(blank=True, null=True)
|
||||||
api_key = models.CharField(max_length=255, blank=True, null=True)
|
api_key = models.CharField(max_length=255, blank=True, null=True)
|
||||||
actor = models.CharField(max_length=255)
|
actor = models.CharField(max_length=255)
|
||||||
local = models.BooleanField(default=True)
|
local = models.BooleanField(default=True)
|
||||||
|
# TODO: a field for if non-local users are readers or others
|
||||||
|
followers = models.ManyToManyField('self', symmetrical=False)
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
followers = models.ManyToManyField('self', symmetrical=False)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# give a new user keys
|
# give a new user keys
|
||||||
|
@ -40,8 +46,9 @@ class User(AbstractUser):
|
||||||
def execute_after_save(sender, instance, created, *args, **kwargs):
|
def execute_after_save(sender, instance, created, *args, **kwargs):
|
||||||
''' create shelves for new users '''
|
''' create shelves for new users '''
|
||||||
# TODO: how are remote users handled? what if they aren't readers?
|
# TODO: how are remote users handled? what if they aren't readers?
|
||||||
if not created:
|
if not instance.local or not created:
|
||||||
return
|
return
|
||||||
|
|
||||||
shelves = [{
|
shelves = [{
|
||||||
'name': 'To Read',
|
'name': 'To Read',
|
||||||
'type': 'to-read',
|
'type': 'to-read',
|
||||||
|
@ -62,14 +69,58 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
class Activity(models.Model):
|
||||||
''' any kind of user post, incl. reviews, replies, and status updates '''
|
''' basic fields for storing activities '''
|
||||||
|
uuid = models.CharField(max_length=255, unique=True)
|
||||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||||
content = JSONField(max_length=5000)
|
content = JSONField(max_length=5000)
|
||||||
|
activity_type = models.CharField(max_length=255)
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ShelveActivity(Activity):
|
||||||
|
''' someone put a book on a shelf '''
|
||||||
|
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
||||||
|
shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
|
||||||
|
class FollowActivity(Activity):
|
||||||
|
''' record follow requests sent out '''
|
||||||
|
followed = models.ForeignKey(
|
||||||
|
'User',
|
||||||
|
related_name='followed',
|
||||||
|
on_delete=models.PROTECT
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.activity_type:
|
||||||
|
self.activity_type = 'Follow'
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Review(Activity):
|
||||||
|
''' a book review '''
|
||||||
|
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
||||||
|
work = models.ForeignKey('Work', on_delete=models.PROTECT)
|
||||||
|
name = models.TextField()
|
||||||
|
rating = models.IntegerField(default=0)
|
||||||
|
review_content = models.TextField()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.activity_type:
|
||||||
|
self.activity_type = 'Article'
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Note(Activity):
|
||||||
|
''' reply to a review, etc '''
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.activity_type:
|
||||||
|
self.activity_type = 'Note'
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Shelf(models.Model):
|
class Shelf(models.Model):
|
||||||
activitypub_id = models.CharField(max_length=255)
|
activitypub_id = models.CharField(max_length=255)
|
||||||
identifier = models.CharField(max_length=255)
|
identifier = models.CharField(max_length=255)
|
||||||
|
@ -96,7 +147,8 @@ class Shelf(models.Model):
|
||||||
re.sub(r'\W', '-', self.name).lower()
|
re.sub(r'\W', '-', self.name).lower()
|
||||||
)
|
)
|
||||||
if not self.activitypub_id:
|
if not self.activitypub_id:
|
||||||
self.activitypub_id = 'https://%s/shelf/%s' % (DOMAIN, self.identifier)
|
self.activitypub_id = 'https://%s/shelf/%s' % \
|
||||||
|
(DOMAIN, self.identifier)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,6 +163,7 @@ class ShelfBook(models.Model):
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
added_date = models.DateTimeField(auto_now_add=True)
|
added_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('book', 'shelf')
|
unique_together = ('book', 'shelf')
|
||||||
|
|
||||||
|
@ -118,7 +171,7 @@ class ShelfBook(models.Model):
|
||||||
class Book(models.Model):
|
class Book(models.Model):
|
||||||
''' a non-canonical copy from open library '''
|
''' a non-canonical copy from open library '''
|
||||||
activitypub_id = models.CharField(max_length=255)
|
activitypub_id = models.CharField(max_length=255)
|
||||||
openlibary_key = models.CharField(max_length=255)
|
openlibrary_key = models.CharField(max_length=255)
|
||||||
data = JSONField()
|
data = JSONField()
|
||||||
works = models.ManyToManyField('Work')
|
works = models.ManyToManyField('Work')
|
||||||
authors = models.ManyToManyField('Author')
|
authors = models.ManyToManyField('Author')
|
||||||
|
@ -138,19 +191,20 @@ class Book(models.Model):
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.activitypub_id = '%s%s' % (OL_URL, self.openlibary_key)
|
self.activitypub_id = '%s%s' % (OL_URL, self.openlibrary_key)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Work(models.Model):
|
class Work(models.Model):
|
||||||
''' encompassses all editions of a book '''
|
''' encompassses all editions of a book '''
|
||||||
openlibary_key = models.CharField(max_length=255)
|
openlibrary_key = models.CharField(max_length=255)
|
||||||
data = JSONField()
|
data = JSONField()
|
||||||
added_date = models.DateTimeField(auto_now_add=True)
|
added_date = models.DateTimeField(auto_now_add=True)
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
class Author(models.Model):
|
class Author(models.Model):
|
||||||
openlibary_key = models.CharField(max_length=255)
|
openlibrary_key = models.CharField(max_length=255)
|
||||||
data = JSONField()
|
data = JSONField()
|
||||||
added_date = models.DateTimeField(auto_now_add=True)
|
added_date = models.DateTimeField(auto_now_add=True)
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
|
|
|
@ -12,11 +12,11 @@ def get_or_create_book(olkey, user=None, update=True):
|
||||||
|
|
||||||
# get the existing entry from our db, if it exists
|
# get the existing entry from our db, if it exists
|
||||||
try:
|
try:
|
||||||
book = Book.objects.get(openlibary_key=olkey)
|
book = Book.objects.get(openlibrary_key=olkey)
|
||||||
if not update:
|
if not update:
|
||||||
return book
|
return book
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
book = Book(openlibary_key=olkey)
|
book = Book(openlibrary_key=olkey)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
book.data = data
|
book.data = data
|
||||||
if user and user.is_authenticated:
|
if user and user.is_authenticated:
|
||||||
|
@ -33,22 +33,22 @@ def get_or_create_book(olkey, user=None, update=True):
|
||||||
def get_or_create_work(olkey):
|
def get_or_create_work(olkey):
|
||||||
''' load em up '''
|
''' load em up '''
|
||||||
try:
|
try:
|
||||||
work = Work.objects.get(openlibary_key=olkey)
|
work = Work.objects.get(openlibrary_key=olkey)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
response = requests.get(OL_URL + olkey + '.json')
|
response = requests.get(OL_URL + olkey + '.json')
|
||||||
data = response.json()
|
data = response.json()
|
||||||
work = Work(openlibary_key=olkey, data=data)
|
work = Work(openlibrary_key=olkey, data=data)
|
||||||
work.save()
|
work.save()
|
||||||
return work
|
return work
|
||||||
|
|
||||||
def get_or_create_author(olkey):
|
def get_or_create_author(olkey):
|
||||||
''' load that author '''
|
''' load that author '''
|
||||||
try:
|
try:
|
||||||
author = Author.objects.get(openlibary_key=olkey)
|
author = Author.objects.get(openlibrary_key=olkey)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
response = requests.get(OL_URL + olkey + '.json')
|
response = requests.get(OL_URL + olkey + '.json')
|
||||||
data = response.json()
|
data = response.json()
|
||||||
author = Author(openlibary_key=olkey, data=data)
|
author = Author(openlibrary_key=olkey, data=data)
|
||||||
author.save()
|
author.save()
|
||||||
return author
|
return author
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,9 @@ SECRET_KEY = '7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr'
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['localhost', 'ff2cb3e9.ngrok.io']
|
|
||||||
|
|
||||||
DOMAIN = 'ff2cb3e9.ngrok.io'
|
DOMAIN = 'bd352ee8.ngrok.io'
|
||||||
|
ALLOWED_HOSTS = ['localhost', DOMAIN]
|
||||||
OL_URL = 'https://openlibrary.org'
|
OL_URL = 'https://openlibrary.org'
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
27
fedireads/templates/book.html
Normal file
27
fedireads/templates/book.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div id="main">
|
||||||
|
<div class="book-profile">
|
||||||
|
<img class="book-cover" src="/static/images/med.jpg">
|
||||||
|
<h1>{{ book.data.title }}</h1>
|
||||||
|
by {{ book.authors.first.data.name }}
|
||||||
|
{{ rating }} stars
|
||||||
|
</div>
|
||||||
|
<form name="review" action="/review/" method="post">
|
||||||
|
<input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input>
|
||||||
|
<input type="text" name="name"></input>
|
||||||
|
<textarea name="content">Your review</textarea>
|
||||||
|
<input type="number" name="rating"></input>
|
||||||
|
<input type="submit" value="Post review"></input>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="reviews">
|
||||||
|
<h2>Reviews</h2>
|
||||||
|
{% for review in reviews %}
|
||||||
|
<p><span class="review-title">{{ review.name }}</span>{{ review.rating }} stars, by {{ review.user.username }}</p>
|
||||||
|
<p>{{ review.review_content }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
{% for book in shelf.books.all %}
|
{% for book in shelf.books.all %}
|
||||||
<div class="book-preview">
|
<div class="book-preview">
|
||||||
<img class="cover" src="static/images/small.jpg">
|
<img class="cover" src="static/images/small.jpg">
|
||||||
<p class="title">{{ book.data.title }}</p>
|
<p class="title"><a href="{{ book.openlibrary_key }}">{{ book.data.title }}</a></p>
|
||||||
<p>by <a href="" class="author">{{ book.authors.first.data.name }}</a></p>
|
<p>by <a href="" class="author">{{ book.authors.first.data.name }}</a></p>
|
||||||
{% if shelf.type == 'reading' %}
|
{% if shelf.type == 'reading' %}
|
||||||
<button>done reading</button>
|
<button>done reading</button>
|
||||||
|
@ -21,7 +21,9 @@
|
||||||
{% for book in recent_books %}
|
{% for book in recent_books %}
|
||||||
<div class="book-preview">
|
<div class="book-preview">
|
||||||
<img class="cover" src="static/images/small.jpg">
|
<img class="cover" src="static/images/small.jpg">
|
||||||
<p class="title">{{ book.data.title }}</p>
|
<p class="title">
|
||||||
|
<a href="{{ book.openlibrary_key }}">{{ book.data.title }}</a>
|
||||||
|
</p>
|
||||||
<p>by <a href="" class="author">{{ book.authors.first.data.name }}</a></p>
|
<p>by <a href="" class="author">{{ book.authors.first.data.name }}</a></p>
|
||||||
{% if not book.user_shelves %}
|
{% if not book.user_shelves %}
|
||||||
<form name="shelve" action="/shelve/{{ request.user.username }}_to-read/{{ book.id }}" method="post">
|
<form name="shelve" action="/shelve/{{ request.user.username }}_to-read/{{ book.id }}" method="post">
|
||||||
|
@ -32,35 +34,13 @@
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% for activity in activities %}
|
||||||
<div class="update">
|
<div class="update">
|
||||||
<div class="user-preview">
|
<div class="user-preview">
|
||||||
<img class="user-pic" src="static/images/profile.jpg">
|
<img class="user-pic" src="static/images/profile.jpg">
|
||||||
<span><a href="" class="user">Mouse</a> is currently reading</span>
|
<span><a href="" class="user">Mouse</a> did {{ activity.activity_type }} </span>
|
||||||
</div>
|
|
||||||
<div class="book-preview">
|
|
||||||
<img class="cover" src="static/images/med.jpg">
|
|
||||||
<p class="title">Moby Dick</p>
|
|
||||||
<p>by <a href="" class="author">Herman Melville</a></p>
|
|
||||||
<p>"Command the murderous chalices! Drink ye harpooners! Drink and swear, ye men that man the deathful whaleboat's bow -- Death to Moby Dick!" So Captain Ahab binds his crew to fulfil his obsession -- the destruction of the great white whale. Under his lordly but maniacal command the Pequod's commercial mission is perverted to one of vengeance...</p>
|
|
||||||
</div>
|
|
||||||
<div class="interact">
|
|
||||||
<span>⭐️ Like</span>
|
|
||||||
<span>💬 <input type="text"></input></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="update">
|
|
||||||
<img class="user-pic" src="static/images/profile.jpg">
|
|
||||||
<span><a href="" class="user">Mouse</a> is currently reading</span>
|
|
||||||
<div class="book-preview">
|
|
||||||
<img class="cover" src="static/images/med.jpg">
|
|
||||||
<p class="title">Moby Dick</p>
|
|
||||||
<p>by <a href="" class="author">Herman Melville</a></p>
|
|
||||||
<p>"Command the murderous chalices! Drink ye harpooners! Drink and swear, ye men that man the deathful whaleboat's bow -- Death to Moby Dick!" So Captain Ahab binds his crew to fulfil his obsession -- the destruction of the great white whale. Under his lordly but maniacal command the Pequod's commercial mission is perverted to one of vengeance...</p>
|
|
||||||
</div>
|
|
||||||
<div class="interact">
|
|
||||||
<span>⭐️ Like</span>
|
|
||||||
<span>💬 <input type="text"></input></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -23,6 +23,8 @@ urlpatterns = [
|
||||||
path('login/', views.user_login),
|
path('login/', views.user_login),
|
||||||
path('logout/', views.user_logout),
|
path('logout/', views.user_logout),
|
||||||
path('user/<str:username>', views.user_profile),
|
path('user/<str:username>', views.user_profile),
|
||||||
|
path('book/<str:book_identifier>', views.book_page),
|
||||||
|
path('review/', views.review),
|
||||||
path('shelve/<str:shelf_id>/<int:book_id>', views.shelve),
|
path('shelve/<str:shelf_id>/<int:book_id>', views.shelve),
|
||||||
path('follow/', views.follow),
|
path('follow/', views.follow),
|
||||||
path('unfollow/', views.unfollow),
|
path('unfollow/', views.unfollow),
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
''' application views/pages '''
|
''' application views/pages '''
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from django.db.models import FilteredRelation, Q
|
from django.db.models import Avg, FilteredRelation, Q
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from fedireads import models
|
from fedireads import models, openlibrary
|
||||||
import fedireads.activitypub_templates as templates
|
from fedireads import federation as api
|
||||||
from fedireads.federation import broadcast_activity, broadcast_follow
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def home(request):
|
def home(request):
|
||||||
|
@ -20,14 +19,24 @@ def home(request):
|
||||||
'shelves',
|
'shelves',
|
||||||
condition=Q(shelves__user_id=request.user.id)
|
condition=Q(shelves__user_id=request.user.id)
|
||||||
)
|
)
|
||||||
).values('id', 'authors', 'data', 'user_shelves')
|
).values('id', 'authors', 'data', 'user_shelves', 'openlibrary_key')
|
||||||
|
|
||||||
|
following = models.User.objects.filter(
|
||||||
|
Q(followers=request.user) | Q(id=request.user.id))
|
||||||
|
|
||||||
|
activities = models.Activity.objects.filter(
|
||||||
|
user__in=following
|
||||||
|
).order_by('-created_date')[:10]
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'user': request.user,
|
'user': request.user,
|
||||||
'shelves': shelves,
|
'shelves': shelves,
|
||||||
'recent_books': recent_books,
|
'recent_books': recent_books,
|
||||||
|
'activities': activities,
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, 'feed.html', data)
|
return TemplateResponse(request, 'feed.html', data)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def user_login(request):
|
def user_login(request):
|
||||||
''' authentication '''
|
''' authentication '''
|
||||||
|
@ -44,6 +53,7 @@ def user_login(request):
|
||||||
return redirect(request.GET.get('next', '/'))
|
return redirect(request.GET.get('next', '/'))
|
||||||
return TemplateResponse(request, 'login.html')
|
return TemplateResponse(request, 'login.html')
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@login_required
|
@login_required
|
||||||
def user_logout(request):
|
def user_logout(request):
|
||||||
|
@ -65,30 +75,43 @@ def user_profile(request, username):
|
||||||
return TemplateResponse(request, 'user.html', data)
|
return TemplateResponse(request, 'user.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def book_page(request, book_identifier):
|
||||||
|
''' info about a book '''
|
||||||
|
book = openlibrary.get_or_create_book('/book/' + book_identifier)
|
||||||
|
reviews = models.Review.objects.filter(
|
||||||
|
Q(work=book.works.first()) | Q(book=book)
|
||||||
|
)
|
||||||
|
rating = reviews.aggregate(Avg('rating'))
|
||||||
|
data = {
|
||||||
|
'book': book,
|
||||||
|
'reviews': reviews,
|
||||||
|
'rating': rating['rating__avg'],
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'book.html', data)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@login_required
|
@login_required
|
||||||
def shelve(request, shelf_id, book_id):
|
def shelve(request, shelf_id, book_id):
|
||||||
''' put a book on a user's shelf '''
|
''' put a book on a user's shelf '''
|
||||||
book = models.Book.objects.get(id=book_id)
|
book = models.Book.objects.get(id=book_id)
|
||||||
shelf = models.Shelf.objects.get(identifier=shelf_id)
|
shelf = models.Shelf.objects.get(identifier=shelf_id)
|
||||||
|
api.handle_shelve(request.user, book, shelf)
|
||||||
# update the database
|
|
||||||
models.ShelfBook(book=book, shelf=shelf, added_by=request.user).save()
|
|
||||||
|
|
||||||
# send out the activitypub action
|
|
||||||
summary = '%s marked %s as %s' % (
|
|
||||||
request.user.username,
|
|
||||||
book.data['title'],
|
|
||||||
shelf.name
|
|
||||||
)
|
|
||||||
|
|
||||||
obj = templates.note_object(request.user, summary)
|
|
||||||
#activity = templates.shelve_activity(request.user, book, shelf)
|
|
||||||
recipients = [templates.inbox(u) for u in request.user.followers.all()]
|
|
||||||
broadcast_activity(request.user, obj, recipients)
|
|
||||||
|
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@login_required
|
||||||
|
def review(request):
|
||||||
|
''' create a book review note '''
|
||||||
|
book_identifier = request.POST.get('book')
|
||||||
|
book = openlibrary.get_or_create_book(book_identifier)
|
||||||
|
name = request.POST.get('name')
|
||||||
|
content = request.POST.get('content')
|
||||||
|
rating = request.POST.get('rating')
|
||||||
|
api.handle_review(request.user, book, name, content, rating)
|
||||||
|
return redirect(book_identifier)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -97,12 +120,10 @@ def follow(request):
|
||||||
to_follow = request.POST.get('user')
|
to_follow = request.POST.get('user')
|
||||||
to_follow = models.User.objects.get(id=to_follow)
|
to_follow = models.User.objects.get(id=to_follow)
|
||||||
|
|
||||||
activity = templates.follow_request(request.user, to_follow.actor)
|
api.handle_outgoing_follow(request.user, to_follow)
|
||||||
broadcast_follow(request.user, activity, templates.inbox(to_follow))
|
|
||||||
return redirect('/user/%s' % to_follow.username)
|
return redirect('/user/%s' % to_follow.username)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@login_required
|
@login_required
|
||||||
def unfollow(request):
|
def unfollow(request):
|
||||||
|
|
Loading…
Reference in a new issue