diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html
index edd43f97c..ec575b388 100644
--- a/bookwyrm/templates/layout.html
+++ b/bookwyrm/templates/layout.html
@@ -82,7 +82,7 @@
-
+
Settings
diff --git a/bookwyrm/templates/user.html b/bookwyrm/templates/user.html
index 8ad5ee087..2d3fac1e3 100644
--- a/bookwyrm/templates/user.html
+++ b/bookwyrm/templates/user.html
@@ -7,7 +7,7 @@
{% if is_self %}
-
+
Edit profile
diff --git a/bookwyrm/tests/test_views.py b/bookwyrm/tests/test_views.py
index 49429c3de..8229edf00 100644
--- a/bookwyrm/tests/test_views.py
+++ b/bookwyrm/tests/test_views.py
@@ -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(
diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py
new file mode 100644
index 000000000..0e2ad9044
--- /dev/null
+++ b/bookwyrm/tests/views/test_user.py
@@ -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')
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py
index b7f21f3ec..a82e6509c 100644
--- a/bookwyrm/urls.py
+++ b/bookwyrm/urls.py
@@ -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\d+)/?$', actions.edit_book),
diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py
index a6cfb7946..5c14a54a6 100644
--- a/bookwyrm/view_actions.py
+++ b/bookwyrm/view_actions.py
@@ -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 '''
diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py
index 048bc80d7..30cc6ea46 100644
--- a/bookwyrm/views/__init__.py
+++ b/bookwyrm/views/__init__.py
@@ -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
diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py
index d702f5809..e20a923fe 100644
--- a/bookwyrm/views/helpers.py
+++ b/bookwyrm/views/helpers.py
@@ -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):
diff --git a/bookwyrm/views/password.py b/bookwyrm/views/password.py
index 85c44d112..915659e3e 100644
--- a/bookwyrm/views/password.py
+++ b/bookwyrm/views/password.py
@@ -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()
diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py
new file mode 100644
index 000000000..35d06fd0f
--- /dev/null
+++ b/bookwyrm/views/user.py
@@ -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)
diff --git a/bookwyrm/vviews.py b/bookwyrm/vviews.py
index 26b0e5f96..ceda2c234 100644
--- a/bookwyrm/vviews.py
+++ b/bookwyrm/vviews.py
@@ -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 '''