Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-09-06 13:17:46 -07:00
commit 7284a4efad
18 changed files with 243 additions and 30 deletions

View file

@ -362,6 +362,13 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin):
self.collection_queryset, **kwargs self.collection_queryset, **kwargs
).serialize() ).serialize()
def delete(self, *args, broadcast=True, **kwargs):
"""Delete the object"""
activity = self.to_delete_activity(self.user)
super().delete(*args, **kwargs)
if self.user.local and broadcast:
self.broadcast(activity, self.user)
class CollectionItemMixin(ActivitypubMixin): class CollectionItemMixin(ActivitypubMixin):
"""for items that are part of an (Ordered)Collection""" """for items that are part of an (Ordered)Collection"""

View file

@ -1,4 +1,4 @@
{% extends 'lists/list_layout.html' %} {% extends 'lists/layout.html' %}
{% load i18n %} {% load i18n %}
{% block panel %} {% block panel %}

View file

@ -0,0 +1,21 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}{% trans "Delete this list?" %}{% endblock %}
{% block modal-body %}
{% trans "This action cannot be un-done" %}
{% endblock %}
{% block modal-footer %}
<form name="delete-list-{{ list.id }}" action="{% url 'delete-list' list.id %}" method="POST">
{% csrf_token %}
<input type="hidden" name="id" value="{{ list.id }}">
<button class="button is-danger" type="submit">
{% trans "Delete" %}
</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="delete_list" controls_uid=list.id %}
</form>
{% endblock %}

View file

@ -9,4 +9,5 @@
<form name="edit-list" method="post" action="{% url 'list' list.id %}"> <form name="edit-list" method="post" action="{% url 'list' list.id %}">
{% include 'lists/form.html' %} {% include 'lists/form.html' %}
</form> </form>
{% include "lists/delete_list_modal.html" with controls_text="delete_list" controls_uid=list.id %}
{% endblock %} {% endblock %}

View file

@ -3,7 +3,7 @@
<input type="hidden" name="user" value="{{ request.user.id }}"> <input type="hidden" name="user" value="{{ request.user.id }}">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column is-two-thirds">
<div class="field"> <div class="field">
<label class="label" for="id_name">{% trans "Name:" %}</label> <label class="label" for="id_name">{% trans "Name:" %}</label>
{{ list_form.name }} {{ list_form.name }}
@ -34,6 +34,8 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
<div class="columns is-mobile">
<div class="column">
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
{% include 'snippets/privacy_select.html' with current=list.privacy %} {% include 'snippets/privacy_select.html' with current=list.privacy %}
@ -42,4 +44,9 @@
<button type="submit" class="button is-primary">{% trans "Save" %}</button> <button type="submit" class="button is-primary">{% trans "Save" %}</button>
</div> </div>
</div> </div>
</div>
<div class="column is-narrow">
{% trans "Delete list" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class="is-danger" text=button_text icon_with_text="x" controls_text="delete_list" controls_uid=list.id focus="modal_title_delete_list" %}
</div>
</div>

View file

@ -1,4 +1,4 @@
{% extends 'lists/list_layout.html' %} {% extends 'lists/layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load markdown %} {% load markdown %}

View file

@ -2,13 +2,15 @@
{% load i18n %} {% load i18n %}
{% block title %}{{ user.username }}{% endblock %} {% block title %}{{ user.username }}{% endblock %}
{% block header %}{{ user.username }}{% endblock %} {% block header %}
{{ user.username }}
<p class="help has-text-weight-normal">
<a href="{% url 'settings-users' %}">{% trans "Back to users" %}</a>
</p>
{% endblock %}
{% block panel %} {% block panel %}
<div class="block">
<a href="{% url 'settings-users' %}">{% trans "Back to users" %}</a>
</div>
{% include 'user_admin/user_info.html' with user=user %} {% include 'user_admin/user_info.html' with user=user %}
{% include 'user_admin/user_moderation_actions.html' with user=user %} {% include 'user_admin/user_moderation_actions.html' with user=user %}

View file

@ -1,7 +1,7 @@
{% extends 'snippets/filters_panel/filters_panel.html' %} {% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %} {% block filter_fields %}
{% include 'user_admin/server_filter.html' %}
{% include 'user_admin/username_filter.html' %} {% include 'user_admin/username_filter.html' %}
{% include 'directory/community_filter.html' %} {% include 'directory/community_filter.html' %}
{% include 'user_admin/server_filter.html' %}
{% endblock %} {% endblock %}

View file

@ -1,8 +1,10 @@
{% load i18n %} {% load i18n %}
{% load markdown %} {% load markdown %}
{% load humanize %}
<div class="block columns"> <div class="block columns">
<div class="column is-flex is-flex-direction-column"> <div class="column is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "User details" %}</h4> <h4 class="title is-4">{% trans "Profile" %}</h4>
<div class="box is-flex-grow-1"> <div class="box is-flex-grow-1">
{% include 'user/user_preview.html' with user=user %} {% include 'user/user_preview.html' with user=user %}
{% if user.summary %} {% if user.summary %}
@ -14,6 +16,95 @@
<p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p> <p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p>
</div> </div>
</div> </div>
<div class="column is-flex is-flex-direction-column is-4">
<h4 class="title is-4">{% trans "Status" %}</h4>
<div class="box is-flex-grow-1 has-text-weight-bold">
{% if user.is_active %}
<p class="notification is-success">
{% trans "Active" %}
</p>
{% else %}
<p class="notification is-warning">
{% trans "Inactive" %}
{% if user.deactivation_reason %}
({{ user.deactivation_reason }})
{% endif %}
</p>
{% endif %}
<p class="notification">
{% if user.local %}
{% trans "Local" %}
{% else %}
{% trans "Remote" %}
{% endif %}
</p>
</div>
</div>
</div>
<div class="block columns">
<div class="column is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "User details" %}</h4>
<div class="box content is-flex-grow-1">
<dl>
{% if user.local %}
<div class="is-flex">
<dt>{% trans "Email:" %}</dt>
<dd>{{ user.email }}</dd>
</div>
{% endif %}
{% with report_count=user.report_set.count %}
<div class="is-flex">
<dt>{% trans "Reports:" %}</dt>
<dd>
{{ report_count|intcomma }}
{% if report_count > 0 %}
<a href="{% url 'settings-reports' %}?username={{ user.username }}">
{% trans "(View reports)" %}
</a>
{% endif %}
</dd>
</div>
{% endwith %}
<div class="is-flex">
<dt>{% trans "Blocked by count:" %}</dt>
<dd>{{ user.blocked_by.count }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Last active date:" %}</dt>
<dd>{{ user.last_active_date }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Manually approved followers:" %}</dt>
<dd>{{ user.manually_approves_followers }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Discoverable:" %}</dt>
<dd>{{ user.discoverable }}</dd>
</div>
{% if not user.is_active %}
<div class="is-flex">
<dt>{% trans "Deactivation reason:" %}</dt>
<dd>{{ user.deactivation_reason }}</dd>
</div>
{% endif %}
{% if not user.is_active and user.deactivation_reason == "pending" %}
<div class="is-flex">
<dt>{% trans "Confirmation code:" %}</dt>
<dd>{{ user.confirmation_code }}</dd>
</div>
{% endif %}
</dl>
</div>
</div>
{% if not user.local %} {% if not user.local %}
{% with server=user.federated_server %} {% with server=user.federated_server %}
<div class="column is-half is-flex is-flex-direction-column"> <div class="column is-half is-flex is-flex-direction-column">

View file

@ -40,14 +40,6 @@ class InboxActivities(TestCase):
remote_id="https://example.com/status/1", remote_id="https://example.com/status/1",
) )
self.create_json = {
"id": "hi",
"type": "Create",
"actor": "hi",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {},
}
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_delete_status(self): def test_delete_status(self):
@ -137,3 +129,30 @@ class InboxActivities(TestCase):
# nothing happens. # nothing happens.
views.inbox.activity_task(activity) views.inbox.activity_task(activity)
self.assertEqual(models.User.objects.filter(is_active=True).count(), 2) self.assertEqual(models.User.objects.filter(is_active=True).count(), 2)
def test_delete_list(self):
"""delete a list"""
book_list = models.List.objects.create(
name="test list",
user=self.remote_user,
remote_id="https://example.com/list/1",
)
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/test-user#delete",
"type": "Delete",
"actor": "https://example.com/users/test-user",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": {
"id": book_list.remote_id,
"owner": self.remote_user.remote_id,
"type": "BookList",
"totalItems": 0,
"first": "",
"name": "test list",
"to": [],
"cc": [],
},
}
views.inbox.activity_task(activity)
self.assertFalse(models.List.objects.exists())

View file

@ -3,6 +3,7 @@ import json
from unittest.mock import patch from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
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
@ -66,6 +67,44 @@ class ListActionViews(TestCase):
self.anonymous_user.is_authenticated = False self.anonymous_user.is_authenticated = False
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_delete_list(self):
"""delete an entire list"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=True,
order=1,
)
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book_two,
approved=False,
order=2,
)
request = self.factory.post("")
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.delete_list(request, self.list.id)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["id"], self.list.remote_id)
self.assertEqual(activity["object"]["type"], "BookList")
self.assertEqual(mock.call_count, 1)
self.assertFalse(models.List.objects.exists())
self.assertFalse(models.ListItem.objects.exists())
def test_delete_list_permission_denied(self):
"""delete an entire list"""
request = self.factory.post("")
request.user = self.rat
with self.assertRaises(PermissionDenied):
views.delete_list(request, self.list.id)
def test_curate_approve(self): def test_curate_approve(self):
"""approve a pending item""" """approve a pending item"""
view = views.Curate.as_view() view = views.Curate.as_view()

View file

@ -220,6 +220,7 @@ urlpatterns = [
re_path(r"^list/?$", views.Lists.as_view(), name="lists"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"),
re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"), re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"),
re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"), re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"),
re_path(r"^list/delete/(?P<list_id>\d+)/?$", views.delete_list, name="delete-list"),
re_path(r"^list/add-book/?$", views.list.add_book, name="list-add-book"), re_path(r"^list/add-book/?$", views.list.add_book, name="list-add-book"),
re_path( re_path(
r"^list/(?P<list_id>\d+)/remove/?$", r"^list/(?P<list_id>\d+)/remove/?$",

View file

@ -27,6 +27,7 @@ from .isbn import Isbn
from .landing import About, Home, Landing from .landing import About, Home, Landing
from .list import Lists, SavedLists, List, Curate, UserLists from .list import Lists, SavedLists, List, Curate, UserLists
from .list import save_list, unsave_list from .list import save_list, unsave_list
from .list import delete_list
from .notifications import Notifications from .notifications import Notifications
from .outbox import Outbox from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough from .reading import edit_readthrough, create_readthrough

View file

@ -16,6 +16,7 @@ class Discover(View):
def get(self, request): def get(self, request):
"""tiled book activity page""" """tiled book activity page"""
# all activities in the "federated" feed associated with a book
activities = ( activities = (
activitystreams.streams["local"] activitystreams.streams["local"]
.get_activity_stream(request.user) .get_activity_stream(request.user)
@ -29,13 +30,19 @@ class Discover(View):
large_activities = Paginator( large_activities = Paginator(
activities.filter(mention_books__isnull=True) activities.filter(mention_books__isnull=True)
.exclude(content=None, quotation__quote=None) # exclude statuses with no user-provided content for large panels
.exclude(content=""), .exclude(
Q(Q(content="") | Q(content__isnull=True)) & Q(quotation__isnull=True),
),
6, 6,
) )
small_activities = Paginator( small_activities = Paginator(
activities.filter( activities.filter(
Q(mention_books__isnull=False) | Q(content=None) | Q(content="") Q(mention_books__isnull=False)
| Q(
Q(Q(content="") | Q(content__isnull=True))
& Q(quotation__isnull=True),
)
), ),
4, 4,
) )

View file

@ -3,6 +3,7 @@ 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
@ -260,6 +261,20 @@ def unsave_list(request, list_id):
return redirect("list", list_id) return redirect("list", list_id)
@require_POST
@login_required
def delete_list(request, list_id):
"""delete a list"""
book_list = get_object_or_404(models.List, id=list_id)
# only the owner or a moderator can delete a list
if book_list.user != request.user and not request.user.has_perm("moderate_post"):
raise PermissionDenied
book_list.delete()
return redirect("lists")
@require_POST @require_POST
@login_required @login_required
def add_book(request): def add_book(request):

View file

@ -31,8 +31,8 @@ class UserAdminList(View):
if username: if username:
filters["username__icontains"] = username filters["username__icontains"] = username
scope = request.GET.get("scope") scope = request.GET.get("scope")
if scope: if scope and scope == "local":
filters["local"] = scope == "local" filters["local"] = True
users = models.User.objects.filter(**filters) users = models.User.objects.filter(**filters)

View file

@ -24,7 +24,9 @@ server {
# listen 443 ssl http2; # listen 443 ssl http2;
# #
# server_name your-domain.com; # server_name your-domain.com;
#
# client_max_body_size 3M;
#
# if ($host != "you-domain.com") { # if ($host != "you-domain.com") {
# return 301 $scheme://your-domain.com$request_uri; # return 301 $scheme://your-domain.com$request_uri;
# } # }