mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-22 16:16:39 +00:00
parent
13b512b569
commit
870d0b9697
15 changed files with 205 additions and 37 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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'}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
29
fedireads/migrations/0004_tag.py
Normal file
29
fedireads/migrations/0004_tag.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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 '''
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
20
fedireads/templates/snippets/tag.html
Normal file
20
fedireads/templates/snippets/tag.html
Normal 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>
|
||||
|
||||
|
|
@ -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(
|
||||
|
|
|
@ -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 '''
|
||||
|
|
Loading…
Reference in a new issue