mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-27 12:01:14 +00:00
Merge pull request #1463 from bookwyrm-social/list-item-perms
Updates how permissions are verified in views
This commit is contained in:
commit
46a7030dbc
31 changed files with 264 additions and 236 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(response.status_code, 403)
|
||||||
self.assertEqual(result.status_code, 403)
|
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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, _):
|
||||||
|
|
|
@ -568,5 +568,6 @@ class ListActionViews(TestCase):
|
||||||
)
|
)
|
||||||
request.user = self.rat
|
request.user = self.rat
|
||||||
|
|
||||||
views.list.remove_book(request, self.list.id)
|
with self.assertRaises(PermissionDenied):
|
||||||
|
views.list.remove_book(request, self.list.id)
|
||||||
self.assertTrue(self.list.listitem_set.exists())
|
self.assertTrue(self.list.listitem_set.exists())
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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,7 +376,8 @@ http://www.fish.com/"""
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
request.user = self.remote_user
|
request.user = self.remote_user
|
||||||
|
|
||||||
view(request, status.id)
|
with self.assertRaises(PermissionDenied):
|
||||||
|
view(request, status.id)
|
||||||
|
|
||||||
status.refresh_from_db()
|
status.refresh_from_db()
|
||||||
self.assertFalse(status.deleted)
|
self.assertFalse(status.deleted)
|
||||||
|
|
|
@ -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",
|
||||||
),
|
),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 = get_object_or_404(
|
||||||
status = models.Status.objects.select_subclasses().get(
|
models.Status.objects.select_subclasses(),
|
||||||
user=user, id=status_id, deleted=False
|
user=user,
|
||||||
)
|
id=status_id,
|
||||||
except (ValueError, models.Status.DoesNotExist):
|
deleted=False,
|
||||||
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))
|
||||||
|
|
||||||
|
|
|
@ -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
|
)
|
||||||
)
|
follow_request.raise_not_deletable(request.user)
|
||||||
except models.UserFollowRequest.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
follow_request.delete()
|
follow_request.delete()
|
||||||
return redirect(f"/user/{request.user.localname}")
|
return redirect(f"/user/{request.user.localname}")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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", "/"))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,16 +329,14 @@ 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"""
|
||||||
|
book_list = get_object_or_404(models.List, id=list_id)
|
||||||
|
item = get_object_or_404(models.ListItem, id=request.POST.get("item"))
|
||||||
|
item.raise_not_deletable(request.user)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
book_list = get_object_or_404(models.List, id=list_id)
|
|
||||||
item = get_object_or_404(models.ListItem, id=request.POST.get("item"))
|
|
||||||
|
|
||||||
if not book_list.user == request.user and not item.user == request.user:
|
|
||||||
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)
|
||||||
return redirect("list", list_id)
|
return redirect("list", list_id)
|
||||||
|
|
||||||
|
|
||||||
|
@ -351,34 +347,32 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
list_item = get_object_or_404(models.ListItem, id=list_item_id)
|
||||||
|
list_item.book_list.raise_not_editable(request.user)
|
||||||
|
try:
|
||||||
|
int_position = int(request.POST.get("position"))
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest("bad value for position. should be an integer")
|
||||||
|
|
||||||
|
if int_position < 1:
|
||||||
|
return HttpResponseBadRequest("position cannot be less than 1")
|
||||||
|
|
||||||
|
book_list = list_item.book_list
|
||||||
|
|
||||||
|
# the max position to which a book may be set is the highest order for
|
||||||
|
# books which are approved
|
||||||
|
order_max = book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
|
||||||
|
"order__max"
|
||||||
|
]
|
||||||
|
|
||||||
|
int_position = min(int_position, order_max)
|
||||||
|
|
||||||
|
original_order = list_item.order
|
||||||
|
if original_order == int_position:
|
||||||
|
# no change
|
||||||
|
return HttpResponse(status=204)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
list_item = get_object_or_404(models.ListItem, id=list_item_id)
|
|
||||||
try:
|
|
||||||
int_position = int(request.POST.get("position"))
|
|
||||||
except ValueError:
|
|
||||||
return HttpResponseBadRequest(
|
|
||||||
"bad value for position. should be an integer"
|
|
||||||
)
|
|
||||||
|
|
||||||
if int_position < 1:
|
|
||||||
return HttpResponseBadRequest("position cannot be less than 1")
|
|
||||||
|
|
||||||
book_list = list_item.book_list
|
|
||||||
|
|
||||||
# the max position to which a book may be set is the highest order for
|
|
||||||
# books which are approved
|
|
||||||
order_max = book_list.listitem_set.filter(approved=True).aggregate(
|
|
||||||
Max("order")
|
|
||||||
)["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
|
|
||||||
if original_order == int_position:
|
|
||||||
return HttpResponse(status=204)
|
|
||||||
if original_order > int_position:
|
if original_order > int_position:
|
||||||
list_item.order = -1
|
list_item.order = -1
|
||||||
list_item.save()
|
list_item.save()
|
||||||
|
|
|
@ -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})
|
||||||
|
|
||||||
|
|
|
@ -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", "/"))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
||||||
|
# first we need to remove from the specified shelf
|
||||||
change_from_current_identifier = request.POST.get("change-shelf-from")
|
change_from_current_identifier = request.POST.get("change-shelf-from")
|
||||||
if change_from_current_identifier is not None:
|
if change_from_current_identifier:
|
||||||
current_shelf = models.Shelf.objects.get(
|
# find the shelfbook obj and delete it
|
||||||
user=request.user, identifier=change_from_current_identifier
|
get_object_or_404(
|
||||||
)
|
models.ShelfBook,
|
||||||
handle_unshelve(book, current_shelf)
|
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
|
||||||
|
@ -198,15 +196,12 @@ def shelve(request):
|
||||||
@login_required
|
@login_required
|
||||||
@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()
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
goal = None
|
try:
|
||||||
|
goal.raise_visible_to_user(request.user)
|
||||||
|
except Http404:
|
||||||
|
goal = None
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"is_self": is_self,
|
"is_self": is_self,
|
||||||
|
|
|
@ -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(
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue