Merge pull request #1463 from bookwyrm-social/list-item-perms

Updates how permissions are verified in views
This commit is contained in:
Mouse Reeve 2021-09-27 19:36:35 -07:00 committed by GitHub
commit 46a7030dbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 264 additions and 236 deletions

View file

@ -228,7 +228,7 @@ class ExpiryWidget(widgets.Select):
elif selected_string == "forever": elif selected_string == "forever":
return None return None
else: else:
return selected_string # "This will raise return selected_string # This will raise
return timezone.now() + interval return timezone.now() + interval

View file

@ -1,8 +1,11 @@
""" base model with default fields """ """ base model with default fields """
import base64 import base64
from Crypto import Random from Crypto import Random
from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.http import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
@ -48,26 +51,26 @@ class BookWyrmModel(models.Model):
"""how to link to this object in the local app""" """how to link to this object in the local app"""
return self.get_remote_id().replace(f"https://{DOMAIN}", "") return self.get_remote_id().replace(f"https://{DOMAIN}", "")
def visible_to_user(self, viewer): def raise_visible_to_user(self, viewer):
"""is a user authorized to view an object?""" """is a user authorized to view an object?"""
# make sure this is an object with privacy owned by a user # make sure this is an object with privacy owned by a user
if not hasattr(self, "user") or not hasattr(self, "privacy"): if not hasattr(self, "user") or not hasattr(self, "privacy"):
return None return
# viewer can't see it if the object's owner blocked them # viewer can't see it if the object's owner blocked them
if viewer in self.user.blocks.all(): if viewer in self.user.blocks.all():
return False raise Http404()
# you can see your own posts and any public or unlisted posts # you can see your own posts and any public or unlisted posts
if viewer == self.user or self.privacy in ["public", "unlisted"]: if viewer == self.user or self.privacy in ["public", "unlisted"]:
return True return
# you can see the followers only posts of people you follow # you can see the followers only posts of people you follow
if ( if (
self.privacy == "followers" self.privacy == "followers"
and self.user.followers.filter(id=viewer.id).first() and self.user.followers.filter(id=viewer.id).first()
): ):
return True return
# you can see dms you are tagged in # you can see dms you are tagged in
if hasattr(self, "mention_users"): if hasattr(self, "mention_users"):
@ -75,8 +78,32 @@ class BookWyrmModel(models.Model):
self.privacy == "direct" self.privacy == "direct"
and self.mention_users.filter(id=viewer.id).first() and self.mention_users.filter(id=viewer.id).first()
): ):
return True return
return False raise Http404()
def raise_not_editable(self, viewer):
"""does this user have permission to edit this object? liable to be overwritten
by models that inherit this base model class"""
if not hasattr(self, "user"):
return
# generally moderators shouldn't be able to edit other people's stuff
if self.user == viewer:
return
raise PermissionDenied()
def raise_not_deletable(self, viewer):
"""does this user have permission to delete this object? liable to be
overwritten by models that inherit this base model class"""
if not hasattr(self, "user"):
return
# but generally moderators can delete other people's stuff
if self.user == viewer or viewer.has_perm("moderate_post"):
return
raise PermissionDenied()
@receiver(models.signals.post_save) @receiver(models.signals.post_save)

View file

@ -92,6 +92,12 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
notification_type="ADD", notification_type="ADD",
) )
def raise_not_deletable(self, viewer):
"""the associated user OR the list owner can delete"""
if self.book_list.user == viewer:
return
super().raise_not_deletable(viewer)
class Meta: class Meta:
"""A book may only be placed into a list once, """A book may only be placed into a list once,
and each order in the list may be used only once""" and each order in the list may be used only once"""

View file

@ -1,5 +1,6 @@
""" puttin' books on shelves """ """ puttin' books on shelves """
import re import re
from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
@ -57,6 +58,12 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
identifier = self.identifier or self.get_identifier() identifier = self.identifier or self.get_identifier()
return f"{base_path}/books/{identifier}" return f"{base_path}/books/{identifier}"
def raise_not_deletable(self, viewer):
"""don't let anyone delete a default shelf"""
super().raise_not_deletable(viewer)
if not self.editable:
raise PermissionDenied()
class Meta: class Meta:
"""user/shelf unqiueness""" """user/shelf unqiueness"""

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re import re
from django.apps import apps from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
@ -187,6 +188,13 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
"""json serialized activitypub class""" """json serialized activitypub class"""
return self.to_activity_dataclass(pure=pure).serialize() return self.to_activity_dataclass(pure=pure).serialize()
def raise_not_editable(self, viewer):
"""certain types of status aren't editable"""
# first, the standard raise
super().raise_not_editable(viewer)
if isinstance(self, (GeneratedNote, ReviewRating)):
raise PermissionDenied()
class GeneratedNote(Status): class GeneratedNote(Status):
"""these are app-generated messages about user activity""" """these are app-generated messages about user activity"""

View file

@ -66,14 +66,14 @@
<p>{% blocktrans with username=item.user.display_name user_path=item.user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p> <p>{% blocktrans with username=item.user.display_name user_path=item.user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
</div> </div>
</div> </div>
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %} {% if list.user == request.user %}
<div class="card-footer-item"> <div class="card-footer-item">
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}"> <form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}">
{% csrf_token %}
<div class="field has-addons mb-0"> <div class="field has-addons mb-0">
<div class="control"> <div class="control">
<label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label> <label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label>
</div> </div>
{% csrf_token %}
<div class="control"> <div class="control">
<input id="input_list_position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}"> <input id="input_list_position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
</div> </div>
@ -83,7 +83,9 @@
</div> </div>
</form> </form>
</div> </div>
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item"> {% endif %}
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
<form name="remove-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="item" value="{{ item.id }}"> <input type="hidden" name="item" value="{{ item.id }}">
<button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button> <button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button>

View file

@ -1,5 +1,6 @@
""" testing models """ """ testing models """
from unittest.mock import patch from unittest.mock import patch
from django.http import Http404
from django.test import TestCase from django.test import TestCase
from bookwyrm import models from bookwyrm import models
@ -39,14 +40,14 @@ class BaseModel(TestCase):
"""these should be generated""" """these should be generated"""
self.test_model.id = 1 self.test_model.id = 1
expected = self.test_model.get_remote_id() expected = self.test_model.get_remote_id()
self.assertEqual(expected, "https://%s/bookwyrmtestmodel/1" % DOMAIN) self.assertEqual(expected, f"https://{DOMAIN}/bookwyrmtestmodel/1")
def test_remote_id_with_user(self): def test_remote_id_with_user(self):
"""format of remote id when there's a user object""" """format of remote id when there's a user object"""
self.test_model.user = self.local_user self.test_model.user = self.local_user
self.test_model.id = 1 self.test_model.id = 1
expected = self.test_model.get_remote_id() expected = self.test_model.get_remote_id()
self.assertEqual(expected, "https://%s/user/mouse/bookwyrmtestmodel/1" % DOMAIN) self.assertEqual(expected, f"https://{DOMAIN}/user/mouse/bookwyrmtestmodel/1")
def test_set_remote_id(self): def test_set_remote_id(self):
"""this function sets remote ids after creation""" """this function sets remote ids after creation"""
@ -55,9 +56,7 @@ class BaseModel(TestCase):
instance = models.Work.objects.create(title="work title") instance = models.Work.objects.create(title="work title")
instance.remote_id = None instance.remote_id = None
base_model.set_remote_id(None, instance, True) base_model.set_remote_id(None, instance, True)
self.assertEqual( self.assertEqual(instance.remote_id, f"https://{DOMAIN}/book/{instance.id}")
instance.remote_id, "https://%s/book/%d" % (DOMAIN, instance.id)
)
# shouldn't set remote_id if it's not created # shouldn't set remote_id if it's not created
instance.remote_id = None instance.remote_id = None
@ -70,28 +69,30 @@ class BaseModel(TestCase):
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="public" content="hi", user=self.remote_user, privacy="public"
) )
self.assertTrue(obj.visible_to_user(self.local_user)) self.assertIsNone(obj.raise_visible_to_user(self.local_user))
obj = models.Shelf.objects.create( obj = models.Shelf.objects.create(
name="test", user=self.remote_user, privacy="unlisted" name="test", user=self.remote_user, privacy="unlisted"
) )
self.assertTrue(obj.visible_to_user(self.local_user)) self.assertIsNone(obj.raise_visible_to_user(self.local_user))
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="followers" content="hi", user=self.remote_user, privacy="followers"
) )
self.assertFalse(obj.visible_to_user(self.local_user)) with self.assertRaises(Http404):
obj.raise_visible_to_user(self.local_user)
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="direct" content="hi", user=self.remote_user, privacy="direct"
) )
self.assertFalse(obj.visible_to_user(self.local_user)) with self.assertRaises(Http404):
obj.raise_visible_to_user(self.local_user)
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="direct" content="hi", user=self.remote_user, privacy="direct"
) )
obj.mention_users.add(self.local_user) obj.mention_users.add(self.local_user)
self.assertTrue(obj.visible_to_user(self.local_user)) self.assertIsNone(obj.raise_visible_to_user(self.local_user))
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_object_visible_to_user_follower(self, _): def test_object_visible_to_user_follower(self, _):
@ -100,18 +101,19 @@ class BaseModel(TestCase):
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="followers" content="hi", user=self.remote_user, privacy="followers"
) )
self.assertTrue(obj.visible_to_user(self.local_user)) self.assertIsNone(obj.raise_visible_to_user(self.local_user))
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="direct" content="hi", user=self.remote_user, privacy="direct"
) )
self.assertFalse(obj.visible_to_user(self.local_user)) with self.assertRaises(Http404):
obj.raise_visible_to_user(self.local_user)
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="direct" content="hi", user=self.remote_user, privacy="direct"
) )
obj.mention_users.add(self.local_user) obj.mention_users.add(self.local_user)
self.assertTrue(obj.visible_to_user(self.local_user)) self.assertIsNone(obj.raise_visible_to_user(self.local_user))
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_object_visible_to_user_blocked(self, _): def test_object_visible_to_user_blocked(self, _):
@ -120,9 +122,11 @@ class BaseModel(TestCase):
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="public" content="hi", user=self.remote_user, privacy="public"
) )
self.assertFalse(obj.visible_to_user(self.local_user)) with self.assertRaises(Http404):
obj.raise_visible_to_user(self.local_user)
obj = models.Shelf.objects.create( obj = models.Shelf.objects.create(
name="test", user=self.remote_user, privacy="unlisted" name="test", user=self.remote_user, privacy="unlisted"
) )
self.assertFalse(obj.visible_to_user(self.local_user)) with self.assertRaises(Http404):
obj.raise_visible_to_user(self.local_user)

View file

@ -3,6 +3,7 @@ import json
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseNotAllowed, HttpResponseNotFound from django.http import HttpResponseNotAllowed, HttpResponseNotFound
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -130,22 +131,24 @@ class Inbox(TestCase):
"", "",
HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)", HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)",
) )
self.assertFalse(views.inbox.is_blocked_user_agent(request)) self.assertIsNone(views.inbox.raise_is_blocked_user_agent(request))
models.FederatedServer.objects.create( models.FederatedServer.objects.create(
server_name="mastodon.social", status="blocked" server_name="mastodon.social", status="blocked"
) )
self.assertTrue(views.inbox.is_blocked_user_agent(request)) with self.assertRaises(PermissionDenied):
views.inbox.raise_is_blocked_user_agent(request)
def test_is_blocked_activity(self): def test_is_blocked_activity(self):
"""check for blocked servers""" """check for blocked servers"""
activity = {"actor": "https://mastodon.social/user/whaatever/else"} activity = {"actor": "https://mastodon.social/user/whaatever/else"}
self.assertFalse(views.inbox.is_blocked_activity(activity)) self.assertIsNone(views.inbox.raise_is_blocked_activity(activity))
models.FederatedServer.objects.create( models.FederatedServer.objects.create(
server_name="mastodon.social", status="blocked" server_name="mastodon.social", status="blocked"
) )
self.assertTrue(views.inbox.is_blocked_activity(activity)) with self.assertRaises(PermissionDenied):
views.inbox.raise_is_blocked_activity(activity)
@patch("bookwyrm.suggested_users.remove_user_task.delay") @patch("bookwyrm.suggested_users.remove_user_task.delay")
def test_create_by_deactivated_user(self, _): def test_create_by_deactivated_user(self, _):
@ -157,11 +160,11 @@ class Inbox(TestCase):
activity = self.create_json activity = self.create_json
activity["actor"] = self.remote_user.remote_id activity["actor"] = self.remote_user.remote_id
activity["object"] = status_data activity["object"] = status_data
activity["type"] = "Create"
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: response = self.client.post(
mock_valid.return_value = True "/inbox",
json.dumps(activity),
result = self.client.post( content_type="application/json",
"/inbox", json.dumps(activity), content_type="application/json"
) )
self.assertEqual(result.status_code, 403) self.assertEqual(response.status_code, 403)

View file

@ -5,6 +5,7 @@ import pathlib
from PIL import Image from PIL import Image
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.http import Http404
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -81,9 +82,8 @@ class FeedViews(TestCase):
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.views.feed.is_api_request") as is_api: with patch("bookwyrm.views.feed.is_api_request") as is_api:
is_api.return_value = False is_api.return_value = False
result = view(request, "mouse", 12345) with self.assertRaises(Http404):
view(request, "mouse", 12345)
self.assertEqual(result.status_code, 404)
def test_status_page_not_found_wrong_user(self, *_): def test_status_page_not_found_wrong_user(self, *_):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
@ -102,9 +102,8 @@ class FeedViews(TestCase):
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.views.feed.is_api_request") as is_api: with patch("bookwyrm.views.feed.is_api_request") as is_api:
is_api.return_value = False is_api.return_value = False
result = view(request, "mouse", status.id) with self.assertRaises(Http404):
view(request, "mouse", status.id)
self.assertEqual(result.status_code, 404)
def test_status_page_with_image(self, *_): def test_status_page_with_image(self, *_):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""

View file

@ -3,6 +3,7 @@ from unittest.mock import patch
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.http import Http404
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -103,8 +104,8 @@ class GoalViews(TestCase):
request = self.factory.get("") request = self.factory.get("")
request.user = self.rat request.user = self.rat
result = view(request, self.local_user.localname, self.year) with self.assertRaises(Http404):
self.assertEqual(result.status_code, 404) view(request, self.local_user.localname, self.year)
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_create_goal(self, _): def test_create_goal(self, _):

View file

@ -568,5 +568,6 @@ class ListActionViews(TestCase):
) )
request.user = self.rat request.user = self.rat
with self.assertRaises(PermissionDenied):
views.list.remove_book(request, self.list.id) views.list.remove_book(request, self.list.id)
self.assertTrue(self.list.listitem_set.exists()) self.assertTrue(self.list.listitem_set.exists())

View file

@ -105,7 +105,7 @@ class ShelfViews(TestCase):
shelf.refresh_from_db() shelf.refresh_from_db()
self.assertEqual(shelf.name, "cool name") self.assertEqual(shelf.name, "cool name")
self.assertEqual(shelf.identifier, "testshelf-%d" % shelf.id) self.assertEqual(shelf.identifier, f"testshelf-{shelf.id}")
def test_edit_shelf_name_not_editable(self, *_): def test_edit_shelf_name_not_editable(self, *_):
"""can't change the name of an non-editable shelf""" """can't change the name of an non-editable shelf"""

View file

@ -1,6 +1,7 @@
""" test for app action functionality """ """ test for app action functionality """
import json import json
from unittest.mock import patch from unittest.mock import patch
from django.core.exceptions import PermissionDenied
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -196,9 +197,9 @@ class StatusViews(TestCase):
) )
with patch("bookwyrm.activitystreams.remove_status_task.delay") as mock: with patch("bookwyrm.activitystreams.remove_status_task.delay") as mock:
result = view(request, status.id) with self.assertRaises(PermissionDenied):
view(request, status.id)
self.assertFalse(mock.called) self.assertFalse(mock.called)
self.assertEqual(result.status_code, 400)
status.refresh_from_db() status.refresh_from_db()
self.assertFalse(status.deleted) self.assertFalse(status.deleted)
@ -214,9 +215,9 @@ class StatusViews(TestCase):
) )
with patch("bookwyrm.activitystreams.remove_status_task.delay") as mock: with patch("bookwyrm.activitystreams.remove_status_task.delay") as mock:
result = view(request, status.id) with self.assertRaises(PermissionDenied):
view(request, status.id)
self.assertFalse(mock.called) self.assertFalse(mock.called)
self.assertEqual(result.status_code, 400)
status.refresh_from_db() status.refresh_from_db()
self.assertFalse(status.deleted) self.assertFalse(status.deleted)
@ -375,6 +376,7 @@ http://www.fish.com/"""
request = self.factory.post("") request = self.factory.post("")
request.user = self.remote_user request.user = self.remote_user
with self.assertRaises(PermissionDenied):
view(request, status.id) view(request, status.id)
status.refresh_from_db() status.refresh_from_db()

View file

@ -276,7 +276,7 @@ urlpatterns = [
# User books # User books
re_path(rf"{USER_PATH}/books/?$", views.Shelf.as_view(), name="user-shelves"), re_path(rf"{USER_PATH}/books/?$", views.Shelf.as_view(), name="user-shelves"),
re_path( re_path(
rf"^{USER_PATH}/(helf|books)/(?P<shelf_identifier>[\w-]+)(.json)?/?$", rf"^{USER_PATH}/(shelf|books)/(?P<shelf_identifier>[\w-]+)(.json)?/?$",
views.Shelf.as_view(), views.Shelf.as_view(),
name="shelf", name="shelf",
), ),

View file

@ -105,7 +105,7 @@ def moderator_delete_user(request, user_id):
# we can't delete users on other instances # we can't delete users on other instances
if not user.local: if not user.local:
raise PermissionDenied raise PermissionDenied()
form = forms.DeleteUserForm(request.POST, instance=user) form = forms.DeleteUserForm(request.POST, instance=user)

View file

@ -1,6 +1,5 @@
""" views for actions you can take in the application """ """ views for actions you can take in the application """
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -32,12 +31,10 @@ class Block(View):
def unblock(request, user_id): def unblock(request, user_id):
"""undo a block""" """undo a block"""
to_unblock = get_object_or_404(models.User, id=user_id) to_unblock = get_object_or_404(models.User, id=user_id)
try: block = get_object_or_404(
block = models.UserBlocks.objects.get( models.UserBlocks,
user_subject=request.user, user_subject=request.user,
user_object=to_unblock, user_object=to_unblock,
) )
except models.UserBlocks.DoesNotExist:
return HttpResponseNotFound()
block.delete() block.delete()
return redirect("prefs-block") return redirect("prefs-block")

View file

@ -50,7 +50,7 @@ class Book(View):
) )
if not book or not book.parent_work: if not book or not book.parent_work:
raise Http404 raise Http404()
# all reviews for all editions of the book # all reviews for all editions of the book
reviews = privacy_filter( reviews = privacy_filter(

View file

@ -3,6 +3,7 @@ from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponseNotFound, Http404 from django.http import HttpResponseNotFound, Http404
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -93,17 +94,15 @@ class Status(View):
def get(self, request, username, status_id): def get(self, request, username, status_id):
"""display a particular status (and replies, etc)""" """display a particular status (and replies, etc)"""
try:
user = get_user_from_username(request.user, username) user = get_user_from_username(request.user, username)
status = models.Status.objects.select_subclasses().get( status = get_object_or_404(
user=user, id=status_id, deleted=False models.Status.objects.select_subclasses(),
user=user,
id=status_id,
deleted=False,
) )
except (ValueError, models.Status.DoesNotExist):
return HttpResponseNotFound()
# make sure the user is authorized to see the status # make sure the user is authorized to see the status
if not status.visible_to_user(request.user): status.raise_visible_to_user(request.user)
return HttpResponseNotFound()
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse( return ActivitypubResponse(
@ -133,6 +132,7 @@ class Replies(View):
status = models.Status.objects.get(id=status_id) status = models.Status.objects.get(id=status_id)
if status.user.localname != username: if status.user.localname != username:
return HttpResponseNotFound() return HttpResponseNotFound()
status.raise_visible_to_user(request.user)
return ActivitypubResponse(status.to_replies(**request.GET)) return ActivitypubResponse(status.to_replies(**request.GET))

View file

@ -1,8 +1,7 @@
""" views for actions you can take in the application """ """ views for actions you can take in the application """
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db import IntegrityError from django.db import IntegrityError
from django.http import HttpResponseBadRequest from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import redirect
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from bookwyrm import models from bookwyrm import models
@ -78,12 +77,10 @@ def delete_follow_request(request):
username = request.POST["user"] username = request.POST["user"]
requester = get_user_from_username(request.user, username) requester = get_user_from_username(request.user, username)
try: follow_request = get_object_or_404(
follow_request = models.UserFollowRequest.objects.get( models.UserFollowRequest, user_subject=requester, user_object=request.user
user_subject=requester, user_object=request.user
) )
except models.UserFollowRequest.DoesNotExist: follow_request.raise_not_deletable(request.user)
return HttpResponseBadRequest()
follow_request.delete() follow_request.delete()
return redirect(f"/user/{request.user.localname}") return redirect(f"/user/{request.user.localname}")

View file

@ -5,7 +5,6 @@ from django.contrib.auth.decorators import login_required
from django.contrib.postgres.search import TrigramSimilarity from django.contrib.postgres.search import TrigramSimilarity
from django.db.models.functions import Greatest from django.db.models.functions import Greatest
from django.db.models import Count, Q from django.db.models import Count, Q
from django.http import HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -91,9 +90,8 @@ class GetStartedBooks(View):
for (book_id, shelf_id) in shelve_actions: for (book_id, shelf_id) in shelve_actions:
book = get_object_or_404(models.Edition, id=book_id) book = get_object_or_404(models.Edition, id=book_id)
shelf = get_object_or_404(models.Shelf, id=shelf_id) shelf = get_object_or_404(models.Shelf, id=shelf_id)
if shelf.user != request.user: shelf.raise_not_editable(request.user)
# hmmmmm
return HttpResponseNotFound()
models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user) models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user)
return redirect(self.next_view) return redirect(self.next_view)

View file

@ -31,8 +31,8 @@ class Goal(View):
if not goal and year != timezone.now().year: if not goal and year != timezone.now().year:
return redirect("user-goal", username, current_year) return redirect("user-goal", username, current_year)
if goal and not goal.visible_to_user(request.user): if goal:
return HttpResponseNotFound() goal.raise_visible_to_user(request.user)
data = { data = {
"goal_form": forms.GoalForm(instance=goal), "goal_form": forms.GoalForm(instance=goal),
@ -45,12 +45,12 @@ class Goal(View):
def post(self, request, username, year): def post(self, request, username, year):
"""update or create an annual goal""" """update or create an annual goal"""
user = get_user_from_username(request.user, username)
if user != request.user:
return HttpResponseNotFound()
year = int(year) year = int(year)
goal = models.AnnualGoal.objects.filter(year=year, user=request.user).first() user = get_user_from_username(request.user, username)
goal = models.AnnualGoal.objects.filter(year=year, user=user).first()
if goal:
goal.raise_not_editable(request.user)
form = forms.GoalForm(request.POST, instance=goal) form = forms.GoalForm(request.POST, instance=goal)
if not form.is_valid(): if not form.is_valid():
data = { data = {
@ -62,11 +62,11 @@ class Goal(View):
goal = form.save() goal = form.save()
if request.POST.get("post-status"): if request.POST.get("post-status"):
# create status, if appropraite # create status, if appropriate
template = get_template("snippets/generated_status/goal.html") template = get_template("snippets/generated_status/goal.html")
create_generated_note( create_generated_note(
request.user, request.user,
template.render({"goal": goal, "user": request.user}).strip(), template.render({"goal": goal, "user": user}).strip(),
privacy=goal.privacy, privacy=goal.privacy,
) )
@ -78,5 +78,5 @@ class Goal(View):
def hide_goal(request): def hide_goal(request):
"""don't keep bugging people to set a goal""" """don't keep bugging people to set a goal"""
request.user.show_goal = False request.user.show_goal = False
request.user.save(broadcast=False) request.user.save(broadcast=False, update_fields=["show_goal"])
return redirect(request.headers.get("Referer", "/")) return redirect(request.headers.get("Referer", "/"))

View file

@ -80,7 +80,7 @@ class ImportStatus(View):
"""status of an import job""" """status of an import job"""
job = get_object_or_404(models.ImportJob, id=job_id) job = get_object_or_404(models.ImportJob, id=job_id)
if job.user != request.user: if job.user != request.user:
raise PermissionDenied raise PermissionDenied()
try: try:
task = app.AsyncResult(job.task_id) task = app.AsyncResult(job.task_id)

View file

@ -3,8 +3,9 @@ import json
import re import re
from urllib.parse import urldefrag from urllib.parse import urldefrag
from django.http import HttpResponse, HttpResponseNotFound from django.http import HttpResponse, Http404
from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.core.exceptions import BadRequest, PermissionDenied
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -21,36 +22,30 @@ from bookwyrm.utils import regex
class Inbox(View): class Inbox(View):
"""requests sent by outside servers""" """requests sent by outside servers"""
# pylint: disable=too-many-return-statements
def post(self, request, username=None): def post(self, request, username=None):
"""only works as POST request""" """only works as POST request"""
# first check if this server is on our shitlist # first check if this server is on our shitlist
if is_blocked_user_agent(request): raise_is_blocked_user_agent(request)
return HttpResponseForbidden()
# make sure the user's inbox even exists # make sure the user's inbox even exists
if username: if username:
try: get_object_or_404(models.User, localname=username, is_active=True)
models.User.objects.get(localname=username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
# is it valid json? does it at least vaguely resemble an activity? # is it valid json? does it at least vaguely resemble an activity?
try: try:
activity_json = json.loads(request.body) activity_json = json.loads(request.body)
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
return HttpResponseBadRequest() raise BadRequest()
# let's be extra sure we didn't block this domain # let's be extra sure we didn't block this domain
if is_blocked_activity(activity_json): raise_is_blocked_activity(activity_json)
return HttpResponseForbidden()
if ( if (
not "object" in activity_json not "object" in activity_json
or not "type" in activity_json or not "type" in activity_json
or not activity_json["type"] in activitypub.activity_objects or not activity_json["type"] in activitypub.activity_objects
): ):
return HttpResponseNotFound() raise Http404()
# verify the signature # verify the signature
if not has_valid_signature(request, activity_json): if not has_valid_signature(request, activity_json):
@ -65,32 +60,35 @@ class Inbox(View):
return HttpResponse() return HttpResponse()
def is_blocked_user_agent(request): def raise_is_blocked_user_agent(request):
"""check if a request is from a blocked server based on user agent""" """check if a request is from a blocked server based on user agent"""
# check user agent # check user agent
user_agent = request.headers.get("User-Agent") user_agent = request.headers.get("User-Agent")
if not user_agent: if not user_agent:
return False return
url = re.search(rf"https?://{regex.DOMAIN}/?", user_agent) url = re.search(rf"https?://{regex.DOMAIN}/?", user_agent)
if not url: if not url:
return False return
url = url.group() url = url.group()
return models.FederatedServer.is_blocked(url) if models.FederatedServer.is_blocked(url):
raise PermissionDenied()
def is_blocked_activity(activity_json): def raise_is_blocked_activity(activity_json):
"""get the sender out of activity json and check if it's blocked""" """get the sender out of activity json and check if it's blocked"""
actor = activity_json.get("actor") actor = activity_json.get("actor")
# check if the user is banned/deleted # check if the user is banned/deleted
existing = models.User.find_existing_by_remote_id(actor) existing = models.User.find_existing_by_remote_id(actor)
if existing and existing.deleted: if existing and existing.deleted:
return True raise PermissionDenied()
if not actor: if not actor:
# well I guess it's not even a valid activity so who knows # well I guess it's not even a valid activity so who knows
return False return
return models.FederatedServer.is_blocked(actor)
if models.FederatedServer.is_blocked(actor):
raise PermissionDenied()
@app.task(queue="medium_priority") @app.task(queue="medium_priority")

View file

@ -3,12 +3,11 @@ from typing import Optional
from urllib.parse import urlencode from urllib.parse import urlencode
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.db.models import Avg, Count, DecimalField, Q, Max from django.db.models import Avg, Count, DecimalField, Q, Max
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse from django.http import HttpResponseBadRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.urls import reverse from django.urls import reverse
@ -111,8 +110,7 @@ class List(View):
def get(self, request, list_id): def get(self, request, list_id):
"""display a book list""" """display a book list"""
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
if not book_list.visible_to_user(request.user): book_list.raise_visible_to_user(request.user)
return HttpResponseNotFound()
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse(book_list.to_activity(**request.GET)) return ActivitypubResponse(book_list.to_activity(**request.GET))
@ -193,6 +191,8 @@ class List(View):
def post(self, request, list_id): def post(self, request, list_id):
"""edit a list""" """edit a list"""
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
book_list.raise_not_editable(request.user)
form = forms.ListForm(request.POST, instance=book_list) form = forms.ListForm(request.POST, instance=book_list)
if not form.is_valid(): if not form.is_valid():
return redirect("list", book_list.id) return redirect("list", book_list.id)
@ -207,9 +207,7 @@ class Curate(View):
def get(self, request, list_id): def get(self, request, list_id):
"""display a pending list""" """display a pending list"""
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
if not book_list.user == request.user: book_list.raise_not_editable(request.user)
# only the creater can curate the list
return HttpResponseNotFound()
data = { data = {
"list": book_list, "list": book_list,
@ -223,6 +221,8 @@ class Curate(View):
def post(self, request, list_id): def post(self, request, list_id):
"""edit a book_list""" """edit a book_list"""
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
book_list.raise_not_editable(request.user)
suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item")) suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item"))
approved = request.POST.get("approved") == "true" approved = request.POST.get("approved") == "true"
if approved: if approved:
@ -270,8 +270,7 @@ def delete_list(request, list_id):
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
# only the owner or a moderator can delete a list # only the owner or a moderator can delete a list
if book_list.user != request.user and not request.user.has_perm("moderate_post"): book_list.raise_not_deletable(request.user)
raise PermissionDenied
book_list.delete() book_list.delete()
return redirect("lists") return redirect("lists")
@ -282,8 +281,7 @@ def delete_list(request, list_id):
def add_book(request): def add_book(request):
"""put a book on a list""" """put a book on a list"""
book_list = get_object_or_404(models.List, id=request.POST.get("list")) book_list = get_object_or_404(models.List, id=request.POST.get("list"))
if not book_list.visible_to_user(request.user): book_list.raise_visible_to_user(request.user)
return HttpResponseNotFound()
book = get_object_or_404(models.Edition, id=request.POST.get("book")) book = get_object_or_404(models.Edition, id=request.POST.get("book"))
# do you have permission to add to the list? # do you have permission to add to the list?
@ -331,13 +329,11 @@ def add_book(request):
@login_required @login_required
def remove_book(request, list_id): def remove_book(request, list_id):
"""remove a book from a list""" """remove a book from a list"""
with transaction.atomic():
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
item = get_object_or_404(models.ListItem, id=request.POST.get("item")) item = get_object_or_404(models.ListItem, id=request.POST.get("item"))
item.raise_not_deletable(request.user)
if not book_list.user == request.user and not item.user == request.user: with transaction.atomic():
return HttpResponseNotFound()
deleted_order = item.order deleted_order = item.order
item.delete() item.delete()
normalize_book_list_ordering(book_list.id, start=deleted_order) normalize_book_list_ordering(book_list.id, start=deleted_order)
@ -351,14 +347,12 @@ def set_book_position(request, list_item_id):
Action for when the list user manually specifies a list position, takes Action for when the list user manually specifies a list position, takes
special care with the unique ordering per list. special care with the unique ordering per list.
""" """
with transaction.atomic():
list_item = get_object_or_404(models.ListItem, id=list_item_id) list_item = get_object_or_404(models.ListItem, id=list_item_id)
list_item.book_list.raise_not_editable(request.user)
try: try:
int_position = int(request.POST.get("position")) int_position = int(request.POST.get("position"))
except ValueError: except ValueError:
return HttpResponseBadRequest( return HttpResponseBadRequest("bad value for position. should be an integer")
"bad value for position. should be an integer"
)
if int_position < 1: if int_position < 1:
return HttpResponseBadRequest("position cannot be less than 1") return HttpResponseBadRequest("position cannot be less than 1")
@ -367,18 +361,18 @@ def set_book_position(request, list_item_id):
# the max position to which a book may be set is the highest order for # the max position to which a book may be set is the highest order for
# books which are approved # books which are approved
order_max = book_list.listitem_set.filter(approved=True).aggregate( order_max = book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
Max("order") "order__max"
)["order__max"] ]
int_position = min(int_position, order_max) int_position = min(int_position, order_max)
if request.user not in (book_list.user, list_item.user):
return HttpResponseNotFound()
original_order = list_item.order original_order = list_item.order
if original_order == int_position: if original_order == int_position:
# no change
return HttpResponse(status=204) return HttpResponse(status=204)
with transaction.atomic():
if original_order > int_position: if original_order > int_position:
list_item.order = -1 list_item.order = -1
list_item.save() list_item.save()

View file

@ -54,9 +54,9 @@ class PasswordReset(View):
try: try:
reset_code = models.PasswordReset.objects.get(code=code) reset_code = models.PasswordReset.objects.get(code=code)
if not reset_code.valid(): if not reset_code.valid():
raise PermissionDenied raise PermissionDenied()
except models.PasswordReset.DoesNotExist: except models.PasswordReset.DoesNotExist:
raise PermissionDenied raise PermissionDenied()
return TemplateResponse(request, "password_reset.html", {"code": code}) return TemplateResponse(request, "password_reset.html", {"code": code})

View file

@ -45,9 +45,9 @@ class ReadingStatus(View):
if not identifier: if not identifier:
return HttpResponseBadRequest() return HttpResponseBadRequest()
desired_shelf = models.Shelf.objects.filter( desired_shelf = get_object_or_404(
identifier=identifier, user=request.user models.Shelf, identifier=identifier, user=request.user
).first() )
book = ( book = (
models.Edition.viewer_aware_objects(request.user) models.Edition.viewer_aware_objects(request.user)
@ -138,10 +138,7 @@ def update_readthrough_on_shelve(
def edit_readthrough(request): def edit_readthrough(request):
"""can't use the form because the dates are too finnicky""" """can't use the form because the dates are too finnicky"""
readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id")) readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id"))
readthrough.raise_not_editable(request.user)
# don't let people edit other people's data
if request.user != readthrough.user:
return HttpResponseBadRequest()
readthrough.start_date = load_date_in_user_tz_as_utc( readthrough.start_date = load_date_in_user_tz_as_utc(
request.POST.get("start_date"), request.user request.POST.get("start_date"), request.user
@ -178,10 +175,7 @@ def edit_readthrough(request):
def delete_readthrough(request): def delete_readthrough(request):
"""remove a readthrough""" """remove a readthrough"""
readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id")) readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id"))
readthrough.raise_not_deletable(request.user)
# don't let people edit other people's data
if request.user != readthrough.user:
return HttpResponseBadRequest()
readthrough.delete() readthrough.delete()
return redirect(request.headers.get("Referer", "/")) return redirect(request.headers.get("Referer", "/"))
@ -225,10 +219,7 @@ def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
def delete_progressupdate(request): def delete_progressupdate(request):
"""remove a progress update""" """remove a progress update"""
update = get_object_or_404(models.ProgressUpdate, id=request.POST.get("id")) update = get_object_or_404(models.ProgressUpdate, id=request.POST.get("id"))
update.raise_not_deletable(request.user)
# don't let people edit other people's data
if request.user != update.user:
return HttpResponseBadRequest()
update.delete() update.delete()
return redirect(request.headers.get("Referer", "/")) return redirect(request.headers.get("Referer", "/"))

View file

@ -29,11 +29,11 @@ class Register(View):
invite_code = request.POST.get("invite_code") invite_code = request.POST.get("invite_code")
if not invite_code: if not invite_code:
raise PermissionDenied raise PermissionDenied()
invite = get_object_or_404(models.SiteInvite, code=invite_code) invite = get_object_or_404(models.SiteInvite, code=invite_code)
if not invite.valid(): if not invite.valid():
raise PermissionDenied raise PermissionDenied()
else: else:
invite = None invite = None

View file

@ -1,11 +1,11 @@
""" shelf views """ """ shelf views """
from collections import namedtuple from collections import namedtuple
from django.db import IntegrityError from django.db import IntegrityError, transaction
from django.db.models import OuterRef, Subquery, F from django.db.models import OuterRef, Subquery, F
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -16,7 +16,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, get_edition, get_user_from_username from .helpers import is_api_request, get_user_from_username
from .helpers import privacy_filter from .helpers import privacy_filter
@ -37,15 +37,11 @@ class Shelf(View):
# get the shelf and make sure the logged in user should be able to see it # get the shelf and make sure the logged in user should be able to see it
if shelf_identifier: if shelf_identifier:
try: shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
shelf = user.shelf_set.get(identifier=shelf_identifier) shelf.raise_visible_to_user(request.user)
except models.Shelf.DoesNotExist:
return HttpResponseNotFound()
if not shelf.visible_to_user(request.user):
return HttpResponseNotFound()
books = shelf.books books = shelf.books
# this is a constructed "all books" view, with a fake "shelf" obj
else: else:
# this is a constructed "all books" view, with a fake "shelf" obj
FakeShelf = namedtuple( FakeShelf = namedtuple(
"Shelf", ("identifier", "name", "user", "books", "privacy") "Shelf", ("identifier", "name", "user", "books", "privacy")
) )
@ -100,13 +96,11 @@ class Shelf(View):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def post(self, request, username, shelf_identifier): def post(self, request, username, shelf_identifier):
"""edit a shelf""" """edit a shelf"""
try: user = get_user_from_username(request.user, username)
shelf = request.user.shelf_set.get(identifier=shelf_identifier) shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
except models.Shelf.DoesNotExist: shelf.raise_not_editable(request.user)
return HttpResponseNotFound()
if request.user != shelf.user: # you can't change the name of the default shelves
return HttpResponseBadRequest()
if not shelf.editable and request.POST.get("name") != shelf.name: if not shelf.editable and request.POST.get("name") != shelf.name:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -134,8 +128,7 @@ def create_shelf(request):
def delete_shelf(request, shelf_id): def delete_shelf(request, shelf_id):
"""user generated shelves""" """user generated shelves"""
shelf = get_object_or_404(models.Shelf, id=shelf_id) shelf = get_object_or_404(models.Shelf, id=shelf_id)
if request.user != shelf.user or not shelf.editable: shelf.raise_not_deletable()
return HttpResponseBadRequest()
shelf.delete() shelf.delete()
return redirect("user-shelves", request.user.localname) return redirect("user-shelves", request.user.localname)
@ -143,25 +136,28 @@ def delete_shelf(request, shelf_id):
@login_required @login_required
@require_POST @require_POST
@transaction.atomic
def shelve(request): def shelve(request):
"""put a book on a user's shelf""" """put a book on a user's shelf"""
book = get_edition(request.POST.get("book")) book = get_object_or_404(models.Edition, id=request.POST.get("book"))
desired_shelf = get_object_or_404(
desired_shelf = models.Shelf.objects.filter( request.user.shelf_set, identifier=request.POST.get("shelf")
identifier=request.POST.get("shelf"), user=request.user
).first()
if not desired_shelf:
return HttpResponseNotFound()
change_from_current_identifier = request.POST.get("change-shelf-from")
if change_from_current_identifier is not None:
current_shelf = models.Shelf.objects.get(
user=request.user, identifier=change_from_current_identifier
) )
handle_unshelve(book, current_shelf)
# first we need to remove from the specified shelf
change_from_current_identifier = request.POST.get("change-shelf-from")
if change_from_current_identifier:
# find the shelfbook obj and delete it
get_object_or_404(
models.ShelfBook,
book=book,
user=request.user,
shelf__identifier=change_from_current_identifier,
).delete()
# A book can be on multiple shelves, but only on one read status shelf at a time # A book can be on multiple shelves, but only on one read status shelf at a time
if desired_shelf.identifier in models.Shelf.READ_STATUS_IDENTIFIERS: if desired_shelf.identifier in models.Shelf.READ_STATUS_IDENTIFIERS:
# figure out where state shelf it's currently on (if any)
current_read_status_shelfbook = ( current_read_status_shelfbook = (
models.ShelfBook.objects.select_related("shelf") models.ShelfBook.objects.select_related("shelf")
.filter( .filter(
@ -176,14 +172,16 @@ def shelve(request):
current_read_status_shelfbook.shelf.identifier current_read_status_shelfbook.shelf.identifier
!= desired_shelf.identifier != desired_shelf.identifier
): ):
handle_unshelve(book, current_read_status_shelfbook.shelf) current_read_status_shelfbook.delete()
else: # It is already on the shelf else: # It is already on the shelf
return redirect(request.headers.get("Referer", "/")) return redirect(request.headers.get("Referer", "/"))
# create the new shelf-book entry
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=book, shelf=desired_shelf, user=request.user book=book, shelf=desired_shelf, user=request.user
) )
else: else:
# we're putting it on a custom shelf
try: try:
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=book, shelf=desired_shelf, user=request.user book=book, shelf=desired_shelf, user=request.user
@ -199,14 +197,11 @@ def shelve(request):
@require_POST @require_POST
def unshelve(request): def unshelve(request):
"""put a on a user's shelf""" """put a on a user's shelf"""
book = models.Edition.objects.get(id=request.POST["book"]) book = get_object_or_404(models.Edition, id=request.POST.get("book"))
current_shelf = models.Shelf.objects.get(id=request.POST["shelf"]) shelf_book = get_object_or_404(
models.ShelfBook, book=book, shelf__id=request.POST["shelf"]
)
shelf_book.raise_not_deletable(request.user)
handle_unshelve(book, current_shelf) shelf_book.delete()
return redirect(request.headers.get("Referer", "/")) return redirect(request.headers.get("Referer", "/"))
def handle_unshelve(book, shelf):
"""unshelve a book"""
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
row.delete()

View file

@ -98,8 +98,7 @@ class DeleteStatus(View):
status = get_object_or_404(models.Status, id=status_id) status = get_object_or_404(models.Status, id=status_id)
# don't let people delete other people's statuses # don't let people delete other people's statuses
if status.user != request.user and not request.user.has_perm("moderate_post"): status.raise_not_deletable(request.user)
return HttpResponseBadRequest()
# perform deletion # perform deletion
status.delete() status.delete()
@ -115,12 +114,8 @@ class DeleteAndRedraft(View):
status = get_object_or_404( status = get_object_or_404(
models.Status.objects.select_subclasses(), id=status_id models.Status.objects.select_subclasses(), id=status_id
) )
if isinstance(status, (models.GeneratedNote, models.ReviewRating)):
return HttpResponseBadRequest()
# don't let people redraft other people's statuses # don't let people redraft other people's statuses
if status.user != request.user: status.raise_not_editable(request.user)
return HttpResponseBadRequest()
status_type = status.status_type.lower() status_type = status.status_type.lower()
if status.reply_parent: if status.reply_parent:

View file

@ -1,6 +1,7 @@
""" non-interactive pages """ """ non-interactive pages """
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone from django.utils import timezone
@ -77,8 +78,12 @@ class User(View):
goal = models.AnnualGoal.objects.filter( goal = models.AnnualGoal.objects.filter(
user=user, year=timezone.now().year user=user, year=timezone.now().year
).first() ).first()
if goal and not goal.visible_to_user(request.user): if goal:
try:
goal.raise_visible_to_user(request.user)
except Http404:
goal = None goal = None
data = { data = {
"user": user, "user": user,
"is_self": is_self, "is_self": is_self,

View file

@ -3,6 +3,7 @@
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.http import HttpResponseNotFound from django.http import HttpResponseNotFound
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone from django.utils import timezone
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
@ -19,10 +20,7 @@ def webfinger(request):
return HttpResponseNotFound() return HttpResponseNotFound()
username = resource.replace("acct:", "") username = resource.replace("acct:", "")
try: user = get_object_or_404(models.User, username__iexact=username)
user = models.User.objects.get(username__iexact=username)
except models.User.DoesNotExist:
return HttpResponseNotFound("No account found")
return JsonResponse( return JsonResponse(
{ {