forked from mirrors/bookwyrm
parent
3f96c8cd9d
commit
7862af9729
16 changed files with 222 additions and 31 deletions
|
@ -1,8 +1,12 @@
|
|||
''' bring activitypub functions into the namespace '''
|
||||
from .actor import get_actor
|
||||
from .collection import get_outbox, get_outbox_page, get_add, get_remove, \
|
||||
get_following, get_followers
|
||||
from .collection import get_outbox, get_outbox_page
|
||||
from .collection import get_add, get_remove
|
||||
from .collection import get_following, get_followers
|
||||
from .create import get_create
|
||||
from .follow import get_follow_request, get_unfollow, get_accept, get_reject
|
||||
from .status import get_review, get_review_article, get_status, get_replies, \
|
||||
get_favorite, get_unfavorite, get_add_tag, get_remove_tag, get_replies_page
|
||||
from .status import get_review, get_review_article
|
||||
from .status import get_comment, get_comment_article
|
||||
from .status import get_status, get_replies, get_replies_page
|
||||
from .status import get_favorite, get_unfavorite
|
||||
from .status import get_add_tag, get_remove_tag
|
||||
|
|
|
@ -12,6 +12,15 @@ def get_review(review):
|
|||
return status
|
||||
|
||||
|
||||
def get_comment(comment):
|
||||
''' fedireads json for book reviews '''
|
||||
status = get_status(comment)
|
||||
status['inReplyToBook'] = comment.book.absolute_id
|
||||
status['fedireadsType'] = comment.status_type
|
||||
status['name'] = comment.name
|
||||
return status
|
||||
|
||||
|
||||
def get_review_article(review):
|
||||
''' a book review formatted for a non-fedireads isntance (mastodon) '''
|
||||
status = get_status(review)
|
||||
|
@ -24,6 +33,17 @@ def get_review_article(review):
|
|||
return status
|
||||
|
||||
|
||||
def get_comment_article(comment):
|
||||
''' a book comment formatted for a non-fedireads isntance (mastodon) '''
|
||||
status = get_status(comment)
|
||||
name = '%s (comment on "%s")' % (
|
||||
comment.name,
|
||||
comment.book.title
|
||||
)
|
||||
status['name'] = name
|
||||
return status
|
||||
|
||||
|
||||
def get_status(status):
|
||||
''' create activitypub json for a status '''
|
||||
user = status.user
|
||||
|
|
|
@ -41,6 +41,17 @@ class ReviewForm(ModelForm):
|
|||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
class Meta:
|
||||
model = models.Comment
|
||||
fields = ['name', 'content']
|
||||
help_texts = {f: None for f in fields}
|
||||
labels = {
|
||||
'name': 'Title',
|
||||
'content': 'Comment',
|
||||
}
|
||||
|
||||
|
||||
class ReplyForm(ModelForm):
|
||||
class Meta:
|
||||
model = models.Status
|
||||
fields = ['content']
|
||||
|
|
|
@ -236,6 +236,19 @@ def handle_incoming_create(activity):
|
|||
)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest()
|
||||
elif activity['object'].get('fedireadsType') == 'Comment' and \
|
||||
'inReplyToBook' in activity['object']:
|
||||
if user.local:
|
||||
comment_id = activity['object']['id'].split('/')[-1]
|
||||
models.Comment.objects.get(id=comment_id)
|
||||
else:
|
||||
try:
|
||||
status_builder.create_comment_from_activity(
|
||||
user,
|
||||
activity['object']
|
||||
)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest()
|
||||
elif not user.local:
|
||||
try:
|
||||
status = status_builder.create_status_from_activity(
|
||||
|
|
26
fedireads/migrations/0019_comment.py
Normal file
26
fedireads/migrations/0019_comment.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-21 22:43
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fedireads', '0018_favorite_remote_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('status_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fedireads.Status')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('fedireads.status',),
|
||||
),
|
||||
]
|
|
@ -1,5 +1,6 @@
|
|||
''' bring all the models into the app namespace '''
|
||||
from .book import Book, Work, Edition, Author
|
||||
from .shelf import Shelf, ShelfBook
|
||||
from .status import Status, Review, Favorite, Tag, Notification
|
||||
from .user import User, FederatedServer, UserFollows, UserFollowRequest, UserBlocks
|
||||
from .status import Status, Review, Comment, Favorite, Tag, Notification
|
||||
from .user import User, FederatedServer, UserFollows, UserFollowRequest, \
|
||||
UserBlocks
|
||||
|
|
|
@ -46,6 +46,17 @@ class Status(FedireadsModel):
|
|||
return '%s/%s/%d' % (base_path, model_name, self.id)
|
||||
|
||||
|
||||
class Comment(Status):
|
||||
''' like a review but without a rating and transient '''
|
||||
name = models.CharField(max_length=255)
|
||||
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.status_type = 'Comment'
|
||||
self.activity_type = 'Article'
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Review(Status):
|
||||
''' a book review '''
|
||||
name = models.CharField(max_length=255)
|
||||
|
|
|
@ -8,7 +8,7 @@ from urllib.parse import urlencode
|
|||
from fedireads import activitypub
|
||||
from fedireads import models
|
||||
from fedireads.status import create_review, create_status, create_tag, \
|
||||
create_notification
|
||||
create_notification, create_comment
|
||||
from fedireads.remote_user import get_or_create_remote_user
|
||||
from fedireads.broadcast import get_recipients, broadcast
|
||||
|
||||
|
@ -175,6 +175,24 @@ def handle_review(user, book, name, content, rating):
|
|||
broadcast(user, article_create_activity, other_recipients)
|
||||
|
||||
|
||||
def handle_comment(user, book, name, content):
|
||||
''' post a review '''
|
||||
# validated and saves the review in the database so it has an id
|
||||
comment = create_comment(user, book, name, content)
|
||||
|
||||
comment_activity = activitypub.get_comment(comment)
|
||||
comment_create_activity = activitypub.get_create(user, comment_activity)
|
||||
fr_recipients = get_recipients(user, 'public', limit='fedireads')
|
||||
broadcast(user, comment_create_activity, fr_recipients)
|
||||
|
||||
# re-format the activity for non-fedireads servers
|
||||
article_activity = activitypub.get_comment_article(comment)
|
||||
article_create_activity = activitypub.get_create(user, article_activity)
|
||||
|
||||
other_recipients = get_recipients(user, 'public', limit='other')
|
||||
broadcast(user, article_create_activity, other_recipients)
|
||||
|
||||
|
||||
def handle_tag(user, book, name):
|
||||
''' tag a book '''
|
||||
tag = create_tag(user, book, name)
|
||||
|
@ -195,19 +213,19 @@ def handle_untag(user, book, name):
|
|||
broadcast(user, tag_activity, recipients)
|
||||
|
||||
|
||||
def handle_comment(user, review, content):
|
||||
def handle_reply(user, review, content):
|
||||
''' respond to a review or status '''
|
||||
# validated and saves the comment in the database so it has an id
|
||||
comment = create_status(user, content, reply_parent=review)
|
||||
if comment.reply_parent:
|
||||
reply = create_status(user, content, reply_parent=review)
|
||||
if reply.reply_parent:
|
||||
create_notification(
|
||||
comment.reply_parent.user,
|
||||
reply.reply_parent.user,
|
||||
'REPLY',
|
||||
related_user=user,
|
||||
related_status=comment,
|
||||
related_status=reply,
|
||||
)
|
||||
comment_activity = activitypub.get_status(comment)
|
||||
create_activity = activitypub.get_create(user, comment_activity)
|
||||
reply_activity = activitypub.get_status(reply)
|
||||
create_activity = activitypub.get_create(user, reply_activity)
|
||||
|
||||
recipients = get_recipients(user, 'public')
|
||||
broadcast(user, create_activity, recipients)
|
||||
|
|
|
@ -18,13 +18,39 @@ function interact(e) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function comment(e) {
|
||||
function reply(e) {
|
||||
e.preventDefault();
|
||||
ajaxPost(e.target);
|
||||
// TODO: display comment
|
||||
return true;
|
||||
}
|
||||
|
||||
function tabChange(e) {
|
||||
e.preventDefault();
|
||||
var target = e.target.parentElement;
|
||||
var identifier = target.getAttribute('data-id');
|
||||
|
||||
var options_class = target.getAttribute('data-category');
|
||||
var options = document.getElementsByClassName(options_class);
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
if (!options[i].className.includes('hidden')) {
|
||||
options[i].className += ' hidden';
|
||||
}
|
||||
}
|
||||
|
||||
var tabs = target.parentElement.children;
|
||||
for (i = 0; i < tabs.length; i++) {
|
||||
if (tabs[i].getAttribute('data-id') == identifier) {
|
||||
tabs[i].className += ' active';
|
||||
} else {
|
||||
tabs[i].className = tabs[i].className.replace('active', '');
|
||||
}
|
||||
}
|
||||
|
||||
var el = document.getElementById(identifier);
|
||||
el.className = el.className.replace('hidden', '');
|
||||
}
|
||||
|
||||
function ajaxPost(form) {
|
||||
fetch(form.action, {
|
||||
method : "POST",
|
||||
|
|
|
@ -41,6 +41,36 @@ def create_review(user, possible_book, name, content, rating):
|
|||
)
|
||||
|
||||
|
||||
def create_comment_from_activity(author, activity):
|
||||
''' parse an activity json blob into a status '''
|
||||
book = activity['inReplyToBook']
|
||||
book = book.split('/')[-1]
|
||||
name = activity.get('name')
|
||||
content = activity.get('content')
|
||||
published = activity.get('published')
|
||||
remote_id = activity['id']
|
||||
|
||||
comment = create_comment(author, book, name, content, rating)
|
||||
comment.published_date = published
|
||||
comment.remote_id = remote_id
|
||||
comment.save()
|
||||
return comment
|
||||
|
||||
|
||||
def create_comment(user, possible_book, name, content):
|
||||
''' a book comment has been added '''
|
||||
# throws a value error if the book is not found
|
||||
book = get_or_create_book(possible_book)
|
||||
content = sanitize(content)
|
||||
|
||||
return models.Comment.objects.create(
|
||||
user=user,
|
||||
book=book,
|
||||
name=name,
|
||||
content=content,
|
||||
)
|
||||
|
||||
|
||||
def create_status_from_activity(author, activity):
|
||||
''' parse a status object out of an activity json blob '''
|
||||
content = activity.get('content')
|
||||
|
@ -58,6 +88,7 @@ def create_status_from_activity(author, activity):
|
|||
|
||||
|
||||
def create_favorite_from_activity(user, activity):
|
||||
''' create a new favorite entry '''
|
||||
status = get_status(activity['object'])
|
||||
remote_id = activity['id']
|
||||
try:
|
||||
|
@ -81,6 +112,7 @@ def get_favorite(absolute_id):
|
|||
|
||||
|
||||
def get_by_absolute_id(absolute_id, model):
|
||||
''' generalized function to get from a model with a remote_id field '''
|
||||
# check if it's a remote status
|
||||
try:
|
||||
return model.objects.get(remote_id=absolute_id)
|
||||
|
|
|
@ -9,24 +9,32 @@
|
|||
</h2>
|
||||
|
||||
<div class="tabs secondary">
|
||||
<div class="tab active">
|
||||
Review
|
||||
<div class="tab active" data-id="tab-review-{{ book.id }}" data-category="tab-option-{{ book.id }}">
|
||||
<a href="{{ book.absolute_id }}/review" onclick="tabChange(event)">Review</a>
|
||||
</div>
|
||||
<div class="tab">
|
||||
Comment
|
||||
<div class="tab" data-id="tab-comment-{{ book.id }}" data-category="tab-option-{{ book.id }}">
|
||||
<a href="{{ book.absolute_id }}/comment" onclick="tabChange(event)">Comment</a>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<div class="tab" data-id="tab-quote-{{ book.id }}" data-category="tab-option-{{ book.id }}">
|
||||
Quote
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="book-preview">
|
||||
{% include 'snippets/book_cover.html' with book=book %}
|
||||
<form class="review-form" name="review" action="/review/" method="post">
|
||||
<form class="tab-option-{{ book.id }} review-form" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}">
|
||||
{% csrf_token %}
|
||||
{# TODO: this shouldn't use the openlibrary key #}
|
||||
{# todo: this shouldn't use the openlibrary key #}
|
||||
<input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input>
|
||||
{{ review_form.as_p }}
|
||||
<button type="submit">Post review</button>
|
||||
<button type="submit">post review</button>
|
||||
</form>
|
||||
|
||||
<form class="hidden tab-option-{{ book.id }} review-form" name="comment" action="/comment/" method="post" id="tab-comment-{{ book.id }}">
|
||||
{% csrf_token %}
|
||||
{# todo: this shouldn't use the openlibrary key #}
|
||||
<input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input>
|
||||
{{ comment_form.as_p }}
|
||||
<button type="submit">post comment</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{% load fr_display %}
|
||||
<div class="interaction">
|
||||
|
||||
<form name="comment" action="/comment" method="post" onsubmit="return comment(e_">
|
||||
<form name="reply" action="/reply" method="post" onsubmit="return reply(event)">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="parent" value="{{ activity.id }}"></input>
|
||||
<textarea name="content" placeholder="Leave a comment..." id="id_content" required="true"></textarea>
|
||||
<button type="submit" class="comment">
|
||||
<button type="submit">
|
||||
<span class="icon icon-comment">
|
||||
<span class="hidden-text">Comment</span>
|
||||
</span>
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
{{ status.content | safe }}
|
||||
{% elif status.status_type == 'Review' %}
|
||||
reviewed {{ status.book.title }}
|
||||
{% elif status.status_type == 'Comment' %}
|
||||
commented on {{ status.book.title }}
|
||||
{% elif status.reply_parent %}
|
||||
{% with parent_status=status|parent %}
|
||||
replied to {% include 'snippets/username.html' with user=parent_status.user possessive=True %} <a href="{{parent_status.absolute_id }}">{{ parent_status.status_type|lower }}</a>
|
||||
|
|
|
@ -10,7 +10,7 @@ username_regex = r'(?P<username>[\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)/(?P<status_id>\d+)' % local_user_path
|
||||
status_path = r'%s/(status|review|comment)/(?P<status_id>\d+)' % local_user_path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
|
@ -65,9 +65,10 @@ urlpatterns = [
|
|||
re_path(r'^user-login/?$', actions.user_login),
|
||||
re_path(r'^register/?$', actions.register),
|
||||
re_path(r'^review/?$', actions.review),
|
||||
re_path(r'^comment/?$', actions.comment),
|
||||
re_path(r'^tag/?$', actions.tag),
|
||||
re_path(r'^untag/?$', actions.untag),
|
||||
re_path(r'^comment/?$', actions.comment),
|
||||
re_path(r'^reply/?$', actions.reply),
|
||||
re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite),
|
||||
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
|
||||
re_path(r'^shelve/?$', actions.shelve),
|
||||
|
|
|
@ -100,7 +100,7 @@ def shelve(request):
|
|||
|
||||
@login_required
|
||||
def review(request):
|
||||
''' create a book review note '''
|
||||
''' create a book review '''
|
||||
form = forms.ReviewForm(request.POST)
|
||||
book_identifier = request.POST.get('book')
|
||||
# TODO: better failure behavior
|
||||
|
@ -116,6 +116,23 @@ def review(request):
|
|||
return redirect('/book/%s' % book_identifier)
|
||||
|
||||
|
||||
@login_required
|
||||
def comment(request):
|
||||
''' create a book comment '''
|
||||
form = forms.CommentForm(request.POST)
|
||||
book_identifier = request.POST.get('book')
|
||||
# TODO: better failure behavior
|
||||
if not form.is_valid():
|
||||
return redirect('/book/%s' % book_identifier)
|
||||
|
||||
# TODO: validation, htmlification
|
||||
name = form.data.get('name')
|
||||
content = form.data.get('content')
|
||||
|
||||
outgoing.handle_comment(request.user, book_identifier, name, content)
|
||||
return redirect('/book/%s' % book_identifier)
|
||||
|
||||
|
||||
@login_required
|
||||
def tag(request):
|
||||
''' tag a book '''
|
||||
|
@ -139,15 +156,15 @@ def untag(request):
|
|||
|
||||
|
||||
@login_required
|
||||
def comment(request):
|
||||
def reply(request):
|
||||
''' respond to a book review '''
|
||||
form = forms.CommentForm(request.POST)
|
||||
form = forms.ReplyForm(request.POST)
|
||||
# this is a bit of a formality, the form is just one text field
|
||||
if not form.is_valid():
|
||||
return redirect('/')
|
||||
parent_id = request.POST['parent']
|
||||
parent = models.Status.objects.get(id=parent_id)
|
||||
outgoing.handle_comment(request.user, parent, form.data['content'])
|
||||
outgoing.handle_reply(request.user, parent, form.data['content'])
|
||||
return redirect('/')
|
||||
|
||||
|
||||
|
|
|
@ -91,6 +91,7 @@ def home_tab(request, tab):
|
|||
],
|
||||
'active_tab': tab,
|
||||
'review_form': forms.ReviewForm(),
|
||||
'comment_form': forms.CommentForm(),
|
||||
}
|
||||
return TemplateResponse(request, 'feed.html', data)
|
||||
|
||||
|
|
Loading…
Reference in a new issue