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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,18 +6,23 @@
<div class="book-preview">
{% include 'snippets/book.html' with book=book size=large rating=rating description=True %}
</div>
</div>
<div class="reviews">
<h2>Reviews</h2>
{% if not reviews %}
<p>No reviews yet!</p>
{% endif %}
<div id="tag-cloud">
{% for tag in tags %}
{% include 'snippets/tag.html' with tag=tag user=request.user %}
{% endfor %}
</div>
<form class="tag-form" name="tag" action="/tag/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input>
{{ tag_form.as_p }}
<button type="submit">Add tag</button>
</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">
{% csrf_token %}
<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
re_path(r'^review/?$', views.review),
re_path(r'^tag/?$', views.tag),
re_path(r'^untag/?$', views.untag),
re_path(r'^comment/?$', views.comment),
re_path(r'^favorite/(?P<status_id>\d+)/?$', views.favorite),
re_path(

View file

@ -202,12 +202,25 @@ def book_page(request, book_identifier):
# TODO: again, post privacy?
reviews = models.Review.objects.filter(book=book)
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()
tag_form = forms.TagForm()
data = {
'book': book,
'reviews': reviews,
'rating': rating['rating__avg'],
'tags': tags,
'user_tags': user_tags,
'review_form': review_form,
'tag_form': tag_form,
}
return TemplateResponse(request, 'book.html', data)
@ -276,16 +289,25 @@ def review(request):
@login_required
def tag(request):
''' 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')
if not form.is_valid():
return redirect('/book/%s' % book_identifier)
name = form.data.get('name')
outgoing.handle_tag(request.user, book_identifier, name)
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
def comment(request):
''' respond to a book review '''