forked from mirrors/bookwyrm
Creates landing page views
This commit is contained in:
parent
fd6603ee07
commit
aa8b2c2f2b
6 changed files with 251 additions and 121 deletions
49
bookwyrm/tests/views/test_landing.py
Normal file
49
bookwyrm/tests/views/test_landing.py
Normal file
|
@ -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)
|
|
@ -55,12 +55,15 @@ urlpatterns = [
|
||||||
re_path(r'^invite/?$', views.ManageInvites.as_view()),
|
re_path(r'^invite/?$', views.ManageInvites.as_view()),
|
||||||
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.Invite.as_view()),
|
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.Invite.as_view()),
|
||||||
|
|
||||||
re_path(r'^about/?$', vviews.about_page),
|
#landing pages
|
||||||
path('', vviews.home),
|
re_path(r'^about/?$', views.About.as_view()),
|
||||||
re_path(r'^(?P<tab>home|local|federated)/?$', vviews.home_tab),
|
path('', views.Home.as_view()),
|
||||||
re_path(r'^discover/?$', vviews.discover_page),
|
re_path(r'^(?P<tab>home|local|federated)/?$', views.Feed.as_view()),
|
||||||
|
re_path(r'^discover/?$', views.Discover.as_view()),
|
||||||
|
|
||||||
re_path(r'^notifications/?$', vviews.notifications_page),
|
re_path(r'^notifications/?$', vviews.notifications_page),
|
||||||
re_path(r'^direct-messages/?$', vviews.direct_messages_page),
|
re_path(r'^direct-messages/?$', vviews.direct_messages_page),
|
||||||
|
|
||||||
re_path(r'^import/?$', vviews.import_page),
|
re_path(r'^import/?$', vviews.import_page),
|
||||||
re_path(r'^import-status/(\d+)/?$', vviews.import_status),
|
re_path(r'^import-status/(\d+)/?$', vviews.import_status),
|
||||||
re_path(r'^user-edit/?$', vviews.edit_profile_page),
|
re_path(r'^user-edit/?$', vviews.edit_profile_page),
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
from .authentication import Login, Register, Logout
|
from .authentication import Login, Register, Logout
|
||||||
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
||||||
from .invite import ManageInvites, Invite
|
from .invite import ManageInvites, Invite
|
||||||
|
from .landing import About, Home, Feed, Discover
|
||||||
|
|
60
bookwyrm/views/helpers.py
Normal file
60
bookwyrm/views/helpers.py
Normal file
|
@ -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
|
133
bookwyrm/views/landing.py
Normal file
133
bookwyrm/views/landing.py
Normal file
|
@ -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
|
|
@ -4,7 +4,7 @@ import re
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from django.contrib.postgres.search import TrigramSimilarity
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
from django.core.paginator import Paginator
|
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.db.models.functions import Greatest
|
||||||
from django.http import HttpResponseNotFound, JsonResponse
|
from django.http import HttpResponseNotFound, JsonResponse
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
@ -64,113 +64,6 @@ def not_found_page(request, _):
|
||||||
request, 'notfound.html', {'title': 'Not found'}, status=404)
|
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
|
@login_required
|
||||||
@require_GET
|
@require_GET
|
||||||
def direct_messages_page(request, page=1):
|
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
|
@login_required
|
||||||
@require_GET
|
@require_GET
|
||||||
def notifications_page(request):
|
def notifications_page(request):
|
||||||
|
|
Loading…
Reference in a new issue