Merge branch 'main' into shelve-buttons

This commit is contained in:
Mouse Reeve
2021-02-09 13:28:00 -08:00
committed by GitHub
84 changed files with 1873 additions and 326 deletions

View File

@ -14,6 +14,7 @@ from .import_data import Import, ImportStatus
from .interaction import Favorite, Unfavorite, Boost, Unboost
from .invite import ManageInvites, Invite
from .landing import About, Home, Discover
from .list import Lists, List, Curate, UserLists
from .notifications import Notifications
from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough, delete_readthrough

View File

@ -35,6 +35,7 @@ class Goal(View):
'goal': goal,
'user': user,
'year': year,
'is_self': request.user == user,
}
return TemplateResponse(request, 'goal.html', data)
@ -70,10 +71,15 @@ class Goal(View):
broadcast(
request.user,
status.to_create_activity(request.user),
privacy=status.privacy,
software='bookwyrm')
# re-format the activity for non-bookwyrm servers
remote_activity = status.to_create_activity(request.user, pure=True)
broadcast(request.user, remote_activity, software='other')
broadcast(
request.user,
remote_activity,
privacy=status.privacy,
software='other')
return redirect(request.headers.get('Referer', '/'))

View File

@ -59,11 +59,55 @@ def object_visible_to_user(viewer, obj):
return True
return False
def privacy_filter(viewer, queryset, privacy_levels, following_only=False):
''' filter objects that have "user" and "privacy" fields '''
# exclude blocks from both directions
if not viewer.is_anonymous:
blocked = models.User.objects.filter(id__in=viewer.blocks.all()).all()
queryset = queryset.exclude(
Q(user__in=blocked) | Q(user__blocks=viewer))
# you can't see followers only or direct messages if you're not logged in
if viewer.is_anonymous:
privacy_levels = [p for p in privacy_levels if \
not p in ['followers', 'direct']]
# filter to only privided privacy levels
queryset = queryset.filter(privacy__in=privacy_levels)
# only include statuses the user follows
if following_only:
queryset = queryset.exclude(
~Q(# remove everythign except
Q(user__in=viewer.following.all()) | # user following
Q(user=viewer) |# is self
Q(mention_users=viewer)# mentions user
),
)
# exclude followers-only statuses the user doesn't follow
elif 'followers' in privacy_levels:
queryset = queryset.exclude(
~Q(# user isn't following and it isn't their own status
Q(user__in=viewer.following.all()) | Q(user=viewer)
),
privacy='followers' # and the status is followers only
)
# exclude direct messages not intended for the user
if 'direct' in privacy_levels:
queryset = queryset.exclude(
~Q(
Q(user=viewer) | Q(mention_users=viewer)
), privacy='direct'
)
return queryset
def get_activity_feed(
user, privacy, local_only=False, following_only=False,
queryset=models.Status.objects):
''' get a filtered queryset of statuses '''
privacy = privacy if isinstance(privacy, list) else [privacy]
# if we're looking at Status, we need this. We don't if it's Comment
if hasattr(queryset, 'select_subclasses'):
queryset = queryset.select_subclasses()
@ -71,44 +115,10 @@ def get_activity_feed(
# exclude deleted
queryset = queryset.exclude(deleted=True).order_by('-published_date')
# exclude blocks from both directions
if not user.is_anonymous:
blocked = models.User.objects.filter(id__in=user.blocks.all()).all()
queryset = queryset.exclude(
Q(user__in=blocked) | Q(user__blocks=user))
# you can't see followers only or direct messages if you're not logged in
if user.is_anonymous:
privacy = [p for p in privacy if not p in ['followers', 'direct']]
# filter to only privided privacy levels
queryset = queryset.filter(privacy__in=privacy)
# only include statuses the user follows
if following_only:
queryset = queryset.exclude(
~Q(# remove everythign except
Q(user__in=user.following.all()) | # user follwoing
Q(user=user) |# is self
Q(mention_users=user)# mentions user
),
)
# exclude followers-only statuses the user doesn't follow
elif 'followers' in privacy:
queryset = queryset.exclude(
~Q(# user isn't following and it isn't their own status
Q(user__in=user.following.all()) | Q(user=user)
),
privacy='followers' # and the status is followers only
)
# exclude direct messages not intended for the user
if 'direct' in privacy:
queryset = queryset.exclude(
~Q(
Q(user=user) | Q(mention_users=user)
), privacy='direct'
)
# apply privacy filters
privacy = privacy if isinstance(privacy, list) else [privacy]
queryset = privacy_filter(
user, queryset, privacy, following_only=following_only)
# filter for only local status
if local_only:

253
bookwyrm/views/list.py Normal file
View File

@ -0,0 +1,253 @@
''' book list views'''
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Count, Q
from django.http import HttpResponseNotFound, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from bookwyrm.connectors import connector_manager
from .helpers import is_api_request, object_visible_to_user, privacy_filter
from .helpers import get_user_from_username
# pylint: disable=no-self-use
class Lists(View):
''' book list page '''
def get(self, request):
''' display a book list '''
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
user = request.user if request.user.is_authenticated else None
# hide lists with no approved books
lists = models.List.objects.filter(
~Q(user=user),
).annotate(
item_count=Count('listitem', filter=Q(listitem__approved=True))
).filter(
item_count__gt=0
).distinct().all()
lists = privacy_filter(request.user, lists, ['public', 'followers'])
paginated = Paginator(lists, 12)
data = {
'title': 'Lists',
'lists': paginated.page(page),
'list_form': forms.ListForm(),
'path': '/list',
}
return TemplateResponse(request, 'lists/lists.html', data)
@method_decorator(login_required, name='dispatch')
# pylint: disable=unused-argument
def post(self, request):
''' create a book_list '''
form = forms.ListForm(request.POST)
if not form.is_valid():
return redirect('lists')
book_list = form.save()
# let the world know
broadcast(
request.user,
book_list.to_create_activity(request.user),
privacy=book_list.privacy,
software='bookwyrm'
)
return redirect(book_list.local_path)
class UserLists(View):
''' a user's book list page '''
def get(self, request, username):
''' display a book list '''
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
user = get_user_from_username(username)
lists = models.List.objects.filter(user=user).all()
lists = privacy_filter(
request.user, lists, ['public', 'followers', 'unlisted'])
paginated = Paginator(lists, 12)
data = {
'title': '%s: Lists' % user.name,
'user': user,
'is_self': request.user.id == user.id,
'lists': paginated.page(page),
'list_form': forms.ListForm(),
'path': user.local_path + '/lists',
}
return TemplateResponse(request, 'user/lists.html', data)
class List(View):
''' book list page '''
def get(self, request, list_id):
''' display a book list '''
book_list = get_object_or_404(models.List, id=list_id)
if not object_visible_to_user(request.user, book_list):
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(book_list.to_activity(**request.GET))
query = request.GET.get('q')
suggestions = None
if query and request.user.is_authenticated:
# search for books
suggestions = connector_manager.local_search(query, raw=True)
elif request.user.is_authenticated:
# just suggest whatever books are nearby
suggestions = request.user.shelfbook_set.filter(
~Q(book__in=book_list.books.all())
)
suggestions = [s.book for s in suggestions[:5]]
if len(suggestions) < 5:
suggestions += [
s.default_edition for s in \
models.Work.objects.filter(
~Q(editions__in=book_list.books.all()),
).order_by('-updated_date')
][:5 - len(suggestions)]
data = {
'title': '%s | Lists' % book_list.name,
'list': book_list,
'items': book_list.listitem_set.filter(approved=True),
'pending_count': book_list.listitem_set.filter(
approved=False).count(),
'suggested_books': suggestions,
'list_form': forms.ListForm(instance=book_list),
'query': query or ''
}
return TemplateResponse(request, 'lists/list.html', data)
@method_decorator(login_required, name='dispatch')
# pylint: disable=unused-argument
def post(self, request, list_id):
''' edit a book_list '''
book_list = get_object_or_404(models.List, id=list_id)
form = forms.ListForm(request.POST, instance=book_list)
if not form.is_valid():
return redirect('list', book_list.id)
book_list = form.save()
# let the world know
broadcast(
request.user,
book_list.to_update_activity(request.user),
privacy=book_list.privacy,
software='bookwyrm'
)
return redirect(book_list.local_path)
class Curate(View):
''' approve or discard list suggestsions '''
@method_decorator(login_required, name='dispatch')
def get(self, request, list_id):
''' display a pending list '''
book_list = get_object_or_404(models.List, id=list_id)
if not book_list.user == request.user:
# only the creater can curate the list
return HttpResponseNotFound()
data = {
'title': 'Curate "%s" | Lists' % book_list.name,
'list': book_list,
'pending': book_list.listitem_set.filter(approved=False),
'list_form': forms.ListForm(instance=book_list),
}
return TemplateResponse(request, 'lists/curate.html', data)
@method_decorator(login_required, name='dispatch')
# pylint: disable=unused-argument
def post(self, request, list_id):
''' edit a book_list '''
book_list = get_object_or_404(models.List, id=list_id)
suggestion = get_object_or_404(
models.ListItem, id=request.POST.get('item'))
approved = request.POST.get('approved') == 'true'
if approved:
suggestion.approved = True
suggestion.save()
# let the world know
broadcast(
request.user,
suggestion.to_add_activity(request.user),
privacy=book_list.privacy,
software='bookwyrm'
)
else:
suggestion.delete()
return redirect('list-curate', book_list.id)
@require_POST
def add_book(request, list_id):
''' put a book on a list '''
book_list = get_object_or_404(models.List, id=list_id)
if not object_visible_to_user(request.user, book_list):
return HttpResponseNotFound()
book = get_object_or_404(models.Edition, id=request.POST.get('book'))
# do you have permission to add to the list?
if request.user == book_list.user or book_list.curation == 'open':
# go ahead and add it
item = models.ListItem.objects.create(
book=book,
book_list=book_list,
added_by=request.user,
)
# let the world know
broadcast(
request.user,
item.to_add_activity(request.user),
privacy=book_list.privacy,
software='bookwyrm'
)
elif book_list.curation == 'curated':
# make a pending entry
models.ListItem.objects.create(
approved=False,
book=book,
book_list=book_list,
added_by=request.user,
)
else:
# you can't add to this list, what were you THINKING
return HttpResponseBadRequest()
return redirect('list', list_id)
@require_POST
def remove_book(request, list_id):
''' put a book on a list '''
book_list = get_object_or_404(models.List, id=list_id)
item = get_object_or_404(models.ListItem, id=request.POST.get('item'))
if not book_list.user == request.user and not item.added_by == request.user:
return HttpResponseNotFound()
activity = item.to_remove_activity(request.user)
item.delete()
# let the world know
broadcast(
request.user,
activity,
privacy=book_list.privacy,
software='bookwyrm'
)
return redirect('list', list_id)

View File

@ -10,7 +10,7 @@ from django.views import View
from bookwyrm import models
from bookwyrm.connectors import connector_manager
from bookwyrm.utils import regex
from .helpers import is_api_request
from .helpers import is_api_request, privacy_filter
from .helpers import handle_remote_webfinger
@ -32,7 +32,7 @@ class Search(View):
if re.match(r'\B%s' % regex.full_username, query):
handle_remote_webfinger(query)
# do a local user search
# do a user search
user_results = models.User.objects.annotate(
similarity=Greatest(
TrigramSimilarity('username', query),
@ -42,12 +42,25 @@ class Search(View):
similarity__gt=0.5,
).order_by('-similarity')[:10]
# any relevent lists?
list_results = privacy_filter(
request.user, models.List.objects, ['public', 'followers']
).annotate(
similarity=Greatest(
TrigramSimilarity('name', query),
TrigramSimilarity('description', query),
)
).filter(
similarity__gt=0.1,
).order_by('-similarity')[:10]
book_results = connector_manager.search(
query, min_confidence=min_confidence)
data = {
'title': 'Search Results',
'book_results': book_results,
'user_results': user_results,
'list_results': list_results,
'query': query,
}
return TemplateResponse(request, 'search_results.html', data)

View File

@ -140,7 +140,12 @@ def shelve(request):
pass
shelfbook = models.ShelfBook.objects.create(
book=book, shelf=desired_shelf, added_by=request.user)
broadcast(request.user, shelfbook.to_add_activity(request.user))
broadcast(
request.user,
shelfbook.to_add_activity(request.user),
privacy=shelfbook.shelf.privacy,
software='bookwyrm'
)
# post about "want to read" shelves
if desired_shelf.identifier == 'to-read' and \
@ -173,4 +178,4 @@ def handle_unshelve(user, book, shelf):
activity = row.to_remove_activity(user)
row.delete()
broadcast(user, activity)
broadcast(user, activity, privacy=shelf.privacy, software='bookwyrm')