Merge pull request #242 from mouse-reeve/privacy

Refactors status creation
This commit is contained in:
Mouse Reeve 2020-10-27 15:17:02 -07:00 committed by GitHub
commit 2c48904e40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 95 additions and 371 deletions

View file

@ -52,47 +52,31 @@ class RegisterForm(CustomForm):
class RatingForm(CustomForm): class RatingForm(CustomForm):
class Meta: class Meta:
model = models.Review model = models.Review
fields = ['rating'] fields = ['user', 'book', 'content', 'rating', 'privacy']
class ReviewForm(CustomForm): class ReviewForm(CustomForm):
class Meta: class Meta:
model = models.Review model = models.Review
fields = ['name', 'content'] fields = ['user', 'book', 'name', 'content', 'rating', 'privacy']
help_texts = {f: None for f in fields}
labels = {
'name': 'Title',
'content': 'Review',
}
class CommentForm(CustomForm): class CommentForm(CustomForm):
class Meta: class Meta:
model = models.Comment model = models.Comment
fields = ['content'] fields = ['user', 'book', 'content', 'privacy']
help_texts = {f: None for f in fields}
labels = {
'content': 'Comment',
}
class QuotationForm(CustomForm): class QuotationForm(CustomForm):
class Meta: class Meta:
model = models.Quotation model = models.Quotation
fields = ['quote', 'content'] fields = ['user', 'book', 'quote', 'content', 'privacy']
help_texts = {f: None for f in fields}
labels = {
'quote': 'Quote',
'content': 'Comment',
}
class ReplyForm(CustomForm): class ReplyForm(CustomForm):
class Meta: class Meta:
model = models.Status model = models.Status
fields = ['content'] fields = ['user', 'content', 'reply_parent', 'privacy']
help_texts = {f: None for f in fields}
labels = {'content': 'Comment'}
class EditUserForm(CustomForm): class EditUserForm(CustomForm):

View file

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-10-26 21:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0056_auto_20201021_0150'),
]
operations = [
migrations.AlterField(
model_name='status',
name='privacy',
field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
),
]

View file

@ -10,6 +10,13 @@ from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
from .base_model import ActivityMapping, BookWyrmModel from .base_model import ActivityMapping, BookWyrmModel
PrivacyLevels = models.TextChoices('Privacy', [
'public',
'unlisted',
'followers',
'direct'
])
class Status(OrderedCollectionPageMixin, BookWyrmModel): class Status(OrderedCollectionPageMixin, BookWyrmModel):
''' any post, like a reply to a review, etc ''' ''' any post, like a reply to a review, etc '''
user = models.ForeignKey('User', on_delete=models.PROTECT) user = models.ForeignKey('User', on_delete=models.PROTECT)
@ -18,7 +25,11 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
mention_books = models.ManyToManyField( mention_books = models.ManyToManyField(
'Edition', related_name='mention_book') 'Edition', related_name='mention_book')
local = models.BooleanField(default=True) local = models.BooleanField(default=True)
privacy = models.CharField(max_length=255, default='public') privacy = models.CharField(
max_length=255,
default='public',
choices=PrivacyLevels.choices
)
sensitive = models.BooleanField(default=False) sensitive = models.BooleanField(default=False)
# the created date can't be this, because of receiving federated posts # the created date can't be this, because of receiving federated posts
published_date = models.DateTimeField(default=timezone.now) published_date = models.DateTimeField(default=timezone.now)
@ -181,9 +192,14 @@ class Review(Status):
@property @property
def ap_pure_name(self): def ap_pure_name(self):
''' clarify review names for mastodon serialization ''' ''' clarify review names for mastodon serialization '''
return 'Review of "%s" (%d stars): %s' % ( if self.rating:
return 'Review of "%s" (%d stars): %s' % (
self.book.title,
self.rating,
self.name
)
return 'Review of "%s": %s' % (
self.book.title, self.book.title,
self.rating,
self.name self.name
) )

View file

@ -9,9 +9,7 @@ import requests
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm import models from bookwyrm import models
from bookwyrm.broadcast import broadcast from bookwyrm.broadcast import broadcast
from bookwyrm.status import create_review, create_status from bookwyrm.status import create_tag, create_notification
from bookwyrm.status import create_quotation, create_comment
from bookwyrm.status import create_tag, create_notification, create_rating
from bookwyrm.status import create_generated_note from bookwyrm.status import create_generated_note
from bookwyrm.status import delete_status from bookwyrm.status import delete_status
from bookwyrm.remote_user import get_or_create_remote_user from bookwyrm.remote_user import get_or_create_remote_user
@ -178,15 +176,16 @@ def handle_import_books(user, items):
broadcast(user, activity) broadcast(user, activity)
if item.rating or item.review: if item.rating or item.review:
review_title = "Review of {!r} on Goodreads".format( review_title = 'Review of {!r} on Goodreads'.format(
item.book.title, item.book.title,
) if item.review else "" ) if item.review else ''
handle_review(
user, models.Review.objects.create(
item.book, user=user,
review_title, book=item.book,
item.review, name=review_title,
item.rating, content=item.review,
rating=item.rating,
) )
for read in item.reads: for read in item.reads:
read.book = item.book read.book = item.book
@ -209,44 +208,25 @@ def handle_delete_status(user, status):
broadcast(user, status.to_activity()) broadcast(user, status.to_activity())
def handle_rate(user, book, rating): def handle_status(user, form):
''' a review that's just a rating '''
builder = create_rating
handle_status(user, book, builder, rating)
def handle_review(user, book, name, content, rating):
''' post a review '''
# validated and saves the review in the database so it has an id
builder = create_review
handle_status(user, book, builder, name, content, rating)
def handle_quotation(user, book, content, quote):
''' post a review '''
# validated and saves the review in the database so it has an id
builder = create_quotation
handle_status(user, book, builder, content, quote)
def handle_comment(user, book, content):
''' post a comment '''
# validated and saves the review in the database so it has an id
builder = create_comment
handle_status(user, book, builder, content)
def handle_status(user, book_id, builder, *args):
''' generic handler for statuses ''' ''' generic handler for statuses '''
book = models.Edition.objects.get(id=book_id) status = form.save()
status = builder(user, book, *args)
# notify reply parent or (TODO) tagged users
if status.reply_parent and status.reply_parent.user.local:
create_notification(
status.reply_parent.user,
'REPLY',
related_user=user,
related_status=status
)
broadcast(user, status.to_create_activity(user), software='bookwyrm') broadcast(user, status.to_create_activity(user), software='bookwyrm')
# re-format the activity for non-bookwyrm servers # re-format the activity for non-bookwyrm servers
remote_activity = status.to_create_activity(user, pure=True) if hasattr(status, 'pure_activity_serializer'):
remote_activity = status.to_create_activity(user, pure=True)
broadcast(user, remote_activity, software='other') broadcast(user, remote_activity, software='other')
def handle_tag(user, book, name): def handle_tag(user, book, name):
@ -265,21 +245,6 @@ def handle_untag(user, book, name):
broadcast(user, tag_activity) broadcast(user, tag_activity)
def handle_reply(user, review, content):
''' respond to a review or status '''
# validated and saves the comment in the database so it has an id
reply = create_status(user, content, reply_parent=review)
if reply.reply_parent:
create_notification(
reply.reply_parent.user,
'REPLY',
related_user=user,
related_status=reply,
)
broadcast(user, reply.to_create_activity(user))
def handle_favorite(user, status): def handle_favorite(user, status):
''' a user likes a status ''' ''' a user likes a status '''
try: try:

View file

@ -13,100 +13,6 @@ def delete_status(status):
status.deleted_date = datetime.now() status.deleted_date = datetime.now()
status.save() status.save()
def create_rating(user, book, rating):
''' a review that's just a rating '''
if not rating or rating < 1 or rating > 5:
raise ValueError('Invalid rating')
return models.Review.objects.create(
user=user,
book=book,
rating=rating,
)
def create_review(user, book, name, content, rating):
''' a book review has been added '''
name = sanitize(name)
content = sanitize(content)
# no ratings outside of 0-5
if rating:
rating = rating if 1 <= rating <= 5 else None
else:
rating = None
return models.Review.objects.create(
user=user,
book=book,
name=name,
rating=rating,
content=content,
)
def create_quotation_from_activity(author, activity):
''' parse an activity json blob into a status '''
book_id = activity['inReplyToBook']
book = get_or_create_book(book_id)
quote = activity.get('quote')
content = activity.get('content')
published = activity.get('published')
remote_id = activity['id']
quotation = create_quotation(author, book, content, quote)
quotation.published_date = published
quotation.remote_id = remote_id
quotation.save()
return quotation
def create_quotation(user, book, content, quote):
''' a quotation has been added '''
# throws a value error if the book is not found
content = sanitize(content)
quote = sanitize(quote)
return models.Quotation.objects.create(
user=user,
book=book,
content=content,
quote=quote,
)
def create_comment_from_activity(author, activity):
''' parse an activity json blob into a status '''
book_id = activity['inReplyToBook']
book = get_or_create_book(book_id)
content = activity.get('content')
published = activity.get('published')
remote_id = activity['id']
comment = create_comment(author, book, content)
comment.published_date = published
comment.remote_id = remote_id
comment.save()
return comment
def create_comment(user, book, content):
''' a book comment has been added '''
# throws a value error if the book is not found
content = sanitize(content)
return models.Comment.objects.create(
user=user,
book=book,
content=content,
)
def get_status(remote_id):
''' find a status in the database '''
return models.Status.objects.select_subclasses().filter(
remote_id=remote_id
).first()
def create_generated_note(user, content, mention_books=None): def create_generated_note(user, content, mention_books=None):
''' a note created by the app about user activity ''' ''' a note created by the app about user activity '''
@ -127,28 +33,6 @@ def create_generated_note(user, content, mention_books=None):
return status return status
def create_status(user, content, reply_parent=None, mention_books=None):
''' a status update '''
# TODO: handle @'ing users
# sanitize input html
parser = InputHtmlParser()
parser.feed(content)
content = parser.get_output()
status = models.Status.objects.create(
user=user,
content=content,
reply_parent=reply_parent,
)
if mention_books:
for book in mention_books:
status.mention_books.add(book)
return status
def create_tag(user, possible_book, name): def create_tag(user, possible_book, name):
''' add a tag to a book ''' ''' add a tag to a book '''
book = get_or_create_book(possible_book) book = get_or_create_book(possible_book)
@ -174,10 +58,3 @@ def create_notification(user, notification_type, related_user=None, \
related_import=related_import, related_import=related_import,
notification_type=notification_type, notification_type=notification_type,
) )
def sanitize(content):
''' remove invalid html from free text '''
parser = InputHtmlParser()
parser.feed(content)
return parser.get_output()

View file

@ -22,6 +22,8 @@
<form class="toggle-content hidden tab-option-{{ book.id }}" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}"> <form class="toggle-content hidden tab-option-{{ book.id }}" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<input type="hidden" name="privacy" value="public">
<div class="control"> <div class="control">
<label class="label" for="id_name_{{ book.id }}_review">Title:</label> <label class="label" for="id_name_{{ book.id }}_review">Title:</label>
<input type="text" name="name" maxlength="255" class="input" required="" id="id_name_{{ book.id }}_review" placeholder="My review of '{{ book.title }}'"> <input type="text" name="name" maxlength="255" class="input" required="" id="id_name_{{ book.id }}_review" placeholder="My review of '{{ book.title }}'">
@ -51,6 +53,9 @@
<form class="toggle-content hidden tab-option-{{ book.id }}" name="comment" action="/comment/" method="post" id="tab-comment-{{ book.id }}"> <form class="toggle-content hidden tab-option-{{ book.id }}" name="comment" action="/comment/" method="post" id="tab-comment-{{ book.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<input type="hidden" name="privacy" value="public">
<div class="control"> <div class="control">
<label class="label" for="id_content_{{ book.id }}_comment">Comment:</label> <label class="label" for="id_content_{{ book.id }}_comment">Comment:</label>
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_comment" placeholder="Some thoughts on '{{ book.title }}'"></textarea> <textarea name="content" class="textarea" id="id_content_{{ book.id }}_comment" placeholder="Some thoughts on '{{ book.title }}'"></textarea>
@ -64,6 +69,9 @@
<form class="toggle-content hidden tab-option-{{ book.id }}" name="quotation" action="/quotate/" method="post" id="tab-quotation-{{ book.id }}"> <form class="toggle-content hidden tab-option-{{ book.id }}" name="quotation" action="/quotate/" method="post" id="tab-quotation-{{ book.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<input type="hidden" name="privacy" value="public">
<div class="control"> <div class="control">
<label class="label" for="id_quote_{{ book.id }}_quote">Quote:</label> <label class="label" for="id_quote_{{ book.id }}_quote">Quote:</label>
<textarea name="quote" class="textarea" required="" id="id_quote_{{ book.id }}_quote" placeholder="An except from '{{ book.title }}'"></textarea> <textarea name="quote" class="textarea" required="" id="id_quote_{{ book.id }}_quote" placeholder="An except from '{{ book.title }}'"></textarea>

View file

@ -4,7 +4,9 @@
<form name="reply" action="/reply" method="post" onsubmit="return reply(event)"> <form name="reply" action="/reply" method="post" onsubmit="return reply(event)">
<div class="field is-grouped"> <div class="field is-grouped">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="parent" value="{{ activity.id }}"> <input type="hidden" name="reply_parent" value="{{ activity.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<input type="hidden" name="privacy" value="public">
<textarea name="content" placeholder="Leave a comment..." id="id_content_{{ activity.id }}" required="true"></textarea> <textarea name="content" placeholder="Leave a comment..." id="id_content_{{ activity.id }}" required="true"></textarea>
<button class="button" type="submit"> <button class="button" type="submit">
<span class="icon icon-comment"> <span class="icon icon-comment">

View file

@ -1 +0,0 @@
from . import *

View file

@ -1,18 +0,0 @@
from django.test import TestCase
from bookwyrm import models
from bookwyrm import status as status_builder
class Comment(TestCase):
''' we have hecka ways to create statuses '''
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
self.book = models.Edition.objects.create(title='Example Edition')
def test_create_comment(self):
comment = status_builder.create_comment(
self.user, self.book, 'commentary')
self.assertEqual(comment.content, 'commentary')

View file

@ -1,24 +0,0 @@
from django.test import TestCase
from bookwyrm import models
from bookwyrm import status as status_builder
class Quotation(TestCase):
''' we have hecka ways to create statuses '''
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
remote_id='https://example.com/user/mouse'
)
self.book = models.Edition.objects.create(
title='Example Edition',
remote_id='https://example.com/book/1',
)
def test_create_quotation(self):
quotation = status_builder.create_quotation(
self.user, self.book, 'commentary', 'a quote')
self.assertEqual(quotation.quote, 'a quote')
self.assertEqual(quotation.content, 'commentary')

View file

@ -1,39 +0,0 @@
from django.test import TestCase
from bookwyrm import models
from bookwyrm import status as status_builder
class Review(TestCase):
''' we have hecka ways to create statuses '''
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
self.book = models.Edition.objects.create(title='Example Edition')
def test_create_review(self):
review = status_builder.create_review(
self.user, self.book, 'review name', 'content', 5)
self.assertEqual(review.name, 'review name')
self.assertEqual(review.content, 'content')
self.assertEqual(review.rating, 5)
review = status_builder.create_review(
self.user, self.book, '<div>review</div> name', '<b>content', 5)
self.assertEqual(review.name, 'review name')
self.assertEqual(review.content, 'content')
self.assertEqual(review.rating, 5)
def test_review_rating(self):
review = status_builder.create_review(
self.user, self.book, 'review name', 'content', -1)
self.assertEqual(review.name, 'review name')
self.assertEqual(review.content, 'content')
self.assertEqual(review.rating, None)
review = status_builder.create_review(
self.user, self.book, 'review name', 'content', 6)
self.assertEqual(review.name, 'review name')
self.assertEqual(review.content, 'content')
self.assertEqual(review.rating, None)

View file

@ -1,28 +0,0 @@
from django.test import TestCase
from bookwyrm import models
from bookwyrm import status as status_builder
class Status(TestCase):
''' we have hecka ways to create statuses '''
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=False,
inbox='https://example.com/user/mouse/inbox',
outbox='https://example.com/user/mouse/outbox',
remote_id='https://example.com/user/mouse'
)
def test_create_status(self):
content = 'statuses are usually <i>replies</i>'
status = status_builder.create_status(
self.user, content)
self.assertEqual(status.content, content)
reply = status_builder.create_status(
self.user, content, reply_parent=status)
self.assertEqual(reply.content, content)
self.assertEqual(reply.reply_parent, status)

View file

@ -289,68 +289,45 @@ def shelve(request):
def rate(request): def rate(request):
''' just a star rating for a book ''' ''' just a star rating for a book '''
form = forms.RatingForm(request.POST) form = forms.RatingForm(request.POST)
book_id = request.POST.get('book') return handle_status(request, form)
# TODO: better failure behavior
if not form.is_valid():
return redirect('/book/%s' % book_id)
rating = form.cleaned_data.get('rating')
# throws a value error if the book is not found
outgoing.handle_rate(request.user, book_id, rating)
return redirect('/book/%s' % book_id)
@login_required @login_required
def review(request): def review(request):
''' create a book review ''' ''' create a book review '''
form = forms.ReviewForm(request.POST) form = forms.ReviewForm(request.POST)
book_id = request.POST.get('book') return handle_status(request, form)
if not form.is_valid():
return redirect('/book/%s' % book_id)
# TODO: validation, htmlification
name = form.cleaned_data.get('name')
content = form.cleaned_data.get('content')
rating = form.data.get('rating', None)
try:
rating = int(rating)
except ValueError:
rating = None
outgoing.handle_review(request.user, book_id, name, content, rating)
return redirect('/book/%s' % book_id)
@login_required @login_required
def quotate(request): def quotate(request):
''' create a book quotation ''' ''' create a book quotation '''
form = forms.QuotationForm(request.POST) form = forms.QuotationForm(request.POST)
book_id = request.POST.get('book') return handle_status(request, form)
if not form.is_valid():
return redirect('/book/%s' % book_id)
quote = form.cleaned_data.get('quote')
content = form.cleaned_data.get('content')
outgoing.handle_quotation(request.user, book_id, content, quote)
return redirect('/book/%s' % book_id)
@login_required @login_required
def comment(request): def comment(request):
''' create a book comment ''' ''' create a book comment '''
form = forms.CommentForm(request.POST) form = forms.CommentForm(request.POST)
return handle_status(request, form)
@login_required
def reply(request):
''' respond to a book review '''
form = forms.ReplyForm(request.POST)
return handle_status(request, form)
def handle_status(request, form):
''' all the "create a status" functions are the same '''
book_id = request.POST.get('book') book_id = request.POST.get('book')
# TODO: better failure behavior
if not form.is_valid(): if not form.is_valid():
return redirect('/book/%s' % book_id) return redirect(request.headers.get('Referer', '/'))
# TODO: validation, htmlification outgoing.handle_status(request.user, form)
content = form.data.get('content') return redirect(request.headers.get('Referer', '/'))
outgoing.handle_comment(request.user, book_id, content)
return redirect('/book/%s' % book_id)
@login_required @login_required
@ -376,19 +353,6 @@ def untag(request):
return redirect('/book/%s' % book_id) return redirect('/book/%s' % book_id)
@login_required
def reply(request):
''' respond to a book review '''
form = forms.ReplyForm(request.POST)
# this is a bit of a formality, the form is just one text field
if not form.is_valid():
return redirect('/')
parent_id = request.POST['parent']
parent = models.Status.objects.get(id=parent_id)
outgoing.handle_reply(request.user, parent, form.data['content'])
return redirect('/')
@login_required @login_required
def favorite(request, status_id): def favorite(request, status_id):
''' like a status ''' ''' like a status '''