Adds commenting

works on #59
This commit is contained in:
Mouse Reeve 2020-03-21 16:50:49 -07:00
parent 3f96c8cd9d
commit 7862af9729
16 changed files with 222 additions and 31 deletions

View file

@ -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

View file

@ -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

View file

@ -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']

View file

@ -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(

View 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',),
),
]

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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",

View file

@ -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)

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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),

View file

@ -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('/')

View file

@ -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)