diff --git a/fedireads/activitypub/__init__.py b/fedireads/activitypub/__init__.py index 22fd6afd0..b74c645e6 100644 --- a/fedireads/activitypub/__init__.py +++ b/fedireads/activitypub/__init__.py @@ -5,4 +5,4 @@ from .collection import get_outbox, get_outbox_page, get_add, get_remove, \ from .create import get_create from .follow import get_follow_request, get_unfollow, get_accept from .status import get_review, get_review_article, get_status, get_replies, \ - get_favorite + get_favorite, get_add_tag, get_remove_tag diff --git a/fedireads/activitypub/collection.py b/fedireads/activitypub/collection.py index 426553b9c..3c7019b43 100644 --- a/fedireads/activitypub/collection.py +++ b/fedireads/activitypub/collection.py @@ -118,6 +118,7 @@ def get_add_remove(user, book, shelf, action='Add'): 'type': action, 'actor': user.actor, 'object': { + # TODO: document?? 'type': 'Document', 'name': book.data['title'], 'url': book.openlibrary_key diff --git a/fedireads/activitypub/status.py b/fedireads/activitypub/status.py index 95429e293..5e89638e8 100644 --- a/fedireads/activitypub/status.py +++ b/fedireads/activitypub/status.py @@ -1,4 +1,7 @@ ''' status serializers ''' +from uuid import uuid4 + + def get_review(review): ''' fedireads json for book reviews ''' status = get_status(review) @@ -76,9 +79,51 @@ def get_replies(status, replies): def get_favorite(favorite): ''' like a post ''' return { - "@context": "https://www.w3.org/ns/activitystreams", - "id": favorite.absolute_id, - "type": "Like", - "actor": favorite.user.actor, - "object": favorite.status.absolute_id, + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': favorite.absolute_id, + 'type': 'Like', + 'actor': favorite.user.actor, + 'object': favorite.status.absolute_id, } + + +def get_add_tag(tag): + ''' add activity for tagging a book ''' + uuid = uuid4() + return { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': str(uuid), + 'type': 'Add', + 'actor': tag.user.actor, + 'object': { + 'type': 'Tag', + 'id': tag.absolute_id, + 'name': tag.name, + }, + 'target': { + 'type': 'Book', + 'id': tag.book.absolute_id, + } + } + + +def get_remove_tag(tag): + ''' add activity for tagging a book ''' + uuid = uuid4() + return { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': str(uuid), + 'type': 'Remove', + 'actor': tag.user.actor, + 'object': { + 'type': 'Tag', + 'id': tag.absolute_id, + 'name': tag.name, + }, + 'target': { + 'type': 'Book', + 'id': tag.book.absolute_id, + } + } + + diff --git a/fedireads/forms.py b/fedireads/forms.py index dc59a8a9e..0dd5f4d67 100644 --- a/fedireads/forms.py +++ b/fedireads/forms.py @@ -54,9 +54,11 @@ class EditUserForm(ModelForm): fields = ['avatar', 'name', 'summary'] help_texts = {f: None for f in fields} + class TagForm(ModelForm): class Meta: model = models.Tag fields = ['name'] help_texts = {f: None for f in fields} + labels = {'name': 'Add a tag'} diff --git a/fedireads/incoming.py b/fedireads/incoming.py index f75fa7ef3..e03b0c941 100644 --- a/fedireads/incoming.py +++ b/fedireads/incoming.py @@ -12,7 +12,8 @@ import requests from fedireads import activitypub from fedireads import models from fedireads import outgoing -from fedireads.status import create_review, create_status +from fedireads.openlibrary import get_or_create_book +from fedireads.status import create_review, create_status, create_tag from fedireads.remote_user import get_or_create_remote_user @@ -49,6 +50,9 @@ def shared_inbox(request): elif activity['type'] == 'Like': response = handle_incoming_favorite(activity) + elif activity['type'] == 'Add': + response = handle_incoming_add(activity) + # TODO: Add, Undo, Remove, etc return response @@ -274,6 +278,19 @@ def handle_incoming_favorite(activity): return HttpResponse() +def handle_incoming_add(activity): + ''' someone is tagging or shelving a book ''' + if activity['object']['type'] == 'Tag': + user = get_or_create_remote_user(activity['actor']) + if not user.local: + book_id = activity['target']['id'].split('/')[-1] + book = get_or_create_book(book_id) + create_tag(user, book, activity['object']['name']) + return HttpResponse() + return HttpResponse() + return HttpResponseNotFound() + + def handle_incoming_accept(activity): ''' someone is accepting a follow request ''' # our local user diff --git a/fedireads/migrations/0004_tag.py b/fedireads/migrations/0004_tag.py new file mode 100644 index 000000000..3a69c6bcd --- /dev/null +++ b/fedireads/migrations/0004_tag.py @@ -0,0 +1,29 @@ +# Generated by Django 3.0.3 on 2020-02-21 05:54 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('fedireads', '0003_auto_20200221_0131'), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=140)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'book', 'name')}, + }, + ), + ] diff --git a/fedireads/models/__init__.py b/fedireads/models/__init__.py index ace5e08ba..c71fa3a36 100644 --- a/fedireads/models/__init__.py +++ b/fedireads/models/__init__.py @@ -1,5 +1,5 @@ ''' bring all the models into the app namespace ''' from .book import Shelf, ShelfBook, Book, Author from .user import User, UserRelationship, FederatedServer -from .activity import Status, Review, Favorite +from .activity import Status, Review, Favorite, Tag diff --git a/fedireads/models/activity.py b/fedireads/models/activity.py index 93e70313a..bff660ad1 100644 --- a/fedireads/models/activity.py +++ b/fedireads/models/activity.py @@ -1,6 +1,7 @@ ''' models for storing different kinds of Activities ''' from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.dispatch import receiver from model_utils.managers import InheritanceManager from fedireads.utils.models import FedireadsModel @@ -46,14 +47,6 @@ class Review(Status): self.activity_type = 'Article' super().save(*args, **kwargs) - -class Tag(FedireadsModel): - ''' freeform tags for books ''' - users = models.ManyToManyField('User') - books = models.ManyToManyField('Book') - name = models.CharField(max_length=140, unique=True) - - class Favorite(FedireadsModel): ''' fav'ing a post ''' user = models.ForeignKey('User', on_delete=models.PROTECT) @@ -62,3 +55,13 @@ class Favorite(FedireadsModel): class Meta: unique_together = ('user', 'status') + +class Tag(FedireadsModel): + ''' freeform tags for books ''' + user = models.ForeignKey('User', on_delete=models.PROTECT) + book = models.ForeignKey('Book', on_delete=models.PROTECT) + name = models.CharField(max_length=140) + + class Meta: + unique_together = ('user', 'book', 'name') + diff --git a/fedireads/outgoing.py b/fedireads/outgoing.py index e75e75767..d032ac058 100644 --- a/fedireads/outgoing.py +++ b/fedireads/outgoing.py @@ -160,11 +160,26 @@ def handle_review(user, book, name, content, rating): other_recipients = get_recipients(user, 'public', limit='other') broadcast(user, article_create_activity, other_recipients) -def handle_tag(user, book, name): - tag = create_tag(user, book, name) - tag_activity = activitypub.get_tag(tag) - book_object = activitypub.get_book(book) +def handle_tag(user, book, name): + ''' tag a book ''' + tag = create_tag(user, book, name) + tag_activity = activitypub.get_add_tag(tag) + + recipients = get_recipients(user, 'public') + broadcast(user, tag_activity, recipients) + + +def handle_untag(user, book, name): + ''' tag a book ''' + book = models.Book.objects.get(openlibrary_key=book) + tag = models.Tag.objects.get(name=name, book=book, user=user) + tag_activity = activitypub.get_remove_tag(tag) + tag.delete() + + recipients = get_recipients(user, 'public') + broadcast(user, tag_activity, recipients) + def handle_comment(user, review, content): ''' respond to a review or status ''' diff --git a/fedireads/static/format.css b/fedireads/static/format.css index 2f22571af..b4f16177b 100644 --- a/fedireads/static/format.css +++ b/fedireads/static/format.css @@ -126,6 +126,17 @@ h2 { padding: 1rem; } +.tag { + border: 1px solid black; + display: inline-block; + padding: 0.2em; + border-radius: 0.2em; + background-color: #F3FFBD; +} +.tag form { + display: inline; +} + .review-form textarea { width: 30rem; height: 10rem; diff --git a/fedireads/status.py b/fedireads/status.py index 190167e9b..c5969289e 100644 --- a/fedireads/status.py +++ b/fedireads/status.py @@ -2,6 +2,7 @@ from fedireads import models from fedireads.openlibrary import get_or_create_book from fedireads.sanitize_html import InputHtmlParser +from django.db import IntegrityError def create_review(user, possible_book, name, content, rating): @@ -50,13 +51,9 @@ def create_tag(user, possible_book, name): book = get_or_create_book(possible_book) try: - # check for an existing tag with this text - tag = models.Tag.objects.get(name=name) - except models.Tag.DoesNotExist(): - # create a new one if there isn't an existing one - tag = models.Tag.objects.create(name=name) - tag.users.add(user) - tag.books.add(book) + tag = models.Tag.objects.create(name=name, book=book, user=user) + except IntegrityError: + return models.Tag.objects.get(name=name, book=book, user=user) return tag diff --git a/fedireads/templates/book.html b/fedireads/templates/book.html index b62929453..32508ca1a 100644 --- a/fedireads/templates/book.html +++ b/fedireads/templates/book.html @@ -6,18 +6,23 @@
No reviews yet!
- {% endif %} +No reviews yet!
+ {% endif %}