moviewyrm/bookwyrm/views/feed.py

260 lines
8.6 KiB
Python
Raw Normal View History

2021-03-08 16:49:10 +00:00
""" non-interactive pages """
2021-01-29 18:25:31 +00:00
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q
from django.http import HttpResponseNotFound, Http404
from django.shortcuts import get_object_or_404
2021-01-29 18:25:31 +00:00
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View
2021-03-23 01:39:16 +00:00
from bookwyrm import activitystreams, forms, models
2021-11-24 10:59:45 +00:00
from bookwyrm.models.user import FeedFilterChoices
2021-01-29 18:25:31 +00:00
from bookwyrm.activitypub import ActivitypubResponse
2021-03-23 01:39:16 +00:00
from bookwyrm.settings import PAGE_LENGTH, STREAMS
2021-04-06 15:31:18 +00:00
from bookwyrm.suggested_users import suggested_users
2021-11-24 12:37:09 +00:00
from .helpers import filter_stream_by_status_type, get_user_from_username
from .helpers import is_api_request, is_bookwyrm_request
2021-01-29 18:25:31 +00:00
# pylint: disable= no-self-use
2021-03-08 16:49:10 +00:00
@method_decorator(login_required, name="dispatch")
2021-01-29 18:25:31 +00:00
class Feed(View):
2021-04-26 16:15:42 +00:00
"""activity stream"""
2021-03-08 16:49:10 +00:00
2021-11-21 23:25:47 +00:00
def post(self, request, tab):
2021-11-21 23:46:24 +00:00
"""save feed settings form, with a silent validation fail"""
2021-11-21 23:25:47 +00:00
settings_saved = False
2021-11-22 17:52:57 +00:00
form = forms.FeedStatusTypesForm(request.POST, instance=request.user)
2021-11-21 23:25:47 +00:00
if form.is_valid():
2021-12-09 23:03:01 +00:00
# workaround to avoid broadcasting this change
user = form.save(commit=False)
user.save(broadcast=False, update_fields=["feed_status_types"])
2021-11-21 23:25:47 +00:00
settings_saved = True
return self.get(request, tab, settings_saved)
def get(self, request, tab, settings_saved=False):
2021-04-26 16:15:42 +00:00
"""user's homepage with activity feed"""
2021-08-05 00:53:44 +00:00
tab = [s for s in STREAMS if s["key"] == tab]
2021-08-05 01:22:06 +00:00
tab = tab[0] if tab else STREAMS[0]
2021-03-22 21:11:23 +00:00
2021-08-05 01:22:06 +00:00
activities = activitystreams.streams[tab["key"]].get_activity_stream(
request.user
)
2021-11-21 23:25:47 +00:00
filtered_activities = filter_stream_by_status_type(
activities,
allowed_types=request.user.feed_status_types,
)
paginated = Paginator(filtered_activities, PAGE_LENGTH)
2021-01-29 18:25:31 +00:00
2021-04-06 15:31:18 +00:00
suggestions = suggested_users.get_suggestions(request.user)
2021-03-26 17:32:42 +00:00
2021-03-08 16:49:10 +00:00
data = {
**feed_page_data(request.user),
**{
"user": request.user,
2021-04-19 22:01:20 +00:00
"activities": paginated.get_page(request.GET.get("page")),
2021-04-06 15:31:18 +00:00
"suggested_users": suggestions,
2021-03-08 16:49:10 +00:00
"tab": tab,
2021-08-05 00:53:44 +00:00
"streams": STREAMS,
2021-03-08 16:49:10 +00:00
"goal_form": forms.GoalForm(),
2021-11-24 10:59:45 +00:00
"feed_status_types_options": FeedFilterChoices,
2021-11-24 18:00:30 +00:00
"allowed_status_types": request.user.feed_status_types,
2021-11-21 23:25:47 +00:00
"settings_saved": settings_saved,
2021-09-18 18:32:00 +00:00
"path": f"/{tab['key']}",
2021-03-08 16:49:10 +00:00
},
}
return TemplateResponse(request, "feed/feed.html", data)
2021-01-29 18:25:31 +00:00
2021-03-08 16:49:10 +00:00
@method_decorator(login_required, name="dispatch")
2021-01-29 18:25:31 +00:00
class DirectMessage(View):
2021-04-26 16:15:42 +00:00
"""dm view"""
2021-03-08 16:49:10 +00:00
2021-01-29 19:44:04 +00:00
def get(self, request, username=None):
2021-04-26 16:15:42 +00:00
"""like a feed but for dms only"""
2021-03-23 02:17:46 +00:00
# remove fancy subclasses of status, keep just good ol' notes
2021-10-06 17:37:09 +00:00
activities = (
models.Status.privacy_filter(request.user, privacy_levels=["direct"])
.filter(
review__isnull=True,
comment__isnull=True,
quotation__isnull=True,
generatednote__isnull=True,
)
.order_by("-published_date")
2021-03-23 02:17:46 +00:00
)
2021-01-29 19:44:04 +00:00
user = None
if username:
try:
2021-02-23 20:41:37 +00:00
user = get_user_from_username(request.user, username)
except Http404:
2021-01-29 19:44:04 +00:00
pass
if user:
2021-10-06 17:37:09 +00:00
activities = activities.filter(Q(user=user) | Q(mention_users=user))
2021-01-29 19:44:04 +00:00
2021-01-29 18:25:31 +00:00
paginated = Paginator(activities, PAGE_LENGTH)
2021-03-08 16:49:10 +00:00
data = {
**feed_page_data(request.user),
**{
"user": request.user,
"partner": user,
2021-04-19 22:01:20 +00:00
"activities": paginated.get_page(request.GET.get("page")),
2021-03-08 16:49:10 +00:00
"path": "/direct-messages",
},
}
return TemplateResponse(request, "feed/direct_messages.html", data)
2021-01-29 18:25:31 +00:00
class Status(View):
2021-04-26 16:15:42 +00:00
"""get posting"""
2021-03-08 16:49:10 +00:00
2021-01-29 18:25:31 +00:00
def get(self, request, username, status_id):
2021-04-26 16:15:42 +00:00
"""display a particular status (and replies, etc)"""
user = get_user_from_username(request.user, username)
2021-09-27 23:08:52 +00:00
status = get_object_or_404(
models.Status.objects.select_subclasses(),
user=user,
id=status_id,
deleted=False,
)
2021-01-29 18:25:31 +00:00
# make sure the user is authorized to see the status
status.raise_visible_to_user(request.user)
2021-01-29 18:25:31 +00:00
if is_api_request(request):
return ActivitypubResponse(
2021-03-08 16:49:10 +00:00
status.to_activity(pure=not is_bookwyrm_request(request))
)
2021-10-06 17:37:09 +00:00
visible_thread = (
models.Status.privacy_filter(request.user)
.filter(thread_id=status.thread_id)
.values_list("id", flat=True)
)
2021-10-02 23:55:05 +00:00
visible_thread = list(visible_thread)
2021-10-03 01:24:54 +00:00
ancestors = models.Status.objects.select_subclasses().raw(
"""
WITH RECURSIVE get_thread(depth, id, path) AS (
SELECT 1, st.id, ARRAY[st.id]
FROM bookwyrm_status st
WHERE id = '%s' AND id = ANY(%s)
UNION
SELECT (gt.depth + 1), st.reply_parent_id, path || st.id
FROM get_thread gt, bookwyrm_status st
WHERE st.id = gt.id AND depth < 5 AND st.id = ANY(%s)
)
SELECT * FROM get_thread ORDER BY path DESC;
""",
params=[status.reply_parent_id or 0, visible_thread, visible_thread],
)
2021-10-02 23:56:23 +00:00
children = models.Status.objects.select_subclasses().raw(
"""
WITH RECURSIVE get_thread(depth, id, path) AS (
SELECT 1, st.id, ARRAY[st.id]
FROM bookwyrm_status st
2021-10-02 23:55:05 +00:00
WHERE reply_parent_id = '%s' AND id = ANY(%s)
UNION
SELECT (gt.depth + 1), st.id, path || st.id
FROM get_thread gt, bookwyrm_status st
2021-10-02 23:55:05 +00:00
WHERE st.reply_parent_id = gt.id AND depth < 5 AND st.id = ANY(%s)
)
SELECT * FROM get_thread ORDER BY path;
2021-10-02 23:56:23 +00:00
""",
params=[status.id, visible_thread, visible_thread],
)
preview = None
if hasattr(status, "book"):
preview = status.book.preview_image
elif status.mention_books.exists():
preview = status.mention_books.first().preview_image
2021-03-08 16:49:10 +00:00
data = {
**feed_page_data(request.user),
**{
"status": status,
"children": children,
2021-10-03 01:24:54 +00:00
"ancestors": ancestors,
"preview": preview,
2021-03-08 16:49:10 +00:00
},
}
return TemplateResponse(request, "feed/status.html", data)
2021-01-29 18:25:31 +00:00
class Replies(View):
2021-04-26 16:15:42 +00:00
"""replies page (a json view of status)"""
2021-03-08 16:49:10 +00:00
2021-01-29 18:25:31 +00:00
def get(self, request, username, status_id):
2021-04-26 16:15:42 +00:00
"""ordered collection of replies to a status"""
2021-01-29 18:25:31 +00:00
# the html view is the same as Status
if not is_api_request(request):
status_view = Status.as_view()
return status_view(request, username, status_id)
# the json view is different than Status
status = models.Status.objects.get(id=status_id)
if status.user.localname != username:
return HttpResponseNotFound()
status.raise_visible_to_user(request.user)
2021-01-29 18:25:31 +00:00
return ActivitypubResponse(status.to_replies(**request.GET))
def feed_page_data(user):
2021-04-26 16:15:42 +00:00
"""info we need for every feed page"""
2021-01-29 18:25:31 +00:00
if not user.is_authenticated:
return {}
2021-03-08 16:49:10 +00:00
goal = models.AnnualGoal.objects.filter(user=user, year=timezone.now().year).first()
2021-01-29 18:25:31 +00:00
return {
2021-03-08 16:49:10 +00:00
"suggested_books": get_suggested_books(user),
"goal": goal,
"goal_form": forms.GoalForm(),
2021-01-29 18:25:31 +00:00
}
2021-03-08 16:49:10 +00:00
2021-01-29 18:25:31 +00:00
def get_suggested_books(user, max_books=5):
2021-04-26 16:15:42 +00:00
"""helper to get a user's recent books"""
2021-01-29 18:25:31 +00:00
book_count = 0
2021-03-08 16:49:10 +00:00
preset_shelves = [("reading", max_books), ("read", 2), ("to-read", max_books)]
2021-01-29 18:25:31 +00:00
suggested_books = []
for (preset, shelf_max) in preset_shelves:
2021-03-08 16:49:10 +00:00
limit = (
shelf_max
if shelf_max < (max_books - book_count)
else max_books - book_count
)
2021-01-29 18:25:31 +00:00
shelf = user.shelf_set.get(identifier=preset)
if not shelf.books.exists():
2021-01-29 18:25:31 +00:00
continue
2021-01-29 18:25:31 +00:00
shelf_preview = {
2021-03-08 16:49:10 +00:00
"name": shelf.name,
"identifier": shelf.identifier,
"books": models.Edition.viewer_aware_objects(user)
.filter(
shelfbook__shelf=shelf,
)
2021-10-26 10:01:45 +00:00
.order_by("-shelfbook__shelved_date")
.prefetch_related("authors")[:limit],
2021-01-29 18:25:31 +00:00
}
suggested_books.append(shelf_preview)
2021-03-08 16:49:10 +00:00
book_count += len(shelf_preview["books"])
2021-01-29 18:25:31 +00:00
return suggested_books