mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-02-02 20:32:20 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
b02a70b484
21 changed files with 261 additions and 500 deletions
|
@ -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):
|
||||||
|
|
18
bookwyrm/migrations/0057_auto_20201026_2131.py
Normal file
18
bookwyrm/migrations/0057_auto_20201026_2131.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
Binary file not shown.
Binary file not shown.
|
@ -1,10 +1,10 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'icomoon';
|
font-family: 'icomoon';
|
||||||
src: url('fonts/icomoon.eot?v0wquk');
|
src: url('fonts/icomoon.eot?7ifunb');
|
||||||
src: url('fonts/icomoon.eot?v0wquk#iefix') format('embedded-opentype'),
|
src: url('fonts/icomoon.eot?7ifunb#iefix') format('embedded-opentype'),
|
||||||
url('fonts/icomoon.ttf?v0wquk') format('truetype'),
|
url('fonts/icomoon.ttf?7ifunb') format('truetype'),
|
||||||
url('fonts/icomoon.woff?v0wquk') format('woff'),
|
url('fonts/icomoon.woff?7ifunb') format('woff'),
|
||||||
url('fonts/icomoon.svg?v0wquk#icomoon') format('svg');
|
url('fonts/icomoon.svg?7ifunb#icomoon') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: block;
|
font-display: block;
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
[class^="icon-"], [class*=" icon-"] {
|
[class^="icon-"], [class*=" icon-"] {
|
||||||
/* use !important to prevent issues with browser extensions that change fonts */
|
/* use !important to prevent issues with browser extensions that change fonts */
|
||||||
font-family: 'icomoon' !important;
|
font-family: 'icomoon' !important;
|
||||||
speak: none;
|
speak: never;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-variant: normal;
|
font-variant: normal;
|
||||||
|
@ -25,26 +25,71 @@
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-arrow-right:before {
|
.icon-envelope:before {
|
||||||
content: "\e900";
|
content: "\e900";
|
||||||
}
|
}
|
||||||
.icon-arrow-left:before {
|
.icon-arrow-right:before {
|
||||||
content: "\e910";
|
content: "\e901";
|
||||||
}
|
}
|
||||||
.icon-arrow-up:before {
|
.icon-bell:before {
|
||||||
content: "\e911";
|
content: "\e902";
|
||||||
}
|
|
||||||
.icon-arrow-down:before {
|
|
||||||
content: "\e912";
|
|
||||||
}
|
}
|
||||||
.icon-x:before {
|
.icon-x:before {
|
||||||
content: "\e902";
|
content: "\e903";
|
||||||
}
|
}
|
||||||
.icon-cancel:before {
|
.icon-quote-close:before {
|
||||||
content: "\e902";
|
content: "\e904";
|
||||||
}
|
}
|
||||||
.icon-close:before {
|
.icon-quote-open:before {
|
||||||
content: "\e902";
|
content: "\e905";
|
||||||
|
}
|
||||||
|
.icon-image:before {
|
||||||
|
content: "\e906";
|
||||||
|
}
|
||||||
|
.icon-pencil:before {
|
||||||
|
content: "\e907";
|
||||||
|
}
|
||||||
|
.icon-list:before {
|
||||||
|
content: "\e908";
|
||||||
|
}
|
||||||
|
.icon-unlock:before {
|
||||||
|
content: "\e909";
|
||||||
|
}
|
||||||
|
.icon-globe:before {
|
||||||
|
content: "\e90a";
|
||||||
|
}
|
||||||
|
.icon-lock:before {
|
||||||
|
content: "\e90b";
|
||||||
|
}
|
||||||
|
.icon-chain-broken:before {
|
||||||
|
content: "\e90c";
|
||||||
|
}
|
||||||
|
.icon-chain:before {
|
||||||
|
content: "\e90d";
|
||||||
|
}
|
||||||
|
.icon-comments:before {
|
||||||
|
content: "\e90e";
|
||||||
|
}
|
||||||
|
.icon-comment:before {
|
||||||
|
content: "\e90f";
|
||||||
|
}
|
||||||
|
.icon-boost:before {
|
||||||
|
content: "\e910";
|
||||||
|
}
|
||||||
|
.icon-arrow-left:before {
|
||||||
|
content: "\e911";
|
||||||
|
}
|
||||||
|
.icon-arrow-up:before {
|
||||||
|
content: "\e912";
|
||||||
|
}
|
||||||
|
.icon-arrow-down:before {
|
||||||
|
content: "\e913";
|
||||||
|
}
|
||||||
|
.icon-home:before {
|
||||||
|
content: "\e914";
|
||||||
|
}
|
||||||
|
.icon-local:before {
|
||||||
|
content: "\e915";
|
||||||
}
|
}
|
||||||
.icon-search:before {
|
.icon-search:before {
|
||||||
content: "\e986";
|
content: "\e986";
|
||||||
|
@ -61,78 +106,3 @@
|
||||||
.icon-heart:before {
|
.icon-heart:before {
|
||||||
content: "\e9da";
|
content: "\e9da";
|
||||||
}
|
}
|
||||||
.icon-local:before {
|
|
||||||
content: "\e914";
|
|
||||||
}
|
|
||||||
.icon-home:before {
|
|
||||||
content: "\e913";
|
|
||||||
}
|
|
||||||
.icon-quote-close:before {
|
|
||||||
content: "\e903";
|
|
||||||
}
|
|
||||||
.icon-quote-open:before {
|
|
||||||
content: "\e904";
|
|
||||||
}
|
|
||||||
.icon-image:before {
|
|
||||||
content: "\e905";
|
|
||||||
}
|
|
||||||
.icon-photo:before {
|
|
||||||
content: "\e905";
|
|
||||||
}
|
|
||||||
.icon-picture-o:before {
|
|
||||||
content: "\e905";
|
|
||||||
}
|
|
||||||
.icon-pencil:before {
|
|
||||||
content: "\e906";
|
|
||||||
}
|
|
||||||
.icon-list:before {
|
|
||||||
content: "\e907";
|
|
||||||
}
|
|
||||||
.icon-unlock:before {
|
|
||||||
content: "\e908";
|
|
||||||
}
|
|
||||||
.icon-unlisted:before {
|
|
||||||
content: "\e908";
|
|
||||||
}
|
|
||||||
.icon-globe:before {
|
|
||||||
content: "\e909";
|
|
||||||
}
|
|
||||||
.icon-global:before {
|
|
||||||
content: "\e909";
|
|
||||||
}
|
|
||||||
.icon-federated:before {
|
|
||||||
content: "\e909";
|
|
||||||
}
|
|
||||||
.icon-public:before {
|
|
||||||
content: "\e909";
|
|
||||||
}
|
|
||||||
.icon-lock:before {
|
|
||||||
content: "\e90a";
|
|
||||||
}
|
|
||||||
.icon-private:before {
|
|
||||||
content: "\e90a";
|
|
||||||
}
|
|
||||||
.icon-chain-broken:before {
|
|
||||||
content: "\e90b";
|
|
||||||
}
|
|
||||||
.icon-unlink:before {
|
|
||||||
content: "\e90b";
|
|
||||||
}
|
|
||||||
.icon-chain:before {
|
|
||||||
content: "\e90c";
|
|
||||||
}
|
|
||||||
.icon-link:before {
|
|
||||||
content: "\e90c";
|
|
||||||
}
|
|
||||||
.icon-comments:before {
|
|
||||||
content: "\e90d";
|
|
||||||
}
|
|
||||||
.icon-comment:before {
|
|
||||||
content: "\e90e";
|
|
||||||
}
|
|
||||||
.icon-boost:before {
|
|
||||||
content: "\e90f";
|
|
||||||
}
|
|
||||||
.icon-bell:before {
|
|
||||||
content: "\e901";
|
|
||||||
}
|
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
|
@ -47,11 +47,11 @@
|
||||||
<div id="mainNav" class="navbar-menu toggle-content">
|
<div id="mainNav" class="navbar-menu toggle-content">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<a href="" class="navbar-item">
|
<a href="/user/{{ request.user.localname }}/shelves" class="navbar-item">
|
||||||
Lists
|
Your shelves
|
||||||
</a>
|
</a>
|
||||||
<a href="" class="navbar-item">
|
<a href="/#feed" class="navbar-item">
|
||||||
Groups
|
Feed
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
<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 }}">
|
||||||
<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 }}'">
|
||||||
|
@ -42,7 +43,17 @@
|
||||||
|
|
||||||
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_review"></textarea>
|
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_review"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button class="button is-primary" type="submit">post review</button>
|
<div class="control is-grouped">
|
||||||
|
<div class="select">
|
||||||
|
<select name="privacy">
|
||||||
|
<option value="public" selected>Public</option>
|
||||||
|
<option value="unlisted">Unlisted</option>
|
||||||
|
<option value="followers">Followers only</option>
|
||||||
|
<option value="direct">Private</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="button is-primary" type="submit">post review</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -51,11 +62,24 @@
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<button class="button is-primary" type="submit">post comment</button>
|
<div class="control is-grouped">
|
||||||
|
<div class="select">
|
||||||
|
<select name="privacy">
|
||||||
|
<option value="public" selected>Public</option>
|
||||||
|
<option value="unlisted">Unlisted</option>
|
||||||
|
<option value="followers">Followers only</option>
|
||||||
|
<option value="direct">Private</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="button is-primary" type="submit">post comment</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -64,6 +88,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>
|
||||||
|
@ -72,7 +99,17 @@
|
||||||
<label class="label" for="id_content_{{ book.id }}_quote">Comment:</label>
|
<label class="label" for="id_content_{{ book.id }}_quote">Comment:</label>
|
||||||
<textarea name="content" class="textarea is-small" id="id_content_{{ book.id }}_quote"></textarea>
|
<textarea name="content" class="textarea is-small" id="id_content_{{ book.id }}_quote"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button class="button is-primary" type="submit">post quote</button>
|
<div class="control is-grouped">
|
||||||
|
<div class="select">
|
||||||
|
<select name="privacy">
|
||||||
|
<option value="public" selected>Public</option>
|
||||||
|
<option value="unlisted">Unlisted</option>
|
||||||
|
<option value="followers">Followers only</option>
|
||||||
|
<option value="direct">Private</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="button is-primary" type="submit">post quote</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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="{{ activity.privacy }}">
|
||||||
<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">
|
||||||
|
|
|
@ -23,15 +23,29 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
<span class="icon icon-public">
|
{% if status.privacy == 'public' %}
|
||||||
|
<span class="icon icon-globe">
|
||||||
<span class="is-sr-only">Public post</span>
|
<span class="is-sr-only">Public post</span>
|
||||||
</span>
|
</span>
|
||||||
|
{% elif status.privacy == 'unlisted' %}
|
||||||
|
<span class="icon icon-unlock">
|
||||||
|
<span class="is-sr-only">Unlisted post</span>
|
||||||
|
</span>
|
||||||
|
{% elif status.privacy == 'followers' %}
|
||||||
|
<span class="icon icon-lock">
|
||||||
|
<span class="is-sr-only">Followers-only post</span>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="icon icon-envelope">
|
||||||
|
<span class="is-sr-only">Private post</span>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
{% if status.user == request.user %}
|
{% if status.user == request.user %}
|
||||||
<form name="delete-{{status.id}}" action="/delete-status" method="post">
|
<form name="delete-{{status.id}}" action="/delete-status" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="status" value="{{ status.id }}">
|
<input type="hidden" name="status" value="{{ status.id }}">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
<span class="icon icon-cancel">
|
<span class="icon icon-x">
|
||||||
<span class="is-sr-only">Delete post</span>
|
<span class="is-sr-only">Delete post</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
from . import *
|
|
|
@ -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')
|
|
|
@ -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')
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -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 '''
|
||||||
|
|
|
@ -130,17 +130,22 @@ def get_activity_feed(user, filter_level, model=models.Status):
|
||||||
if filter_level in ['friends', 'home']:
|
if filter_level in ['friends', '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(user__in=following, privacy__in=['public', 'unlisted', 'followers']) | \
|
||||||
Q(mention_users=user)
|
Q(mention_users=user) | Q(user=user)
|
||||||
)
|
)
|
||||||
elif filter_level == 'self':
|
elif filter_level == 'self':
|
||||||
activities = activities.filter(user=user, privacy='public')
|
activities = activities.filter(user=user, privacy='public')
|
||||||
elif filter_level == 'local':
|
elif filter_level == 'local':
|
||||||
# everyone on this instance
|
# everyone on this instance except unlisted
|
||||||
activities = activities.filter(user__local=True, privacy='public')
|
activities = activities.filter(
|
||||||
|
Q(user__in=following, privacy='followers') | Q(privacy='public'),
|
||||||
|
user__local=True
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# all activities from everyone you federate with
|
# all activities from everyone you federate with
|
||||||
activities = activities.filter(privacy='public')
|
activities = activities.filter(
|
||||||
|
Q(user__in=following, privacy='followers') | Q(privacy='public')
|
||||||
|
)
|
||||||
|
|
||||||
return activities
|
return activities
|
||||||
|
|
||||||
|
@ -386,9 +391,14 @@ def status_page(request, username, status_id):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
# the url should have the poster's username in it
|
||||||
if user != status.user:
|
if user != status.user:
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
# make sure the user is authorized to see the status
|
||||||
|
if not status_visible_to_user(request.user, status):
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
if is_api_request(request):
|
if is_api_request(request):
|
||||||
return JsonResponse(status.to_activity(), encoder=ActivityEncoder)
|
return JsonResponse(status.to_activity(), encoder=ActivityEncoder)
|
||||||
|
|
||||||
|
@ -397,6 +407,19 @@ def status_page(request, username, status_id):
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, 'status.html', data)
|
return TemplateResponse(request, 'status.html', data)
|
||||||
|
|
||||||
|
def status_visible_to_user(viewer, status):
|
||||||
|
''' is a user authorized to view a status? '''
|
||||||
|
if viewer == status.user or status.privacy in ['public', 'unlisted']:
|
||||||
|
return True
|
||||||
|
if status.privacy == 'followers' and \
|
||||||
|
status.user.followers.filter(id=viewer.id).first():
|
||||||
|
return True
|
||||||
|
if status.privacy == 'direct' and \
|
||||||
|
status.mention_users.filter(id=viewer.id).first():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def replies_page(request, username, status_id):
|
def replies_page(request, username, status_id):
|
||||||
|
|
Loading…
Reference in a new issue