From aa8b2c2f2b7ad928e272812749d8f5cd0d3907f1 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 12 Jan 2021 10:44:17 -0800 Subject: [PATCH] Creates landing page views --- bookwyrm/tests/views/test_landing.py | 49 ++++++++++ bookwyrm/urls.py | 11 ++- bookwyrm/views/__init__.py | 1 + bookwyrm/views/helpers.py | 60 ++++++++++++ bookwyrm/views/landing.py | 133 +++++++++++++++++++++++++++ bookwyrm/vviews.py | 118 +----------------------- 6 files changed, 251 insertions(+), 121 deletions(-) create mode 100644 bookwyrm/tests/views/test_landing.py create mode 100644 bookwyrm/views/helpers.py create mode 100644 bookwyrm/views/landing.py diff --git a/bookwyrm/tests/views/test_landing.py b/bookwyrm/tests/views/test_landing.py new file mode 100644 index 00000000..d57f0347 --- /dev/null +++ b/bookwyrm/tests/views/test_landing.py @@ -0,0 +1,49 @@ +''' test for app action functionality ''' +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import models +from bookwyrm import views + + +class LandingViews(TestCase): + ''' pages you land on without really trying ''' + def setUp(self): + ''' we need basic test data and mocks ''' + self.factory = RequestFactory() + self.local_user = models.User.objects.create_user( + 'mouse@local.com', 'mouse@mouse.mouse', 'password', + local=True, localname='mouse') + + + def test_about_page(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.About.as_view() + request = self.factory.get('') + request.user = self.local_user + result = view(request) + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'about.html') + self.assertEqual(result.status_code, 200) + + + def test_feed(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.Feed.as_view() + request = self.factory.get('') + request.user = self.local_user + result = view(request, 'local') + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'feed.html') + self.assertEqual(result.status_code, 200) + + + def test_discover(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.Discover.as_view() + request = self.factory.get('') + result = view(request) + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'discover.html') + self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index c2e3ef36..c1741397 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -55,12 +55,15 @@ urlpatterns = [ re_path(r'^invite/?$', views.ManageInvites.as_view()), re_path(r'^invite/(?P[A-Za-z0-9]+)/?$', views.Invite.as_view()), - re_path(r'^about/?$', vviews.about_page), - path('', vviews.home), - re_path(r'^(?Phome|local|federated)/?$', vviews.home_tab), - re_path(r'^discover/?$', vviews.discover_page), + #landing pages + re_path(r'^about/?$', views.About.as_view()), + path('', views.Home.as_view()), + re_path(r'^(?Phome|local|federated)/?$', views.Feed.as_view()), + re_path(r'^discover/?$', views.Discover.as_view()), + re_path(r'^notifications/?$', vviews.notifications_page), re_path(r'^direct-messages/?$', vviews.direct_messages_page), + re_path(r'^import/?$', vviews.import_page), re_path(r'^import-status/(\d+)/?$', vviews.import_status), re_path(r'^user-edit/?$', vviews.edit_profile_page), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 5d2fda52..03a589dc 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -2,3 +2,4 @@ from .authentication import Login, Register, Logout from .password import PasswordResetRequest, PasswordReset, ChangePassword from .invite import ManageInvites, Invite +from .landing import About, Home, Feed, Discover diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py new file mode 100644 index 00000000..d702f580 --- /dev/null +++ b/bookwyrm/views/helpers.py @@ -0,0 +1,60 @@ +''' helper functions used in various views ''' +from django.db.models import Q +from bookwyrm import models + +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() + + # exclude deleted + queryset = queryset.exclude(deleted=True).order_by('-published_date') + + # 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' + ) + + # filter for only local status + if local_only: + queryset = queryset.filter(user__local=True) + + # remove statuses that have boosts in the same queryset + try: + queryset = queryset.filter(~Q(boosters__in=queryset)) + except ValueError: + pass + + return queryset diff --git a/bookwyrm/views/landing.py b/bookwyrm/views/landing.py new file mode 100644 index 00000000..389ab2ec --- /dev/null +++ b/bookwyrm/views/landing.py @@ -0,0 +1,133 @@ +''' non-interactive pages ''' +from django.contrib.auth.decorators import login_required +from django.core.paginator import Paginator +from django.db.models import Avg, Max +from django.template.response import TemplateResponse +from django.utils.decorators import method_decorator +from django.views import View + +from bookwyrm import forms, models +from bookwyrm.settings import PAGE_LENGTH +from .helpers import get_activity_feed + + +# pylint: disable= no-self-use +@method_decorator(login_required, name='dispatch') +class About(View): + ''' create invites ''' + def get(self, request): + ''' more information about the instance ''' + data = { + 'title': 'About', + } + return TemplateResponse(request, 'about.html', data) + +class Home(View): + ''' discover page or home feed depending on auth ''' + def get(self, request): + ''' this is the same as the feed on the home tab ''' + if request.user.is_authenticated: + feed_view = Feed.as_view() + return feed_view(request, 'home') + discover_view = Discover.as_view() + return discover_view(request) + +class Discover(View): + ''' preview of recently reviewed books ''' + def get(self, request): + ''' tiled book activity page ''' + books = models.Edition.objects.filter( + review__published_date__isnull=False, + review__user__local=True, + review__privacy__in=['public', 'unlisted'], + ).exclude( + cover__exact='' + ).annotate( + Max('review__published_date') + ).order_by('-review__published_date__max')[:6] + + ratings = {} + for book in books: + reviews = models.Review.objects.filter( + book__in=book.parent_work.editions.all() + ) + reviews = get_activity_feed( + request.user, ['public', 'unlisted'], queryset=reviews) + ratings[book.id] = reviews.aggregate(Avg('rating'))['rating__avg'] + data = { + 'title': 'Discover', + 'register_form': forms.RegisterForm(), + 'books': list(set(books)), + 'ratings': ratings + } + return TemplateResponse(request, 'discover.html', data) + + +@method_decorator(login_required, name='dispatch') +class Feed(View): + ''' activity stream ''' + def get(self, request, tab): + ''' user's homepage with activity feed ''' + try: + page = int(request.GET.get('page', 1)) + except ValueError: + page = 1 + + suggested_books = get_suggested_books(request.user) + + if tab == 'home': + activities = get_activity_feed( + request.user, ['public', 'unlisted', 'followers'], + following_only=True) + elif tab == 'local': + activities = get_activity_feed( + request.user, ['public', 'followers'], local_only=True) + else: + activities = get_activity_feed( + request.user, ['public', 'followers']) + paginated = Paginator(activities, PAGE_LENGTH) + activity_page = paginated.page(page) + + prev_page = next_page = None + if activity_page.has_next(): + next_page = '/%s/?page=%d#feed' % \ + (tab, activity_page.next_page_number()) + if activity_page.has_previous(): + prev_page = '/%s/?page=%d#feed' % \ + (tab, activity_page.previous_page_number()) + data = { + 'title': 'Updates Feed', + 'user': request.user, + 'suggested_books': suggested_books, + 'activities': activity_page.object_list, + 'tab': tab, + 'next': next_page, + 'prev': prev_page, + } + return TemplateResponse(request, 'feed.html', data) + + +def get_suggested_books(user, max_books=5): + ''' helper to get a user's recent books ''' + book_count = 0 + preset_shelves = [ + ('reading', max_books), ('read', 2), ('to-read', max_books) + ] + suggested_books = [] + for (preset, shelf_max) in preset_shelves: + limit = shelf_max if shelf_max < (max_books - book_count) \ + else max_books - book_count + shelf = user.shelf_set.get(identifier=preset) + + shelf_books = shelf.shelfbook_set.order_by( + '-updated_date' + ).all()[:limit] + if not shelf_books: + continue + shelf_preview = { + 'name': shelf.name, + 'books': [s.book for s in shelf_books] + } + suggested_books.append(shelf_preview) + book_count += len(shelf_preview['books']) + return suggested_books diff --git a/bookwyrm/vviews.py b/bookwyrm/vviews.py index 17ff8806..0b839561 100644 --- a/bookwyrm/vviews.py +++ b/bookwyrm/vviews.py @@ -4,7 +4,7 @@ import re from django.contrib.auth.decorators import login_required, permission_required from django.contrib.postgres.search import TrigramSimilarity from django.core.paginator import Paginator -from django.db.models import Avg, Q, Max +from django.db.models import Avg, Q from django.db.models.functions import Greatest from django.http import HttpResponseNotFound, JsonResponse from django.core.exceptions import PermissionDenied @@ -64,113 +64,6 @@ def not_found_page(request, _): request, 'notfound.html', {'title': 'Not found'}, status=404) -@require_GET -def home(request): - ''' this is the same as the feed on the home tab ''' - if request.user.is_authenticated: - return home_tab(request, 'home') - return discover_page(request) - - -@login_required -@require_GET -def home_tab(request, tab): - ''' user's homepage with activity feed ''' - try: - page = int(request.GET.get('page', 1)) - except ValueError: - page = 1 - - suggested_books = get_suggested_books(request.user) - - if tab == 'home': - activities = get_activity_feed( - request.user, ['public', 'unlisted', 'followers'], - following_only=True) - elif tab == 'local': - activities = get_activity_feed( - request.user, ['public', 'followers'], local_only=True) - else: - activities = get_activity_feed( - request.user, ['public', 'followers']) - paginated = Paginator(activities, PAGE_LENGTH) - activity_page = paginated.page(page) - - prev_page = next_page = None - if activity_page.has_next(): - next_page = '/%s/?page=%d#feed' % \ - (tab, activity_page.next_page_number()) - if activity_page.has_previous(): - prev_page = '/%s/?page=%d#feed' % \ - (tab, activity_page.previous_page_number()) - data = { - 'title': 'Updates Feed', - 'user': request.user, - 'suggested_books': suggested_books, - 'activities': activity_page.object_list, - 'tab': tab, - 'next': next_page, - 'prev': prev_page, - } - return TemplateResponse(request, 'feed.html', data) - - -def get_suggested_books(user, max_books=5): - ''' helper to get a user's recent books ''' - book_count = 0 - preset_shelves = [ - ('reading', max_books), ('read', 2), ('to-read', max_books) - ] - suggested_books = [] - for (preset, shelf_max) in preset_shelves: - limit = shelf_max if shelf_max < (max_books - book_count) \ - else max_books - book_count - shelf = user.shelf_set.get(identifier=preset) - - shelf_books = shelf.shelfbook_set.order_by( - '-updated_date' - ).all()[:limit] - if not shelf_books: - continue - shelf_preview = { - 'name': shelf.name, - 'books': [s.book for s in shelf_books] - } - suggested_books.append(shelf_preview) - book_count += len(shelf_preview['books']) - return suggested_books - - -@require_GET -def discover_page(request): - ''' tiled book activity page ''' - books = models.Edition.objects.filter( - review__published_date__isnull=False, - review__user__local=True, - review__privacy__in=['public', 'unlisted'], - ).exclude( - cover__exact='' - ).annotate( - Max('review__published_date') - ).order_by('-review__published_date__max')[:6] - - ratings = {} - for book in books: - reviews = models.Review.objects.filter( - book__in=book.parent_work.editions.all() - ) - reviews = get_activity_feed( - request.user, ['public', 'unlisted'], queryset=reviews) - ratings[book.id] = reviews.aggregate(Avg('rating'))['rating__avg'] - data = { - 'title': 'Discover', - 'register_form': forms.RegisterForm(), - 'books': list(set(books)), - 'ratings': ratings - } - return TemplateResponse(request, 'discover.html', data) - - @login_required @require_GET def direct_messages_page(request, page=1): @@ -323,15 +216,6 @@ def import_status(request, job_id): }) -@require_GET -def about_page(request): - ''' more information about the instance ''' - data = { - 'title': 'About', - } - return TemplateResponse(request, 'about.html', data) - - @login_required @require_GET def notifications_page(request):