diff --git a/fedireads/activitypub/__init__.py b/fedireads/activitypub/__init__.py index a7ed5b946..04a378bcb 100644 --- a/fedireads/activitypub/__init__.py +++ b/fedireads/activitypub/__init__.py @@ -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 diff --git a/fedireads/activitypub/status.py b/fedireads/activitypub/status.py index 3d2c30720..8eff0194f 100644 --- a/fedireads/activitypub/status.py +++ b/fedireads/activitypub/status.py @@ -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 diff --git a/fedireads/forms.py b/fedireads/forms.py index 7a69fadc7..e812fc0e6 100644 --- a/fedireads/forms.py +++ b/fedireads/forms.py @@ -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'] diff --git a/fedireads/incoming.py b/fedireads/incoming.py index 6fbe5da8b..0aaff5424 100644 --- a/fedireads/incoming.py +++ b/fedireads/incoming.py @@ -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( diff --git a/fedireads/migrations/0019_comment.py b/fedireads/migrations/0019_comment.py new file mode 100644 index 000000000..5a88c6989 --- /dev/null +++ b/fedireads/migrations/0019_comment.py @@ -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',), + ), + ] diff --git a/fedireads/models/__init__.py b/fedireads/models/__init__.py index 61b18c413..0902e38be 100644 --- a/fedireads/models/__init__.py +++ b/fedireads/models/__init__.py @@ -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 diff --git a/fedireads/models/status.py b/fedireads/models/status.py index 1229db3b4..cc2ae0f3b 100644 --- a/fedireads/models/status.py +++ b/fedireads/models/status.py @@ -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) diff --git a/fedireads/outgoing.py b/fedireads/outgoing.py index 250779aaf..78eea77b9 100644 --- a/fedireads/outgoing.py +++ b/fedireads/outgoing.py @@ -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) diff --git a/fedireads/static/js/shared.js b/fedireads/static/js/shared.js index 1fad60e5a..87c3a1239 100644 --- a/fedireads/static/js/shared.js +++ b/fedireads/static/js/shared.js @@ -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", diff --git a/fedireads/status.py b/fedireads/status.py index c00fcd821..29185da42 100644 --- a/fedireads/status.py +++ b/fedireads/status.py @@ -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) diff --git a/fedireads/templates/snippets/create_status.html b/fedireads/templates/snippets/create_status.html index 6fbb1f2ed..8206e768f 100644 --- a/fedireads/templates/snippets/create_status.html +++ b/fedireads/templates/snippets/create_status.html @@ -9,24 +9,32 @@
-
- Review +
+ Review
-
- Comment + -
+
Quote
{% include 'snippets/book_cover.html' with book=book %} -
+ {% csrf_token %} - {# TODO: this shouldn't use the openlibrary key #} + {# todo: this shouldn't use the openlibrary key #} {{ review_form.as_p }} - + +
+ +
diff --git a/fedireads/templates/snippets/interaction.html b/fedireads/templates/snippets/interaction.html index d56afe0ad..c7a92ca7d 100644 --- a/fedireads/templates/snippets/interaction.html +++ b/fedireads/templates/snippets/interaction.html @@ -1,11 +1,11 @@ {% load fr_display %}
-
+ {% csrf_token %} -