Moves user views to class view

This commit is contained in:
Mouse Reeve 2021-01-12 12:05:30 -08:00
parent 8693895bc6
commit 85d01d5df0
11 changed files with 316 additions and 269 deletions

View file

@ -82,7 +82,7 @@
</a>
</li>
<li>
<a href="/user-edit" class="navbar-item">
<a href="/edit-profile" class="navbar-item">
Settings
</a>
</li>

View file

@ -7,7 +7,7 @@
</div>
{% if is_self %}
<div class="column is-narrow">
<a href="/user-edit/">
<a href="/edit-profile">
<span class="icon icon-pencil">
<span class="is-sr-only">Edit profile</span>
</span>

View file

@ -221,83 +221,6 @@ class Views(TestCase):
response.context_data['user_results'][0], self.local_user)
def test_import_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
result = views.import_page(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'import.html')
self.assertEqual(result.status_code, 200)
def test_import_status(self):
''' there are so many views, this just makes sure it LOADS '''
import_job = models.ImportJob.objects.create(user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.tasks.app.AsyncResult') as async_result:
async_result.return_value = []
result = views.import_status(request, import_job.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'import_status.html')
self.assertEqual(result.status_code, 200)
def test_user_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.user_page(request, 'mouse')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'user.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.user_page(request, 'mouse')
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_followers_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.followers_page(request, 'mouse')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'followers.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.followers_page(request, 'mouse')
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_following_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.following_page(request, 'mouse')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'following.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.following_page(request, 'mouse')
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_status_page(self):
''' there are so many views, this just makes sure it LOADS '''
status = models.Status.objects.create(

View file

@ -0,0 +1,99 @@
''' test for app action functionality '''
from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
from bookwyrm.activitypub import ActivitypubResponse
class UserViews(TestCase):
''' view user and edit profile '''
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_user_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.User.as_view()
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'mouse')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'user.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = True
result = view(request, 'mouse')
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_followers_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Followers.as_view()
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'mouse')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'followers.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = True
result = view(request, 'mouse')
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_following_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Following.as_view()
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'mouse')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'following.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = True
result = view(request, 'mouse')
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_edit_profile_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.EditUser.as_view()
request = self.factory.get('')
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'edit_user.html')
self.assertEqual(result.status_code, 200)
def test_edit_user(self):
''' use a form to update a user '''
view = views.EditUser.as_view()
form = forms.EditUserForm(instance=self.local_user)
form.data['name'] = 'New Name'
request = self.factory.post('', form.data)
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request)
self.assertEqual(self.local_user.name, 'New Name')

View file

@ -69,16 +69,14 @@ urlpatterns = [
re_path(r'^import/?$', views.Import.as_view()),
re_path(r'^import/(\d+)/?$', views.ImportStatus.as_view()),
re_path(r'^user-edit/?$', vviews.edit_profile_page),
# should return a ui view or activitypub json blob as requested
# users
re_path(r'%s/?$' % user_path, vviews.user_page),
re_path(r'%s\.json$' % local_user_path, vviews.user_page),
re_path(r'%s/?$' % local_user_path, vviews.user_page),
re_path(r'%s/shelves/?$' % local_user_path, vviews.user_shelves_page),
re_path(r'%s/followers(.json)?/?$' % local_user_path, vviews.followers_page),
re_path(r'%s/following(.json)?/?$' % local_user_path, vviews.following_page),
re_path(r'%s/?$' % user_path, views.User.as_view()),
re_path(r'%s\.json$' % user_path, views.User.as_view()),
re_path(r'%s/shelves/?$' % user_path, vviews.user_shelves_page),
re_path(r'%s/followers(.json)?/?$' % user_path, views.Followers.as_view()),
re_path(r'%s/following(.json)?/?$' % user_path, views.Following.as_view()),
re_path(r'^edit-profile/?$', views.EditUser.as_view()),
# statuses
re_path(r'%s(.json)?/?$' % status_path, vviews.status_page),
@ -102,7 +100,6 @@ urlpatterns = [
re_path(r'^search/?$', vviews.search),
# internal action endpoints
re_path(r'^edit-profile/?$', actions.edit_profile),
re_path(r'^resolve-book/?$', actions.resolve_book),
re_path(r'^edit-book/(?P<book_id>\d+)/?$', actions.edit_book),

View file

@ -1,13 +1,8 @@
''' views for actions you can take in the application '''
from io import BytesIO
from uuid import uuid4
from PIL import Image
import dateutil.parser
from dateutil.parser import ParserError
from django.contrib.auth.decorators import login_required, permission_required
from django.core.files.base import ContentFile
from django.db import transaction
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect
@ -21,50 +16,6 @@ from bookwyrm.broadcast import broadcast
from bookwyrm.vviews import get_user_from_username, get_edition
@login_required
@require_POST
def edit_profile(request):
''' les get fancy with images '''
form = forms.EditUserForm(
request.POST, request.FILES, instance=request.user)
if not form.is_valid():
data = {'form': form, 'user': request.user}
return TemplateResponse(request, 'edit_user.html', data)
user = form.save(commit=False)
if 'avatar' in form.files:
# crop and resize avatar upload
image = Image.open(form.files['avatar'])
target_size = 120
width, height = image.size
thumbnail_scale = height / (width / target_size) if height > width \
else width / (height / target_size)
image.thumbnail([thumbnail_scale, thumbnail_scale])
width, height = image.size
width_diff = width - target_size
height_diff = height - target_size
cropped = image.crop((
int(width_diff / 2),
int(height_diff / 2),
int(width - (width_diff / 2)),
int(height - (height_diff / 2))
))
output = BytesIO()
cropped.save(output, format=image.format)
ContentFile(output.getvalue())
# set the name to a hash
extension = form.files['avatar'].name.split('.')[-1]
filename = '%s.%s' % (uuid4(), extension)
user.avatar.save(filename, ContentFile(output.getvalue()))
user.save()
broadcast(user, user.to_update_activity(user))
return redirect('/user/%s' % request.user.localname)
@require_POST
def resolve_book(request):
''' figure out the local path to a book from a remote_id '''

View file

@ -6,3 +6,4 @@ from .landing import About, Home, Feed, Discover
from .notifications import Notifications
from .direct_message import DirectMessage
from .import_data import Import, ImportStatus
from .user import User, EditUser, Followers, Following

View file

@ -2,6 +2,21 @@
from django.db.models import Q
from bookwyrm import models
def get_user_from_username(username):
''' helper function to resolve a localname or a username to a user '''
# raises DoesNotExist if user is now found
try:
return models.User.objects.get(localname=username)
except models.User.DoesNotExist:
return models.User.objects.get(username=username)
def is_api_request(request):
''' check whether a request is asking for html or data '''
return 'json' in request.headers.get('Accept') or \
request.path[-5:] == '.json'
def get_activity_feed(
user, privacy, local_only=False, following_only=False,
queryset=models.Status.objects):

View file

@ -94,7 +94,7 @@ class ChangePassword(View):
confirm_password = request.POST.get('confirm-password')
if new_password != confirm_password:
return redirect('/user-edit')
return redirect('/edit-profile')
request.user.set_password(new_password)
request.user.save()

192
bookwyrm/views/user.py Normal file
View file

@ -0,0 +1,192 @@
''' non-interactive pages '''
from io import BytesIO
from uuid import uuid4
from PIL import Image
from django.contrib.auth.decorators import login_required
from django.core.files.base import ContentFile
from django.core.paginator import Paginator
from django.http import HttpResponseNotFound
from django.shortcuts import redirect
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.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_activity_feed, get_user_from_username, is_api_request
# pylint: disable= no-self-use
class User(View):
''' user profile page '''
def get(self, request, username):
''' profile page for a user '''
try:
user = get_user_from_username(username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
if is_api_request(request):
# we have a json request
return ActivitypubResponse(user.to_activity())
# otherwise we're at a UI view
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
shelf_preview = []
# only show other shelves that should be visible
shelves = user.shelf_set
is_self = request.user.id == user.id
if not is_self:
follower = user.followers.filter(id=request.user.id).exists()
if follower:
shelves = shelves.filter(privacy__in=['public', 'followers'])
else:
shelves = shelves.filter(privacy='public')
for user_shelf in shelves.all():
if not user_shelf.books.count():
continue
shelf_preview.append({
'name': user_shelf.name,
'local_path': user_shelf.local_path,
'books': user_shelf.books.all()[:3],
'size': user_shelf.books.count(),
})
if len(shelf_preview) > 2:
break
# user's posts
activities = get_activity_feed(
request.user,
['public', 'unlisted', 'followers'],
queryset=models.Status.objects.filter(user=user)
)
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)
prev_page = next_page = None
if activity_page.has_next():
next_page = '/user/%s/?page=%d' % \
(username, activity_page.next_page_number())
if activity_page.has_previous():
prev_page = '/user/%s/?page=%d' % \
(username, activity_page.previous_page_number())
data = {
'title': user.name,
'user': user,
'is_self': is_self,
'shelves': shelf_preview,
'shelf_count': shelves.count(),
'activities': activity_page.object_list,
'next': next_page,
'prev': prev_page,
}
return TemplateResponse(request, 'user.html', data)
class Followers(View):
''' list of followers view '''
def get(self, request, username):
''' list of followers '''
try:
user = get_user_from_username(username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(
user.to_followers_activity(**request.GET))
data = {
'title': '%s: followers' % user.name,
'user': user,
'is_self': request.user.id == user.id,
'followers': user.followers.all(),
}
return TemplateResponse(request, 'followers.html', data)
class Following(View):
''' list of following view '''
def get(self, request, username):
''' list of followers '''
try:
user = get_user_from_username(username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(
user.to_following_activity(**request.GET))
data = {
'title': '%s: following' % user.name,
'user': user,
'is_self': request.user.id == user.id,
'following': user.following.all(),
}
return TemplateResponse(request, 'following.html', data)
@method_decorator(login_required, name='dispatch')
class EditUser(View):
''' edit user view '''
def get(self, request):
''' profile page for a user '''
user = request.user
form = forms.EditUserForm(instance=request.user)
data = {
'title': 'Edit profile',
'form': form,
'user': user,
}
return TemplateResponse(request, 'edit_user.html', data)
def post(self, request):
''' les get fancy with images '''
form = forms.EditUserForm(
request.POST, request.FILES, instance=request.user)
if not form.is_valid():
data = {'form': form, 'user': request.user}
return TemplateResponse(request, 'edit_user.html', data)
user = form.save(commit=False)
if 'avatar' in form.files:
# crop and resize avatar upload
image = Image.open(form.files['avatar'])
target_size = 120
width, height = image.size
thumbnail_scale = height / (width / target_size) if height > width \
else width / (height / target_size)
image.thumbnail([thumbnail_scale, thumbnail_scale])
width, height = image.size
width_diff = width - target_size
height_diff = height - target_size
cropped = image.crop((
int(width_diff / 2),
int(height_diff / 2),
int(width - (width_diff / 2)),
int(height - (height_diff / 2))
))
output = BytesIO()
cropped.save(output, format=image.format)
ContentFile(output.getvalue())
# set the name to a hash
extension = form.files['avatar'].name.split('.')[-1]
filename = '%s.%s' % (uuid4(), extension)
user.avatar.save(filename, ContentFile(output.getvalue()))
user.save()
broadcast(user, user.to_update_activity(user))
return redirect('/user/%s' % request.user.localname)

View file

@ -157,121 +157,6 @@ def search(request):
return TemplateResponse(request, 'search_results.html', data)
@csrf_exempt
@require_GET
def user_page(request, username):
''' profile page for a user '''
try:
user = get_user_from_username(username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
if is_api_request(request):
# we have a json request
return ActivitypubResponse(user.to_activity())
# otherwise we're at a UI view
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
shelf_preview = []
# only show other shelves that should be visible
shelves = user.shelf_set
is_self = request.user.id == user.id
if not is_self:
follower = user.followers.filter(id=request.user.id).exists()
if follower:
shelves = shelves.filter(privacy__in=['public', 'followers'])
else:
shelves = shelves.filter(privacy='public')
for user_shelf in shelves.all():
if not user_shelf.books.count():
continue
shelf_preview.append({
'name': user_shelf.name,
'local_path': user_shelf.local_path,
'books': user_shelf.books.all()[:3],
'size': user_shelf.books.count(),
})
if len(shelf_preview) > 2:
break
# user's posts
activities = get_activity_feed(
request.user,
['public', 'unlisted', 'followers'],
queryset=models.Status.objects.filter(user=user)
)
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)
prev_page = next_page = None
if activity_page.has_next():
next_page = '/user/%s/?page=%d' % \
(username, activity_page.next_page_number())
if activity_page.has_previous():
prev_page = '/user/%s/?page=%d' % \
(username, activity_page.previous_page_number())
data = {
'title': user.name,
'user': user,
'is_self': is_self,
'shelves': shelf_preview,
'shelf_count': shelves.count(),
'activities': activity_page.object_list,
'next': next_page,
'prev': prev_page,
}
return TemplateResponse(request, 'user.html', data)
@csrf_exempt
@require_GET
def followers_page(request, username):
''' list of followers '''
try:
user = get_user_from_username(username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(user.to_followers_activity(**request.GET))
data = {
'title': '%s: followers' % user.name,
'user': user,
'is_self': request.user.id == user.id,
'followers': user.followers.all(),
}
return TemplateResponse(request, 'followers.html', data)
@csrf_exempt
@require_GET
def following_page(request, username):
''' list of followers '''
try:
user = get_user_from_username(username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(user.to_following_activity(**request.GET))
data = {
'title': '%s: following' % user.name,
'user': user,
'is_self': request.user.id == user.id,
'following': user.following.all(),
}
return TemplateResponse(request, 'following.html', data)
@csrf_exempt
@require_GET
def status_page(request, username, status_id):
@ -327,22 +212,6 @@ def replies_page(request, username, status_id):
return ActivitypubResponse(status.to_replies(**request.GET))
@login_required
@require_GET
def edit_profile_page(request):
''' profile page for a user '''
user = request.user
form = forms.EditUserForm(instance=request.user)
data = {
'title': 'Edit profile',
'form': form,
'user': user,
}
return TemplateResponse(request, 'edit_user.html', data)
@require_GET
def book_page(request, book_id):
''' info about a book '''