Merge pull request #60 from mouse-reeve/tagging

Tagging
This commit is contained in:
Mouse Reeve 2020-02-20 22:23:49 -08:00 committed by GitHub
commit 3fdb57e26a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 234 additions and 12 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,3 +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,7 +47,6 @@ class Review(Status):
self.activity_type = 'Article'
super().save(*args, **kwargs)
class Favorite(FedireadsModel):
''' fav'ing a post '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
@ -55,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

@ -7,7 +7,7 @@ from urllib.parse import urlencode
from fedireads import activitypub
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.broadcast import get_recipients, broadcast
@ -161,6 +161,26 @@ def handle_review(user, book, name, content, rating):
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):
''' respond to a review or status '''
# validated and saves the comment in the database so it has an id

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):
@ -45,6 +46,17 @@ def create_status(user, content, reply_parent=None, mention_books=None):
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):
''' remove invalid html from free text '''
parser = InputHtmlParser()

View file

@ -6,6 +6,17 @@
<div class="book-preview">
{% include 'snippets/book.html' with book=book size=large rating=rating description=True %}
</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 class="reviews">
<h2>Reviews</h2>

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

@ -51,6 +51,8 @@ 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

@ -11,7 +11,7 @@ class FedireadsModel(models.Model):
def absolute_id(self):
''' constructs the absolute reference to any db object '''
base_path = 'https://%s' % DOMAIN
if self.user:
if hasattr(self, 'user'):
base_path = self.user.absolute_id
model_name = type(self).__name__.lower()
return '%s/%s/%d' % (base_path, model_name, self.id)

View file

@ -50,7 +50,8 @@ def home_tab(request, tab):
if tab == 'home':
# people you follow and direct mentions
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':
# everyone on this instance
@ -201,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)
@ -272,6 +286,28 @@ def review(request):
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
def comment(request):
''' respond to a book review '''