From 76d2e20742b0960fd664e5c635424986937fcf81 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 22 Feb 2020 14:02:03 -0800 Subject: [PATCH] cleans up urls and views --- fedireads/incoming.py | 12 +- fedireads/models/__init__.py | 2 +- fedireads/models/{activity.py => status.py} | 0 fedireads/outgoing.py | 6 +- fedireads/templates/user.html | 2 +- fedireads/urls.py | 57 +++---- fedireads/view_actions.py | 159 +++++++++++++++++ fedireads/views.py | 178 ++------------------ 8 files changed, 216 insertions(+), 200 deletions(-) rename fedireads/models/{activity.py => status.py} (100%) create mode 100644 fedireads/view_actions.py diff --git a/fedireads/incoming.py b/fedireads/incoming.py index 647d8ed73..3fff64ecb 100644 --- a/fedireads/incoming.py +++ b/fedireads/incoming.py @@ -108,8 +108,11 @@ def inbox(request, username): ''' incoming activitypub events ''' # TODO: should do some kind of checking if the user accepts # this action from the sender probably? idk - # but this will just throw an error if the user doesn't exist I guess - models.User.objects.get(localname=username) + # but this will just throw a 404 if the user doesn't exist + try: + models.User.objects.get(localname=username) + except models.User.DoesNotExist: + return HttpResponseNotFound() return shared_inbox(request) @@ -120,7 +123,10 @@ def get_actor(request, username): if request.method != 'GET': return HttpResponseBadRequest() - user = models.User.objects.get(localname=username) + try: + user = models.User.objects.get(localname=username) + except models.User.DoesNotExist: + return HttpResponseNotFound() return JsonResponse(activitypub.get_actor(user)) diff --git a/fedireads/models/__init__.py b/fedireads/models/__init__.py index c71fa3a36..02154eeba 100644 --- a/fedireads/models/__init__.py +++ b/fedireads/models/__init__.py @@ -1,5 +1,5 @@ ''' bring all the models into the app namespace ''' from .book import Shelf, ShelfBook, Book, Author from .user import User, UserRelationship, FederatedServer -from .activity import Status, Review, Favorite, Tag +from .status import Status, Review, Favorite, Tag diff --git a/fedireads/models/activity.py b/fedireads/models/status.py similarity index 100% rename from fedireads/models/activity.py rename to fedireads/models/status.py diff --git a/fedireads/outgoing.py b/fedireads/outgoing.py index d032ac058..e1a9c5103 100644 --- a/fedireads/outgoing.py +++ b/fedireads/outgoing.py @@ -15,10 +15,14 @@ from fedireads.broadcast import get_recipients, broadcast @csrf_exempt def outbox(request, username): ''' outbox for the requested user ''' - user = models.User.objects.get(localname=username) if request.method != 'GET': return HttpResponseNotFound() + try: + user = models.User.objects.get(localname=username) + except models.User.DoesNotExist: + return HttpResponseNotFound() + # paginated list of messages if request.GET.get('page'): limit = 20 diff --git a/fedireads/templates/user.html b/fedireads/templates/user.html index 16d4604ad..802b99209 100644 --- a/fedireads/templates/user.html +++ b/fedireads/templates/user.html @@ -16,7 +16,7 @@ {% if is_self %}
- Edit profile + Edit profile
{% endif %} diff --git a/fedireads/urls.py b/fedireads/urls.py index b064137ed..e883eff64 100644 --- a/fedireads/urls.py +++ b/fedireads/urls.py @@ -4,30 +4,26 @@ from django.contrib import admin from django.urls import path, re_path from fedireads import incoming, outgoing, views, settings, wellknown +from fedireads import view_actions as actions +username_regex = r'[\w@\.-]+' +localname_regex = r'(?P[\w\.-]+)' +user_path = r'^user/%s' % username_regex +local_user_path = r'^user/%s' % localname_regex +status_path = r'%s/(status|review)/(?P\d+)' % local_user_path urlpatterns = [ path('admin/', admin.site.urls), # federation endpoints re_path(r'^inbox/?$', incoming.shared_inbox), - re_path(r'^user/(?P\w+).json/?$', incoming.get_actor), - re_path(r'^user/(?P\w+)/inbox/?$', incoming.inbox), - re_path(r'^user/(?P\w+)/outbox/?$', outgoing.outbox), - re_path(r'^user/(?P\w+)/followers/?$', incoming.get_followers), - re_path(r'^user/(?P\w+)/following/?$', incoming.get_following), - re_path( - r'^user/(?P\w+)/(status|review)/(?P\d+)/?$', - incoming.get_status - ), - re_path( - r'^user/(?P\w+)/(status|review)/(?P\d+)/activity/?$', - incoming.get_status - ), - re_path( - r'^user/(?P\w+)/(status|review)/(?P\d+)/replies/?$', - incoming.get_replies - ), + re_path(r'%s.json/?$' % local_user_path, incoming.get_actor), + re_path(r'%s/inbox/?$' % local_user_path, incoming.inbox), + re_path(r'%s/outbox/?$' % local_user_path, outgoing.outbox), + re_path(r'%s/followers/?$' % local_user_path, incoming.get_followers), + re_path(r'%s/following/?$' % local_user_path, incoming.get_following), + re_path(r'%s(/activity/?)?$' % status_path, incoming.get_status), + re_path(r'%s/replies/?$' % status_path, incoming.get_replies), # .well-known endpoints re_path(r'^.well-known/webfinger/?$', wellknown.webfinger), @@ -43,23 +39,24 @@ urlpatterns = [ re_path(r'^login/?$', views.user_login), re_path(r'^logout/?$', views.user_logout), # this endpoint is both ui and fed depending on Accept type - re_path(r'^user/(?P[\w@\.-]+)/?$', views.user_profile), - re_path(r'^user/(?P\w+)/edit/?$', views.user_profile_edit), + re_path(r'%s/?$' % user_path, views.user_page), + re_path(r'%s/edit/?$' % user_path, views.edit_profile_page), + re_path(r'^user/edit/?$', views.edit_profile_page), re_path(r'^book/(?P\w+)/?$', views.book_page), re_path(r'^author/(?P\w+)/?$', views.author_page), re_path(r'^tag/(?P[\w-]+)/?$', views.tag_page), - re_path(r'^shelf/(?P[\w@\.-]+)/(?P[\w-]+)/?$', views.shelf_page), + re_path(r'^shelf/%s/(?P[\w-]+)/?$' % username_regex, views.shelf_page), # internal action endpoints - re_path(r'^review/?$', views.review), - re_path(r'^tag/?$', views.tag), - re_path(r'^untag/?$', views.untag), - re_path(r'^comment/?$', views.comment), - re_path(r'^favorite/(?P\d+)/?$', views.favorite), - re_path(r'^shelve/?$', views.shelve), - re_path(r'^follow/?$', views.follow), - re_path(r'^unfollow/?$', views.unfollow), - re_path(r'^search/?$', views.search), - re_path(r'^edit_profile/?$', views.edit_profile), + re_path(r'^review/?$', actions.review), + re_path(r'^tag/?$', actions.tag), + re_path(r'^untag/?$', actions.untag), + re_path(r'^comment/?$', actions.comment), + re_path(r'^favorite/(?P\d+)/?$', actions.favorite), + re_path(r'^shelve/?$', actions.shelve), + re_path(r'^follow/?$', actions.follow), + re_path(r'^unfollow/?$', actions.unfollow), + re_path(r'^search/?$', actions.search), + re_path(r'^edit_profile/?$', actions.edit_profile), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/fedireads/view_actions.py b/fedireads/view_actions.py new file mode 100644 index 000000000..e016c3a13 --- /dev/null +++ b/fedireads/view_actions.py @@ -0,0 +1,159 @@ +''' views for actions you can take in the application ''' +from django.contrib.auth.decorators import login_required +from django.http import HttpResponseBadRequest +from django.shortcuts import redirect +from django.template.response import TemplateResponse +import re + +from fedireads import forms, models, openlibrary, outgoing +from fedireads.views import get_user_from_username + + +@login_required +def edit_profile(request): + ''' les get fancy with images ''' + if not request.method == 'POST': + return redirect('/user/%s' % request.user.localname) + + form = forms.EditUserForm(request.POST, request.FILES) + if not form.is_valid(): + return redirect('/') + + request.user.name = form.data['name'] + if 'avatar' in form.files: + request.user.avatar = form.files['avatar'] + request.user.summary = form.data['summary'] + request.user.save() + return redirect('/user/%s' % request.user.localname) + + +@login_required +def shelve(request): + ''' put a book on a user's shelf ''' + book = models.Book.objects.get(id=request.POST['book']) + desired_shelf = models.Shelf.objects.filter( + identifier=request.POST['shelf'], + user=request.user + ).first() + + if request.POST.get('reshelve', True): + try: + current_shelf = models.Shelf.objects.get( + user=request.user, + book=book + ) + outgoing.handle_unshelve(request.user, book, current_shelf) + except models.Shelf.DoesNotExist: + # this just means it isn't currently on the user's shelves + pass + outgoing.handle_shelve(request.user, book, desired_shelf) + return redirect('/') + + +@login_required +def review(request): + ''' create a book review note ''' + form = forms.ReviewForm(request.POST) + book_identifier = request.POST.get('book') + # TODO: better failure behavior + if not form.is_valid(): + return redirect('/book/%s' % book_identifier) + + # TODO: validation, htmlification + name = form.data.get('name') + content = form.data.get('content') + rating = int(form.data.get('rating')) + + outgoing.handle_review(request.user, book_identifier, name, content, rating) + return redirect('/book/%s' % book_identifier) + + +@login_required +def tag(request): + ''' tag a book ''' + # I'm not using a form here because sometimes "name" is sent as a hidden + # field which doesn't validate + name = request.POST.get('name') + book_identifier = request.POST.get('book') + + outgoing.handle_tag(request.user, book_identifier, name) + return redirect('/book/%s' % book_identifier) + + +@login_required +def untag(request): + ''' untag a book ''' + name = request.POST.get('name') + book_identifier = request.POST.get('book') + + outgoing.handle_untag(request.user, book_identifier, name) + return redirect('/book/%s' % book_identifier) + + +@login_required +def comment(request): + ''' respond to a book review ''' + form = forms.CommentForm(request.POST) + # this is a bit of a formality, the form is just one text field + if not form.is_valid(): + return redirect('/') + parent_id = request.POST['parent'] + parent = models.Status.objects.get(id=parent_id) + outgoing.handle_comment(request.user, parent, form.data['content']) + return redirect('/') + + +@login_required +def favorite(request, status_id): + ''' like a status ''' + status = models.Status.objects.get(id=status_id) + outgoing.handle_outgoing_favorite(request.user, status) + return redirect(request.headers.get('Referer', '/')) + + +@login_required +def follow(request): + ''' follow another user, here or abroad ''' + username = request.POST['user'] + try: + to_follow = get_user_from_username(username) + except models.User.DoesNotExist: + return HttpResponseBadRequest() + + outgoing.handle_outgoing_follow(request.user, to_follow) + user_slug = to_follow.localname if to_follow.localname \ + else to_follow.username + return redirect('/user/%s' % user_slug) + + +@login_required +def unfollow(request): + ''' unfollow a user ''' + username = request.POST['user'] + try: + to_unfollow = get_user_from_username(username) + except models.User.DoesNotExist: + return HttpResponseBadRequest() + + outgoing.handle_outgoing_unfollow(request.user, to_unfollow) + user_slug = to_unfollow.localname if to_unfollow.localname \ + else to_unfollow.username + return redirect('/user/%s' % user_slug) + + +@login_required +def search(request): + ''' that search bar up top ''' + query = request.GET.get('q') + if re.match(r'\w+@\w+.\w+', query): + # if something looks like a username, search with webfinger + results = [outgoing.handle_account_search(query)] + template = 'user_results.html' + else: + # just send the question over to openlibrary for book search + results = openlibrary.book_search(query) + template = 'book_results.html' + + return TemplateResponse(request, template, {'results': results}) + + diff --git a/fedireads/views.py b/fedireads/views.py index 613245431..c2d675c24 100644 --- a/fedireads/views.py +++ b/fedireads/views.py @@ -1,16 +1,24 @@ -''' application views/pages ''' +''' views for pages you can go to in the application ''' from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required from django.db.models import Avg, Q -from django.http import HttpResponseBadRequest, HttpResponseNotFound +from django.http import HttpResponseNotFound from django.shortcuts import redirect from django.template.response import TemplateResponse -import re -from fedireads import forms, models, openlibrary, outgoing, incoming +from fedireads import forms, models, openlibrary, incoming from fedireads.settings import DOMAIN +def get_user_from_username(username): + ''' helper function to resolve a localname or a username to a user ''' + try: + user = models.User.objects.get(localname=username) + except models.User.DoesNotExist: + user = models.User.objects.get(username=username) + return user + + @login_required def home(request): ''' this is the same as the feed on the home tab ''' @@ -133,7 +141,7 @@ def register(request): return redirect('/') -def user_profile(request, username): +def user_page(request, username): ''' profile page for a user ''' content = request.headers.get('Accept') # TODO: this should probably be the full content type? maybe? @@ -160,17 +168,9 @@ def user_profile(request, username): } return TemplateResponse(request, 'user.html', data) -def get_user_from_username(username): - ''' resolve a localname or a username to a user ''' - try: - user = models.User.objects.get(localname=username) - except models.User.DoesNotExist: - user = models.User.objects.get(username=username) - return user - @login_required -def user_profile_edit(request, username): +def edit_profile_page(request, username): ''' profile page for a user ''' try: user = models.User.objects.get(localname=username) @@ -185,26 +185,7 @@ def user_profile_edit(request, username): return TemplateResponse(request, 'edit_user.html', data) -# TODO: there oughta be clear naming between endpoints and pages -@login_required -def edit_profile(request): - ''' les get fancy with images ''' - if not request.method == 'POST': - return redirect('/user/%s' % request.user.localname) - form = forms.EditUserForm(request.POST, request.FILES) - if not form.is_valid(): - return redirect('/') - - request.user.name = form.data['name'] - if 'avatar' in form.files: - request.user.avatar = form.files['avatar'] - request.user.summary = form.data['summary'] - request.user.save() - return redirect('/user/%s' % request.user.localname) - - -@login_required def book_page(request, book_identifier): ''' info about a book ''' book = openlibrary.get_or_create_book(book_identifier) @@ -234,7 +215,6 @@ def book_page(request, book_identifier): return TemplateResponse(request, 'book.html', data) -@login_required def author_page(request, author_identifier): ''' landing page for an author ''' try: @@ -276,133 +256,3 @@ def shelf_page(request, username, shelf_identifier): } return TemplateResponse(request, 'shelf.html', data) - -@login_required -def shelve(request): - ''' put a book on a user's shelf ''' - book = models.Book.objects.get(id=request.POST['book']) - desired_shelf = models.Shelf.objects.filter( - identifier=request.POST['shelf'], - user=request.user - ).first() - - if request.POST.get('reshelve', True): - try: - current_shelf = models.Shelf.objects.get( - user=request.user, - book=book - ) - outgoing.handle_unshelve(request.user, book, current_shelf) - except models.Shelf.DoesNotExist: - # this just means it isn't currently on the user's shelves - pass - outgoing.handle_shelve(request.user, book, desired_shelf) - return redirect('/') - - -@login_required -def review(request): - ''' create a book review note ''' - form = forms.ReviewForm(request.POST) - book_identifier = request.POST.get('book') - # TODO: better failure behavior - if not form.is_valid(): - return redirect('/book/%s' % book_identifier) - - # TODO: validation, htmlification - name = form.data.get('name') - content = form.data.get('content') - rating = int(form.data.get('rating')) - - outgoing.handle_review(request.user, book_identifier, name, content, rating) - return redirect('/book/%s' % book_identifier) - - -@login_required -def tag(request): - ''' tag a book ''' - # I'm not using a form here because sometimes "name" is sent as a hidden - # field which doesn't validate - name = request.POST.get('name') - book_identifier = request.POST.get('book') - - outgoing.handle_tag(request.user, book_identifier, name) - return redirect('/book/%s' % book_identifier) - - -@login_required -def untag(request): - ''' untag a book ''' - name = request.POST.get('name') - book_identifier = request.POST.get('book') - - outgoing.handle_untag(request.user, book_identifier, name) - return redirect('/book/%s' % book_identifier) - - -@login_required -def comment(request): - ''' respond to a book review ''' - form = forms.CommentForm(request.POST) - # this is a bit of a formality, the form is just one text field - if not form.is_valid(): - return redirect('/') - parent_id = request.POST['parent'] - parent = models.Status.objects.get(id=parent_id) - outgoing.handle_comment(request.user, parent, form.data['content']) - return redirect('/') - - -@login_required -def favorite(request, status_id): - ''' like a status ''' - status = models.Status.objects.get(id=status_id) - outgoing.handle_outgoing_favorite(request.user, status) - return redirect(request.headers.get('Referer', '/')) - - -@login_required -def follow(request): - ''' follow another user, here or abroad ''' - username = request.POST['user'] - try: - to_follow = get_user_from_username(username) - except models.User.DoesNotExist: - return HttpResponseBadRequest() - - outgoing.handle_outgoing_follow(request.user, to_follow) - user_slug = to_follow.localname if to_follow.localname \ - else to_follow.username - return redirect('/user/%s' % user_slug) - - -@login_required -def unfollow(request): - ''' unfollow a user ''' - username = request.POST['user'] - try: - to_unfollow = get_user_from_username(username) - except models.User.DoesNotExist: - return HttpResponseBadRequest() - - outgoing.handle_outgoing_unfollow(request.user, to_unfollow) - user_slug = to_unfollow.localname if to_unfollow.localname \ - else to_unfollow.username - return redirect('/user/%s' % user_slug) - - -@login_required -def search(request): - ''' that search bar up top ''' - query = request.GET.get('q') - if re.match(r'\w+@\w+.\w+', query): - # if something looks like a username, search with webfinger - results = [outgoing.handle_account_search(query)] - template = 'user_results.html' - else: - # just send the question over to openlibrary for book search - results = openlibrary.book_search(query) - template = 'book_results.html' - - return TemplateResponse(request, template, {'results': results}) -