Works on #55
This commit is contained in:
Mouse Reeve 2020-02-20 22:19:19 -08:00
parent 13b512b569
commit 870d0b9697
15 changed files with 205 additions and 37 deletions

View File

@ -5,4 +5,4 @@ from .collection import get_outbox, get_outbox_page, get_add, get_remove, \
from .create import get_create from .create import get_create
from .follow import get_follow_request, get_unfollow, get_accept from .follow import get_follow_request, get_unfollow, get_accept
from .status import get_review, get_review_article, get_status, get_replies, \ from .status import get_review, get_review_article, get_status, get_replies, \
get_favorite get_favorite, get_add_tag, get_remove_tag

View File

@ -118,6 +118,7 @@ def get_add_remove(user, book, shelf, action='Add'):
'type': action, 'type': action,
'actor': user.actor, 'actor': user.actor,
'object': { 'object': {
# TODO: document??
'type': 'Document', 'type': 'Document',
'name': book.data['title'], 'name': book.data['title'],
'url': book.openlibrary_key 'url': book.openlibrary_key

View File

@ -1,4 +1,7 @@
''' status serializers ''' ''' status serializers '''
from uuid import uuid4
def get_review(review): def get_review(review):
''' fedireads json for book reviews ''' ''' fedireads json for book reviews '''
status = get_status(review) status = get_status(review)
@ -76,9 +79,51 @@ def get_replies(status, replies):
def get_favorite(favorite): def get_favorite(favorite):
''' like a post ''' ''' like a post '''
return { return {
"@context": "https://www.w3.org/ns/activitystreams", '@context': 'https://www.w3.org/ns/activitystreams',
"id": favorite.absolute_id, 'id': favorite.absolute_id,
"type": "Like", 'type': 'Like',
"actor": favorite.user.actor, 'actor': favorite.user.actor,
"object": favorite.status.absolute_id, '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,
}
}

View File

@ -54,9 +54,11 @@ class EditUserForm(ModelForm):
fields = ['avatar', 'name', 'summary'] fields = ['avatar', 'name', 'summary']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
class TagForm(ModelForm): class TagForm(ModelForm):
class Meta: class Meta:
model = models.Tag model = models.Tag
fields = ['name'] fields = ['name']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
labels = {'name': 'Add a tag'}

View File

@ -12,7 +12,8 @@ import requests
from fedireads import activitypub from fedireads import activitypub
from fedireads import models from fedireads import models
from fedireads import outgoing 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 from fedireads.remote_user import get_or_create_remote_user
@ -49,6 +50,9 @@ def shared_inbox(request):
elif activity['type'] == 'Like': elif activity['type'] == 'Like':
response = handle_incoming_favorite(activity) response = handle_incoming_favorite(activity)
elif activity['type'] == 'Add':
response = handle_incoming_add(activity)
# TODO: Add, Undo, Remove, etc # TODO: Add, Undo, Remove, etc
return response return response
@ -274,6 +278,19 @@ def handle_incoming_favorite(activity):
return HttpResponse() 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): def handle_incoming_accept(activity):
''' someone is accepting a follow request ''' ''' someone is accepting a follow request '''
# our local user # our local user

View File

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

View File

@ -1,5 +1,5 @@
''' bring all the models into the app namespace ''' ''' bring all the models into the app namespace '''
from .book import Shelf, ShelfBook, Book, Author from .book import Shelf, ShelfBook, Book, Author
from .user import User, UserRelationship, FederatedServer from .user import User, UserRelationship, FederatedServer
from .activity import Status, Review, Favorite from .activity import Status, Review, Favorite, Tag

View File

@ -1,6 +1,7 @@
''' models for storing different kinds of Activities ''' ''' models for storing different kinds of Activities '''
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.dispatch import receiver
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from fedireads.utils.models import FedireadsModel from fedireads.utils.models import FedireadsModel
@ -46,14 +47,6 @@ class Review(Status):
self.activity_type = 'Article' self.activity_type = 'Article'
super().save(*args, **kwargs) 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): class Favorite(FedireadsModel):
''' fav'ing a post ''' ''' fav'ing a post '''
user = models.ForeignKey('User', on_delete=models.PROTECT) user = models.ForeignKey('User', on_delete=models.PROTECT)
@ -62,3 +55,13 @@ class Favorite(FedireadsModel):
class Meta: class Meta:
unique_together = ('user', 'status') 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')

View File

@ -160,11 +160,26 @@ def handle_review(user, book, name, content, rating):
other_recipients = get_recipients(user, 'public', limit='other') other_recipients = get_recipients(user, 'public', limit='other')
broadcast(user, article_create_activity, other_recipients) 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) def handle_tag(user, book, name):
book_object = activitypub.get_book(book) ''' 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): def handle_comment(user, review, content):
''' respond to a review or status ''' ''' respond to a review or status '''

View File

@ -126,6 +126,17 @@ h2 {
padding: 1rem; 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 { .review-form textarea {
width: 30rem; width: 30rem;
height: 10rem; height: 10rem;

View File

@ -2,6 +2,7 @@
from fedireads import models from fedireads import models
from fedireads.openlibrary import get_or_create_book from fedireads.openlibrary import get_or_create_book
from fedireads.sanitize_html import InputHtmlParser from fedireads.sanitize_html import InputHtmlParser
from django.db import IntegrityError
def create_review(user, possible_book, name, content, rating): 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) book = get_or_create_book(possible_book)
try: try:
# check for an existing tag with this text tag = models.Tag.objects.create(name=name, book=book, user=user)
tag = models.Tag.objects.get(name=name) except IntegrityError:
except models.Tag.DoesNotExist(): return models.Tag.objects.get(name=name, book=book, user=user)
# 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)
return tag return tag

View File

@ -6,18 +6,23 @@
<div class="book-preview"> <div class="book-preview">
{% include 'snippets/book.html' with book=book size=large rating=rating description=True %} {% include 'snippets/book.html' with book=book size=large rating=rating description=True %}
</div> </div>
</div> <div id="tag-cloud">
<div class="reviews"> {% for tag in tags %}
<h2>Reviews</h2> {% include 'snippets/tag.html' with tag=tag user=request.user %}
{% if not reviews %} {% endfor %}
<p>No reviews yet!</p> </div>
{% endif %}
<form class="tag-form" name="tag" action="/tag/" method="post"> <form class="tag-form" name="tag" action="/tag/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input> <input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input>
{{ tag_form.as_p }} {{ tag_form.as_p }}
<button type="submit">Add tag</button> <button type="submit">Add tag</button>
</form> </form>
</div>
<div class="reviews">
<h2>Reviews</h2>
{% if not reviews %}
<p>No reviews yet!</p>
{% endif %}
<form class="review-form" name="review" action="/review/" method="post"> <form class="review-form" name="review" action="/review/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input> <input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input>

View File

@ -0,0 +1,20 @@
<div class="tag">
{{ tag.name }}
{% if tag.name in user_tags %}
<form class="tag-form" name="tag" action="/untag/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input>
<input type="hidden" name="name" value="{{ tag.name }}"></input>
<button type="submit">x</button>
</form>
{% else %}
<form class="tag-form" name="tag" action="/tag/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input>
<input type="hidden" name="name" value="{{ tag.name }}"></input>
<button type="submit">+</button>
</form>
{% endif %}
</div>

View File

@ -52,6 +52,7 @@ urlpatterns = [
# internal action endpoints # internal action endpoints
re_path(r'^review/?$', views.review), re_path(r'^review/?$', views.review),
re_path(r'^tag/?$', views.tag), re_path(r'^tag/?$', views.tag),
re_path(r'^untag/?$', views.untag),
re_path(r'^comment/?$', views.comment), re_path(r'^comment/?$', views.comment),
re_path(r'^favorite/(?P<status_id>\d+)/?$', views.favorite), re_path(r'^favorite/(?P<status_id>\d+)/?$', views.favorite),
re_path( re_path(

View File

@ -202,12 +202,25 @@ def book_page(request, book_identifier):
# TODO: again, post privacy? # TODO: again, post privacy?
reviews = models.Review.objects.filter(book=book) reviews = models.Review.objects.filter(book=book)
rating = reviews.aggregate(Avg('rating')) rating = reviews.aggregate(Avg('rating'))
tags = models.Tag.objects.filter(
book=book
).values(
'book', 'name'
).distinct().all()
user_tags = models.Tag.objects.filter(
book=book, user=request.user
).values_list('name', flat=True)
review_form = forms.ReviewForm() review_form = forms.ReviewForm()
tag_form = forms.TagForm()
data = { data = {
'book': book, 'book': book,
'reviews': reviews, 'reviews': reviews,
'rating': rating['rating__avg'], 'rating': rating['rating__avg'],
'tags': tags,
'user_tags': user_tags,
'review_form': review_form, 'review_form': review_form,
'tag_form': tag_form,
} }
return TemplateResponse(request, 'book.html', data) return TemplateResponse(request, 'book.html', data)
@ -276,16 +289,25 @@ def review(request):
@login_required @login_required
def tag(request): def tag(request):
''' tag a book ''' ''' tag a book '''
form = forms.ReviewForm(request.POST) # I'm not using a form here because sometimes "name" is sent as a hidden
# field which doesn't validate
name = request.POST.get('name')
book_identifier = request.POST.get('book') book_identifier = request.POST.get('book')
if not form.is_valid():
return redirect('/book/%s' % book_identifier)
name = form.data.get('name')
outgoing.handle_tag(request.user, book_identifier, name) outgoing.handle_tag(request.user, book_identifier, name)
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_identifier)
@login_required
def untag(request):
''' untag a book '''
name = request.POST.get('name')
book_identifier = request.POST.get('book')
outgoing.handle_untag(request.user, book_identifier, name)
return redirect('/book/%s' % book_identifier)
@login_required @login_required
def comment(request): def comment(request):
''' respond to a book review ''' ''' respond to a book review '''