Merge pull request #555 from mouse-reeve/blocking

Blocking
This commit is contained in:
Mouse Reeve 2021-01-26 14:43:01 -08:00 committed by GitHub
commit 662cbf2da9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 696 additions and 182 deletions

View file

@ -14,7 +14,7 @@ from .person import Person, PublicKey
from .response import ActivitypubResponse
from .book import Edition, Work, Author
from .verbs import Create, Delete, Undo, Update
from .verbs import Follow, Accept, Reject
from .verbs import Follow, Accept, Reject, Block
from .verbs import Add, AddBook, Remove
# this creates a list of all the Activity types that we can serialize,

View file

@ -48,6 +48,10 @@ class Follow(Verb):
''' Follow activity '''
type: str = 'Follow'
@dataclass(init=False)
class Block(Verb):
''' Block activity '''
type: str = 'Block'
@dataclass(init=False)
class Accept(Verb):

View file

@ -51,6 +51,7 @@ def shared_inbox(request):
'Follow': handle_follow,
'Accept': handle_follow_accept,
'Reject': handle_follow_reject,
'Block': handle_block,
'Create': handle_create,
'Delete': handle_delete_status,
'Like': handle_favorite,
@ -62,6 +63,7 @@ def shared_inbox(request):
'Follow': handle_unfollow,
'Like': handle_unfavorite,
'Announce': handle_unboost,
'Block': handle_unblock,
},
'Update': {
'Person': handle_update_user,
@ -179,6 +181,27 @@ def handle_follow_reject(activity):
request.delete()
#raises models.UserFollowRequest.DoesNotExist
@app.task
def handle_block(activity):
''' blocking a user '''
# create "block" databse entry
activitypub.Block(**activity).to_model(models.UserBlocks)
# the removing relationships is handled in post-save hook in model
@app.task
def handle_unblock(activity):
''' undoing a block '''
try:
block_id = activity['object']['id']
except KeyError:
return
try:
block = models.UserBlocks.objects.get(remote_id=block_id)
except models.UserBlocks.DoesNotExist:
return
block.delete()
@app.task
def handle_create(activity):

View file

@ -1,5 +1,7 @@
''' defines relationships between users '''
from django.db import models
from django.db.models import Q
from django.dispatch import receiver
from bookwyrm import activitypub
from .base_model import ActivitypubMixin, BookWyrmModel
@ -94,5 +96,23 @@ class UserFollowRequest(UserRelationship):
class UserBlocks(UserRelationship):
''' prevent another user from following you and seeing your posts '''
# TODO: not implemented
status = 'blocks'
activity_serializer = activitypub.Block
@receiver(models.signals.post_save, sender=UserBlocks)
#pylint: disable=unused-argument
def execute_after_save(sender, instance, created, *args, **kwargs):
''' remove follow or follow request rels after a block is created '''
UserFollows.objects.filter(
Q(user_subject=instance.user_subject,
user_object=instance.user_object) | \
Q(user_subject=instance.user_object,
user_object=instance.user_subject)
).delete()
UserFollowRequest.objects.filter(
Q(user_subject=instance.user_subject,
user_object=instance.user_object) | \
Q(user_subject=instance.user_object,
user_object=instance.user_subject)
).delete()

View file

@ -0,0 +1,24 @@
{% extends 'preferences_layout.html' %}
{% block header %}
Blocked Users
{% endblock %}
{% block panel %}
{% if not request.user.blocks.exists %}
<p>No users currently blocked.</p>
{% else %}
<ul>
{% for user in request.user.blocks.all %}
<li class="is-flex">
<p>
{% include 'snippets/avatar.html' with user=user %} {% include 'snippets/username.html' with user=user %}
</p>
<p class="mr-2">
{% include 'snippets/block_button.html' with user=user %}
</p>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends 'preferences_layout.html' %}
{% block header %}
Change Password
{% endblock %}
{% block panel %}
<form name="edit-profile" action="/change-password/" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="block">
<label class="label" for="id_password">New password:</label>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password">
</div>
<div class="block">
<label class="label" for="id_confirm_password">Confirm password:</label>
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password">
</div>
<button class="button is-primary" type="submit">Change password</button>
</form>
{% endblock %}

View file

@ -1,66 +1,48 @@
{% extends 'layout.html' %}
{% block content %}
<div class="block columns">
<div class="column is-half">
<h1 class="title">Profile</h1>
{% if form.non_field_errors %}
<p class="notification is-danger">{{ form.non_field_errors }}</p>
{% endif %}
<form name="edit-profile" action="/edit-profile/" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="block">
<label class="label" for="id_avatar">Avatar:</label>
{{ form.avatar }}
{% for error in form.avatar.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_name">Display name:</label>
{{ form.name }}
{% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_summary">Summary:</label>
{{ form.summary }}
{% for error in form.summary.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_email">Email address:</label>
{{ form.email }}
{% for error in form.email.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="checkbox label" for="id_manually_approves_followers">
Manually approve followers:
{{ form.manually_approves_followers }}
</label>
</div>
<button class="button is-primary" type="submit">Save</button>
</form>
</div>
<div class="column is-half">
<div class="block">
<h2 class="title">Change password</h2>
<form name="edit-profile" action="/change-password/" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="block">
<label class="label" for="id_password">New password:</label>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password">
</div>
<div class="block">
<label class="label" for="id_confirm_password">Confirm password:</label>
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password">
</div>
<button class="button is-primary" type="submit">Change password</button>
</form>
</div>
</div>
</div>
{% extends 'preferences_layout.html' %}
{% block header %}
Edit Profile
{% endblock %}
{% block panel %}
{% if form.non_field_errors %}
<p class="notification is-danger">{{ form.non_field_errors }}</p>
{% endif %}
<form name="edit-profile" action="/edit-profile/" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="block">
<label class="label" for="id_avatar">Avatar:</label>
{{ form.avatar }}
{% for error in form.avatar.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_name">Display name:</label>
{{ form.name }}
{% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_summary">Summary:</label>
{{ form.summary }}
{% for error in form.summary.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_email">Email address:</label>
{{ form.email }}
{% for error in form.email.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="checkbox label" for="id_manually_approves_followers">
Manually approve followers:
{{ form.manually_approves_followers }}
</label>
</div>
<button class="button is-primary" type="submit">Save</button>
</form>
{% endblock %}

View file

@ -0,0 +1,31 @@
{% extends 'layout.html' %}
{% block content %}
<header class="block column is-offset-one-quarter pl-1">
<h1 class="title">{% block header %}{% endblock %}</h1>
</header>
<div class="block columns">
<nav class="menu column is-one-quarter">
<h2 class="menu-label">Account</h2>
<ul class="menu-list">
<li>
<a href="/edit-profile"{% if '/edit-profile' in request.path %} class="is-active" aria-selected="true"{% endif %}>Profile</a>
</li>
<li>
<a href="/change-password"{% if '/change-password' in request.path %} class="is-active" aria-selected="true"{% endif %}>Change password</a>
</li>
</ul>
<h2 class="menu-label">Relationships</h2>
<ul class="menu-list">
<li>
<a href="/block"{% if '/block' in request.path %} class="is-active" aria-selected="true"{% endif %}>Blocked users</a>
</li>
</ul>
</nav>
<div class="column content">
{% block panel %}{% endblock %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,11 @@
{% if not user in request.user.blocks.all %}
<form name="blocks" method="post" action="/block/{{ user.id }}">
{% csrf_token %}
<button class="button is-danger is-light is-small {{ class }}" type="submit">Block</button>
</form>
{% else %}
<form name="unblocks" method="post" action="/unblock/{{ user.id }}">
{% csrf_token %}
<button class="button is-small {{ class }}" type="submit">Un-block</button>
</form>
{% endif %}

View file

@ -54,11 +54,9 @@
<div class="card-footer-item">
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
</div>
{% if status.user == request.user %}
<div class="card-footer-item">
{% include 'snippets/status_options.html' with class="is-small" right=True %}
</div>
{% endif %}
{% endblock %}

View file

@ -1,4 +1,5 @@
{% extends 'snippets/components/dropdown.html' %}
{% load bookwyrm_tags %}
{% block dropdown-trigger %}
<span class="icon icon-dots-three">
@ -7,6 +8,7 @@
{% endblock %}
{% block dropdown-list %}
{% if status.user == request.user %}
<li role="menuitem">
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %}
@ -15,4 +17,9 @@
</button>
</form>
</li>
{% else %}
<li role="menuitem">
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
</li>
{% endif %}
{% endblock %}

View file

@ -35,7 +35,14 @@
</div>
</div>
{% if not is_self %}
{% include 'snippets/follow_button.html' with user=user %}
<div class="field has-addons">
<div class="control">
{% include 'snippets/follow_button.html' with user=user %}
</div>
<div class="control">
{% include 'snippets/user_options.html' with user=user class="is-small" %}
</div>
</div>
{% endif %}
{% if is_self and user.follower_requests.all %}

View file

@ -0,0 +1,14 @@
{% extends 'snippets/components/dropdown.html' %}
{% load bookwyrm_tags %}
{% block dropdown-trigger %}
<span class="icon icon-dots-three">
<span class="is-sr-only">More options</span>
</span>
{% endblock %}
{% block dropdown-list %}
<li role="menuitem">
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" %}
</li>
{% endblock %}

View file

@ -17,7 +17,7 @@
</div>
{% include 'snippets/user_header.html' with user=user %}
{% if user.bookwyrm_user %}
<div class="block">
<h2 class="title">Shelves</h2>
<div class="columns">
@ -39,6 +39,7 @@
</div>
<small><a href="{{ user.local_path }}/shelves">See all {{ shelf_count }} shelves</a></small>
</div>
{% endif %}
{% if goal %}
<div class="block">

View file

@ -540,3 +540,46 @@ class Incoming(TestCase):
incoming.handle_update_work({'object': bookdata})
book = models.Work.objects.get(id=book.id)
self.assertEqual(book.title, 'Piranesi')
def test_handle_blocks(self):
''' create a "block" database entry from an activity '''
self.local_user.followers.add(self.remote_user)
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user)
self.assertTrue(models.UserFollows.objects.exists())
self.assertTrue(models.UserFollowRequest.objects.exists())
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/9e1f41ac-9ddd-4159-aede-9f43c6b9314f",
"type": "Block",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse"
}
incoming.handle_block(activity)
block = models.UserBlocks.objects.get()
self.assertEqual(block.user_subject, self.remote_user)
self.assertEqual(block.user_object, self.local_user)
self.assertFalse(models.UserFollows.objects.exists())
self.assertFalse(models.UserFollowRequest.objects.exists())
def test_handle_unblock(self):
''' undoing a block '''
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://friend.camp/users/tripofmice#blocks/1155/undo",
"type": "Undo",
"actor": "https://friend.camp/users/tripofmice",
"object": {
"id": "https://friend.camp/0a7d85f7-6359-4c03-8ab6-74e61a8fb678",
"type": "Block",
"actor": "https://friend.camp/users/tripofmice",
"object": "https://1b1a78582461.ngrok.io/user/mouse"
}
}
self.remote_user.blocks.add(self.local_user)

View file

@ -42,83 +42,6 @@ class AuthenticationViews(TestCase):
self.assertEqual(result.status_code, 302)
def test_password_reset_request(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.PasswordResetRequest.as_view()
request = self.factory.get('')
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'password_reset_request.html')
self.assertEqual(result.status_code, 200)
def test_password_reset_request_post(self):
''' send 'em an email '''
request = self.factory.post('', {'email': 'aa@bb.ccc'})
view = views.PasswordResetRequest.as_view()
resp = view(request)
self.assertEqual(resp.status_code, 302)
request = self.factory.post('', {'email': 'mouse@mouse.com'})
with patch('bookwyrm.emailing.send_email.delay'):
resp = view(request)
self.assertEqual(resp.template_name, 'password_reset_request.html')
self.assertEqual(
models.PasswordReset.objects.get().user, self.local_user)
def test_password_reset(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.PasswordReset.as_view()
code = models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.get('')
request.user = self.anonymous_user
result = view(request, code.code)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'password_reset.html')
self.assertEqual(result.status_code, 200)
def test_password_reset_post(self):
''' reset from code '''
view = views.PasswordReset.as_view()
code = models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hi'
})
with patch('bookwyrm.views.password.login'):
resp = view(request, code.code)
self.assertEqual(resp.status_code, 302)
self.assertFalse(models.PasswordReset.objects.exists())
def test_password_reset_wrong_code(self):
''' reset from code '''
view = views.PasswordReset.as_view()
models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hi'
})
resp = view(request, 'jhgdkfjgdf')
self.assertEqual(resp.template_name, 'password_reset.html')
self.assertTrue(models.PasswordReset.objects.exists())
def test_password_reset_mismatch(self):
''' reset from code '''
view = views.PasswordReset.as_view()
code = models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hihi'
})
resp = view(request, code.code)
self.assertEqual(resp.template_name, 'password_reset.html')
self.assertTrue(models.PasswordReset.objects.exists())
def test_register(self):
''' create a user '''
view = views.Register.as_view()
@ -274,29 +197,3 @@ class AuthenticationViews(TestCase):
with self.assertRaises(Http404):
response = view(request)
self.assertEqual(models.User.objects.count(), 2)
def test_password_change(self):
''' change password '''
view = views.ChangePassword.as_view()
password_hash = self.local_user.password
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hi'
})
request.user = self.local_user
with patch('bookwyrm.views.password.login'):
view(request)
self.assertNotEqual(self.local_user.password, password_hash)
def test_password_change_mismatch(self):
''' change password '''
view = views.ChangePassword.as_view()
password_hash = self.local_user.password
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hihi'
})
request.user = self.local_user
view(request)
self.assertEqual(self.local_user.password, password_hash)

View file

@ -0,0 +1,68 @@
''' test for app action functionality '''
from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
class BlockViews(TestCase):
''' view user and edit profile '''
def setUp(self):
''' we need basic test data and mocks '''
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
'mouse@local.com', 'mouse@mouse.mouse', 'password',
local=True, localname='mouse')
with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
local=False,
remote_id='https://example.com/users/rat',
inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox',
)
def test_block_get(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Block.as_view()
request = self.factory.get('')
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'blocks.html')
self.assertEqual(result.status_code, 200)
def test_block_post(self):
''' create a "block" database entry from an activity '''
view = views.Block.as_view()
self.local_user.followers.add(self.remote_user)
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user)
self.assertTrue(models.UserFollows.objects.exists())
self.assertTrue(models.UserFollowRequest.objects.exists())
request = self.factory.post('')
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, self.remote_user.id)
block = models.UserBlocks.objects.get()
self.assertEqual(block.user_subject, self.local_user)
self.assertEqual(block.user_object, self.remote_user)
self.assertFalse(models.UserFollows.objects.exists())
self.assertFalse(models.UserFollowRequest.objects.exists())
def test_unblock(self):
''' undo a block '''
self.local_user.blocks.add(self.remote_user)
request = self.factory.post('')
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
views.block.unblock(request, self.remote_user.id)
self.assertFalse(models.UserBlocks.objects.exists())

View file

@ -154,6 +154,34 @@ class ViewsHelpers(TestCase):
self.assertEqual(statuses[0], rat_mention)
def test_get_activity_feed_blocks(self):
''' feed generation with blocked users '''
rat = models.User.objects.create_user(
'rat', 'rat@rat.rat', 'password', local=True)
public_status = models.Comment.objects.create(
content='public status', book=self.book, user=self.local_user)
rat_public = models.Status.objects.create(
content='blah blah', user=rat)
statuses = views.helpers.get_activity_feed(
self.local_user, ['public'])
self.assertEqual(len(statuses), 2)
# block relationship
rat.blocks.add(self.local_user)
statuses = views.helpers.get_activity_feed(
self.local_user, ['public'])
self.assertEqual(len(statuses), 1)
self.assertEqual(statuses[0], public_status)
statuses = views.helpers.get_activity_feed(
rat, ['public'])
self.assertEqual(len(statuses), 1)
self.assertEqual(statuses[0], rat_public)
def test_is_bookwyrm_request(self):
''' checks if a request came from a bookwyrm instance '''
request = self.factory.get('', {'q': 'Test Book'})
@ -248,3 +276,63 @@ class ViewsHelpers(TestCase):
views.helpers.handle_reading_status(
self.local_user, self.shelf, self.book, 'public')
self.assertFalse(models.GeneratedNote.objects.exists())
def test_object_visible_to_user(self):
''' does a user have permission to view an object '''
obj = models.Status.objects.create(
content='hi', user=self.remote_user, privacy='public')
self.assertTrue(
views.helpers.object_visible_to_user(self.local_user, obj))
obj = models.Shelf.objects.create(
name='test', user=self.remote_user, privacy='unlisted')
self.assertTrue(
views.helpers.object_visible_to_user(self.local_user, obj))
obj = models.Status.objects.create(
content='hi', user=self.remote_user, privacy='followers')
self.assertFalse(
views.helpers.object_visible_to_user(self.local_user, obj))
obj = models.Status.objects.create(
content='hi', user=self.remote_user, privacy='direct')
self.assertFalse(
views.helpers.object_visible_to_user(self.local_user, obj))
obj = models.Status.objects.create(
content='hi', user=self.remote_user, privacy='direct')
obj.mention_users.add(self.local_user)
self.assertTrue(
views.helpers.object_visible_to_user(self.local_user, obj))
def test_object_visible_to_user_follower(self):
''' what you can see if you follow a user '''
self.remote_user.followers.add(self.local_user)
obj = models.Status.objects.create(
content='hi', user=self.remote_user, privacy='followers')
self.assertTrue(
views.helpers.object_visible_to_user(self.local_user, obj))
obj = models.Status.objects.create(
content='hi', user=self.remote_user, privacy='direct')
self.assertFalse(
views.helpers.object_visible_to_user(self.local_user, obj))
obj = models.Status.objects.create(
content='hi', user=self.remote_user, privacy='direct')
obj.mention_users.add(self.local_user)
self.assertTrue(
views.helpers.object_visible_to_user(self.local_user, obj))
def test_object_visible_to_user_blocked(self):
''' you can't see it if they block you '''
self.remote_user.blocks.add(self.local_user)
obj = models.Status.objects.create(
content='hi', user=self.remote_user, privacy='public')
self.assertFalse(
views.helpers.object_visible_to_user(self.local_user, obj))
obj = models.Shelf.objects.create(
name='test', user=self.remote_user, privacy='unlisted')
self.assertFalse(
views.helpers.object_visible_to_user(self.local_user, obj))

View file

@ -0,0 +1,136 @@
''' test for app action functionality '''
from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
class PasswordViews(TestCase):
''' view user and edit profile '''
def setUp(self):
''' we need basic test data and mocks '''
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
'mouse@local.com', 'mouse@mouse.com', 'password',
local=True, localname='mouse')
self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False
def test_password_reset_request(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.PasswordResetRequest.as_view()
request = self.factory.get('')
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'password_reset_request.html')
self.assertEqual(result.status_code, 200)
def test_password_reset_request_post(self):
''' send 'em an email '''
request = self.factory.post('', {'email': 'aa@bb.ccc'})
view = views.PasswordResetRequest.as_view()
resp = view(request)
self.assertEqual(resp.status_code, 302)
request = self.factory.post('', {'email': 'mouse@mouse.com'})
with patch('bookwyrm.emailing.send_email.delay'):
resp = view(request)
self.assertEqual(resp.template_name, 'password_reset_request.html')
self.assertEqual(
models.PasswordReset.objects.get().user, self.local_user)
def test_password_reset(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.PasswordReset.as_view()
code = models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.get('')
request.user = self.anonymous_user
result = view(request, code.code)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'password_reset.html')
self.assertEqual(result.status_code, 200)
def test_password_reset_post(self):
''' reset from code '''
view = views.PasswordReset.as_view()
code = models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hi'
})
with patch('bookwyrm.views.password.login'):
resp = view(request, code.code)
self.assertEqual(resp.status_code, 302)
self.assertFalse(models.PasswordReset.objects.exists())
def test_password_reset_wrong_code(self):
''' reset from code '''
view = views.PasswordReset.as_view()
models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hi'
})
resp = view(request, 'jhgdkfjgdf')
self.assertEqual(resp.template_name, 'password_reset.html')
self.assertTrue(models.PasswordReset.objects.exists())
def test_password_reset_mismatch(self):
''' reset from code '''
view = views.PasswordReset.as_view()
code = models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hihi'
})
resp = view(request, code.code)
self.assertEqual(resp.template_name, 'password_reset.html')
self.assertTrue(models.PasswordReset.objects.exists())
def test_password_change_get(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.ChangePassword.as_view()
request = self.factory.get('')
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'change_password.html')
self.assertEqual(result.status_code, 200)
def test_password_change(self):
''' change password '''
view = views.ChangePassword.as_view()
password_hash = self.local_user.password
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hi'
})
request.user = self.local_user
with patch('bookwyrm.views.password.login'):
view(request)
self.assertNotEqual(self.local_user.password, password_hash)
def test_password_change_mismatch(self):
''' change password '''
view = views.ChangePassword.as_view()
password_hash = self.local_user.password
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hihi'
})
request.user = self.local_user
view(request)
self.assertEqual(self.local_user.password, password_hash)

View file

@ -20,6 +20,9 @@ class UserViews(TestCase):
self.local_user = models.User.objects.create_user(
'mouse@local.com', 'mouse@mouse.mouse', 'password',
local=True, localname='mouse')
self.rat = models.User.objects.create_user(
'rat@local.com', 'rat@rat.rat', 'password',
local=True, localname='rat')
def test_user_page(self):
@ -41,6 +44,18 @@ class UserViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_user_page_blocked(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.User.as_view()
request = self.factory.get('')
request.user = self.local_user
self.rat.blocks.add(self.local_user)
with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'rat')
self.assertEqual(result.status_code, 404)
def test_followers_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Followers.as_view()
@ -60,6 +75,18 @@ class UserViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_followers_page_blocked(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Followers.as_view()
request = self.factory.get('')
request.user = self.local_user
self.rat.blocks.add(self.local_user)
with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'rat')
self.assertEqual(result.status_code, 404)
def test_following_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Following.as_view()
@ -79,6 +106,18 @@ class UserViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_following_page_blocked(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Following.as_view()
request = self.factory.get('')
request.user = self.local_user
self.rat.blocks.add(self.local_user)
with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'rat')
self.assertEqual(result.status_code, 404)
def test_edit_profile_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.EditUser.as_view()

View file

@ -47,7 +47,7 @@ urlpatterns = [
re_path(r'^password-reset/?$', views.PasswordResetRequest.as_view()),
re_path(r'^password-reset/(?P<code>[A-Za-z0-9]+)/?$',
views.PasswordReset.as_view()),
re_path(r'^change-password/?$', views.ChangePassword),
re_path(r'^change-password/?$', views.ChangePassword.as_view()),
# invites
re_path(r'^invite/?$', views.ManageInvites.as_view()),
@ -136,4 +136,8 @@ urlpatterns = [
re_path(r'^unfollow/?$', views.unfollow),
re_path(r'^accept-follow-request/?$', views.accept_follow_request),
re_path(r'^delete-follow-request/?$', views.delete_follow_request),
re_path(r'^block/?$', views.Block.as_view()),
re_path(r'^block/(?P<user_id>\d+)/?$', views.Block.as_view()),
re_path(r'^unblock/(?P<user_id>\d+)/?$', views.unblock),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -1,6 +1,7 @@
''' make sure all our nice views are available '''
from .authentication import Login, Register, Logout
from .author import Author, EditAuthor
from .block import Block, unblock
from .books import Book, EditBook, Editions
from .books import upload_cover, add_description, switch_edition, resolve_book
from .direct_message import DirectMessage

58
bookwyrm/views/block.py Normal file
View file

@ -0,0 +1,58 @@
''' views for actions you can take in the application '''
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import models
from bookwyrm.broadcast import broadcast
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
class Block(View):
''' blocking users '''
def get(self, request):
''' list of blocked users? '''
return TemplateResponse(
request, 'blocks.html', {'title': 'Blocked Users'})
def post(self, request, user_id):
''' block a user '''
to_block = get_object_or_404(models.User, id=user_id)
block = models.UserBlocks.objects.create(
user_subject=request.user, user_object=to_block)
if not to_block.local:
broadcast(
request.user,
block.to_activity(),
privacy='direct',
direct_recipients=[to_block]
)
return redirect('/block')
@require_POST
@login_required
def unblock(request, user_id):
''' undo a block '''
to_unblock = get_object_or_404(models.User, id=user_id)
try:
block = models.UserBlocks.objects.get(
user_subject=request.user,
user_object=to_unblock,
)
except models.UserBlocks.DoesNotExist:
return HttpResponseNotFound()
if not to_unblock.local:
broadcast(
request.user,
block.to_undo_activity(request.user),
privacy='direct',
direct_recipients=[to_unblock]
)
block.delete()
return redirect('/block')

View file

@ -38,11 +38,21 @@ def object_visible_to_user(viewer, obj):
''' is a user authorized to view an object? '''
if not obj:
return False
# viewer can't see it if the object's owner blocked them
if viewer in obj.user.blocks.all():
return False
# you can see your own posts and any public or unlisted posts
if viewer == obj.user or obj.privacy in ['public', 'unlisted']:
return True
# you can see the followers only posts of people you follow
if obj.privacy == 'followers' and \
obj.user.followers.filter(id=viewer.id).first():
return True
# you can see dms you are tagged in
if isinstance(obj, models.Status):
if obj.privacy == 'direct' and \
obj.mention_users.filter(id=viewer.id).first():
@ -61,6 +71,12 @@ def get_activity_feed(
# exclude deleted
queryset = queryset.exclude(deleted=True).order_by('-published_date')
# exclude blocks from both directions
if not user.is_anonymous:
blocked = models.User.objects.filter(id__in=user.blocks.all()).all()
queryset = queryset.exclude(
Q(user__in=blocked) | Q(user__blocks=user))
# you can't see followers only or direct messages if you're not logged in
if user.is_anonymous:
privacy = [p for p in privacy if not p in ['followers', 'direct']]
@ -174,3 +190,9 @@ def handle_reading_status(user, shelf, book, privacy):
status.save()
broadcast(user, status.to_create_activity(user))
def is_blocked(viewer, user):
''' is this viewer blocked by the user? '''
if viewer.is_authenticated and viewer in user.blocks.all():
return True
return False

View file

@ -88,6 +88,14 @@ class PasswordReset(View):
@method_decorator(login_required, name='dispatch')
class ChangePassword(View):
''' change password as logged in user '''
def get(self, request):
''' change password page '''
data = {
'title': 'Change Password',
'user': request.user,
}
return TemplateResponse(request, 'change_password.html', data)
def post(self, request):
''' allow a user to change their password '''
new_password = request.POST.get('password')

View file

@ -18,7 +18,7 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_activity_feed, get_user_from_username, is_api_request
from .helpers import object_visible_to_user
from .helpers import is_blocked, object_visible_to_user
# pylint: disable= no-self-use
@ -31,6 +31,10 @@ class User(View):
except models.User.DoesNotExist:
return HttpResponseNotFound()
# make sure we're not blocked
if is_blocked(request.user, user):
return HttpResponseNotFound()
if is_api_request(request):
# we have a json request
return ActivitypubResponse(user.to_activity())
@ -97,6 +101,10 @@ class Followers(View):
except models.User.DoesNotExist:
return HttpResponseNotFound()
# make sure we're not blocked
if is_blocked(request.user, user):
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(
user.to_followers_activity(**request.GET))
@ -118,6 +126,10 @@ class Following(View):
except models.User.DoesNotExist:
return HttpResponseNotFound()
# make sure we're not blocked
if is_blocked(request.user, user):
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(
user.to_following_activity(**request.GET))
@ -135,14 +147,11 @@ class Following(View):
class EditUser(View):
''' edit user view '''
def get(self, request):
''' profile page for a user '''
user = request.user
form = forms.EditUserForm(instance=request.user)
''' edit profile page for a user '''
data = {
'title': 'Edit profile',
'form': form,
'user': user,
'form': forms.EditUserForm(instance=request.user),
'user': request.user,
}
return TemplateResponse(request, 'edit_user.html', data)