mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-01-05 14:58:43 +00:00
commit
3fdb57e26a
16 changed files with 234 additions and 12 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -54,3 +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 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 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
|
||||||
|
|
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 '''
|
''' 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
|
||||||
|
|
||||||
|
|
|
@ -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,7 +47,6 @@ class Review(Status):
|
||||||
self.activity_type = 'Article'
|
self.activity_type = 'Article'
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
@ -55,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')
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from urllib.parse import urlencode
|
||||||
|
|
||||||
from fedireads import activitypub
|
from fedireads import activitypub
|
||||||
from fedireads import models
|
from fedireads import models
|
||||||
from fedireads.status import create_review, create_status
|
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
|
||||||
from fedireads.broadcast import get_recipients, broadcast
|
from fedireads.broadcast import get_recipients, broadcast
|
||||||
|
|
||||||
|
@ -161,6 +161,26 @@ def handle_review(user, book, name, content, rating):
|
||||||
broadcast(user, article_create_activity, other_recipients)
|
broadcast(user, article_create_activity, other_recipients)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def handle_comment(user, review, content):
|
||||||
''' respond to a review or status '''
|
''' respond to a review or status '''
|
||||||
# validated and saves the comment in the database so it has an id
|
# validated and saves the comment in the database so it has an id
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -45,6 +46,17 @@ def create_status(user, content, reply_parent=None, mention_books=None):
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
def create_tag(user, possible_book, name):
|
||||||
|
''' add a tag to a book '''
|
||||||
|
book = get_or_create_book(possible_book)
|
||||||
|
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
def sanitize(content):
|
def sanitize(content):
|
||||||
''' remove invalid html from free text '''
|
''' remove invalid html from free text '''
|
||||||
parser = InputHtmlParser()
|
parser = InputHtmlParser()
|
||||||
|
|
|
@ -6,6 +6,17 @@
|
||||||
<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 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>
|
||||||
<div class="reviews">
|
<div class="reviews">
|
||||||
<h2>Reviews</h2>
|
<h2>Reviews</h2>
|
||||||
|
|
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>
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,8 @@ 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'^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(
|
||||||
|
|
|
@ -11,7 +11,7 @@ class FedireadsModel(models.Model):
|
||||||
def absolute_id(self):
|
def absolute_id(self):
|
||||||
''' constructs the absolute reference to any db object '''
|
''' constructs the absolute reference to any db object '''
|
||||||
base_path = 'https://%s' % DOMAIN
|
base_path = 'https://%s' % DOMAIN
|
||||||
if self.user:
|
if hasattr(self, 'user'):
|
||||||
base_path = self.user.absolute_id
|
base_path = self.user.absolute_id
|
||||||
model_name = type(self).__name__.lower()
|
model_name = type(self).__name__.lower()
|
||||||
return '%s/%s/%d' % (base_path, model_name, self.id)
|
return '%s/%s/%d' % (base_path, model_name, self.id)
|
||||||
|
|
|
@ -50,7 +50,8 @@ def home_tab(request, tab):
|
||||||
if tab == 'home':
|
if tab == 'home':
|
||||||
# people you follow and direct mentions
|
# people you follow and direct mentions
|
||||||
activities = activities.filter(
|
activities = activities.filter(
|
||||||
Q(user__in=following, privacy='public') | Q(mention_users=request.user)
|
Q(user__in=following, privacy='public') | \
|
||||||
|
Q(mention_users=request.user)
|
||||||
)
|
)
|
||||||
elif tab == 'local':
|
elif tab == 'local':
|
||||||
# everyone on this instance
|
# everyone on this instance
|
||||||
|
@ -201,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)
|
||||||
|
|
||||||
|
@ -272,6 +286,28 @@ def review(request):
|
||||||
return redirect('/book/%s' % book_identifier)
|
return redirect('/book/%s' % book_identifier)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def tag(request):
|
||||||
|
''' tag a book '''
|
||||||
|
# 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')
|
||||||
|
|
||||||
|
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
|
@login_required
|
||||||
def comment(request):
|
def comment(request):
|
||||||
''' respond to a book review '''
|
''' respond to a book review '''
|
||||||
|
|
Loading…
Reference in a new issue