Skip to content

Commit

Permalink
Usability changes.
Browse files Browse the repository at this point in the history
New field on `Webmention`: `has_been_read: bool = False`
- New context processor `mentions.context_processors.unread_webmentions`
  adds `unread_webmentions` field to template context.
- Added admin actions for marking as read/unread.

New objects manager for Webmention with some common filters.
  • Loading branch information
beatonma committed Feb 9, 2024
1 parent d15f644 commit 3d80e5d
Show file tree
Hide file tree
Showing 15 changed files with 298 additions and 123 deletions.
25 changes: 19 additions & 6 deletions mentions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
SimpleMention,
Webmention,
)
from mentions.models.managers.webmention import WebmentionQuerySet

RETRYABLEMIXIN_FIELDS = [
"is_awaiting_retry",
Expand All @@ -20,13 +21,23 @@


@admin.action(permissions=["change"])
def approve_webmention(modeladmin, request, queryset):
queryset.update(approved=True)
def mark_webmention_approved(modeladmin, request, queryset: WebmentionQuerySet):
queryset.mark_as_approved()


@admin.action(permissions=["change"])
def disapprove_webmention(modeladmin, request, queryset):
queryset.update(approved=False)
def mark_webmention_unapproved(modeladmin, request, queryset: WebmentionQuerySet):
queryset.mark_as_unapproved()


@admin.action(permissions=["change"])
def mark_webmention_read(modeladmin, request, queryset: WebmentionQuerySet):
queryset.mark_as_read()


@admin.action(permissions=["change"])
def mark_webmention_unread(modeladmin, request, queryset: WebmentionQuerySet):
queryset.mark_as_unread()


class BaseAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -89,8 +100,10 @@ def get_hcard_name(self, obj):
class WebmentionAdmin(ClickableUrlMixin, QuotableAdmin):
form = TextAreaForm
actions = [
approve_webmention,
disapprove_webmention,
mark_webmention_approved,
mark_webmention_unapproved,
mark_webmention_read,
mark_webmention_unread,
]
list_display = [
"source_url",
Expand Down
11 changes: 11 additions & 0 deletions mentions/context_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from mentions import contract
from mentions.models import Webmention


def unread_webmentions(request) -> dict:
if not request.user.has_perm("mentions.change_webmention"):
return {}

return {
contract.CONTEXT_UNREAD_WEBMENTIONS: Webmention.objects.filter_unread(),
}
4 changes: 4 additions & 0 deletions mentions/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
URLPATTERNS_MODEL_NAME = "model_name"
URLPATTERNS_MODEL_FILTERS = "model_filters"
URLPATTERNS_MODEL_FILTER_MAP = "model_filter_map"


# Context processor key.
CONTEXT_UNREAD_WEBMENTIONS = "unread_webmentions"
18 changes: 18 additions & 0 deletions mentions/migrations/0013_webmention_has_been_read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.0.8 on 2023-05-07 16:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('mentions', '0012_alter_hcard_options_and_more'),
]

operations = [
migrations.AddField(
model_name='webmention',
name='has_been_read',
field=models.BooleanField(default=False, verbose_name='Read'),
),
]
Empty file.
32 changes: 32 additions & 0 deletions mentions/models/managers/webmention.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import cast

from django.db.models import QuerySet


class WebmentionQuerySet(QuerySet):
def filter(self, *args, **kwargs) -> "WebmentionQuerySet":
return cast(WebmentionQuerySet, super().filter(*args, **kwargs))

def filter_unread(self) -> "WebmentionQuerySet":
return self.filter(has_been_read=False)

def mark_as_read(self) -> int:
return self.update(has_been_read=True)

def mark_as_unread(self) -> int:
return self.update(has_been_read=False)

def filter_approved(self) -> "WebmentionQuerySet":
return self.filter(approved=True)

def mark_as_approved(self) -> int:
return self.update(approved=True)

def mark_as_unapproved(self) -> int:
return self.update(approved=False)

def filter_validated(self) -> "WebmentionQuerySet":
return self.filter(validated=True)

def filter_public(self) -> "WebmentionQuerySet":
return self.filter_approved().filter_validated()
7 changes: 7 additions & 0 deletions mentions/models/webmention.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from mentions import options
from mentions.models.base import MentionsBaseModel
from mentions.models.managers.webmention import WebmentionQuerySet
from mentions.models.mixins import QuotableMixin

__all__ = [
Expand All @@ -17,6 +18,8 @@ def _approve_default():
class Webmention(QuotableMixin, MentionsBaseModel):
"""An incoming webmention that is received by your server."""

objects = WebmentionQuerySet.as_manager()

sent_by = models.URLField(
_("sent by"),
blank=True,
Expand All @@ -36,6 +39,10 @@ class Webmention(QuotableMixin, MentionsBaseModel):
"confirmed to exist, and source really does link to target."
),
)
has_been_read = models.BooleanField(
_("Read"),
default=False,
)

notes = models.CharField(
_("notes"),
Expand Down
7 changes: 7 additions & 0 deletions mentions/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from mentions.apps import MentionsConfig

__all__ = [
"can_change_webmention",
"can_view_dashboard",
]

Expand Down Expand Up @@ -39,3 +40,9 @@ def __str__(self):
"view_webmention_dashboard",
_("Can view the webmention dashboard/status page."),
)


# The following permissions are created automatically by Django.
can_change_webmention = MentionsPermission(
"change_webmention", _("Default 'change' permission for Webmention model.")
)
1 change: 1 addition & 0 deletions tests/config/settings/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def _any_str() -> str:
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"mentions.context_processors.unread_webmentions",
],
},
},
Expand Down
142 changes: 142 additions & 0 deletions tests/tests/test_permissions/test_admin_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from django.contrib import admin
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
from django.contrib.auth.models import User
from django.test.utils import override_settings
from django.urls import path, reverse

from mentions import permissions
from mentions.models import Webmention
from tests.config.urls import core_urlpatterns
from tests.tests.util import testfunc
from tests.tests.util.testcase import WebmentionTestCase

urlpatterns = [
*core_urlpatterns,
path("test-admin/", admin.site.urls),
]


@override_settings(ROOT_URLCONF=__name__)
class AdminActionTests(WebmentionTestCase):
"""Tests for Webmention admin actions `approve_webmention` and `disapprove_webmention`."""

def setUp(self) -> None:
super().setUp()

user_allowed = User.objects.create_user("allowed", is_staff=True)
permissions.can_change_webmention.grant(user_allowed)

User.objects.create_user("not_allowed", is_staff=True)

def post_action(self, action: str, *pks):
return self.client.post(
reverse("admin:mentions_webmention_changelist"),
{
"action": action,
ACTION_CHECKBOX_NAME: [str(pk) for pk in pks],
},
)


class ApprovalActionTests(AdminActionTests):
def setUp(self) -> None:
super().setUp()
self.pks = [
x.pk
for x in [
testfunc.create_webmention(approved=True, quote="approved"),
testfunc.create_webmention(approved=False, quote="not approved"),
]
]

action_approve = "mark_webmention_approved"
action_unapprove = "mark_webmention_unapproved"

def _assert(self, username: str, action, should_be_approved: int):
self.client.force_login(User.objects.get(username=username))
self.post_action(action, *self.pks)

self.assertEqual(
Webmention.objects.filter(approved=True).count(),
should_be_approved,
)

def test_action_approve_webmention_with_permission(self):
self._assert(
username="allowed",
action=self.action_approve,
should_be_approved=2,
)

def test_action_approve_webmention_without_permission(self):
self._assert(
username="not_allowed",
action=self.action_approve,
should_be_approved=1,
)

def test_action_disapprove_webmention_with_permission(self):
self._assert(
username="allowed",
action=self.action_unapprove,
should_be_approved=0,
)

def test_action_disapprove_webmention_without_permission(self):
self._assert(
username="not_allowed",
action=self.action_unapprove,
should_be_approved=1,
)


class UnreadActionTests(AdminActionTests):
def setUp(self) -> None:
super().setUp()
self.pks = [
x.pk
for x in [
testfunc.create_webmention(has_been_read=True, quote="read"),
testfunc.create_webmention(has_been_read=False, quote="unread"),
]
]

action_read = "mark_webmention_read"
action_unread = "mark_webmention_unread"

def _assert(self, username: str, action, should_be_approved: int):
self.client.force_login(User.objects.get(username=username))
self.post_action(action, *self.pks)

self.assertEqual(
Webmention.objects.filter(has_been_read=True).count(),
should_be_approved,
)

def test_action_read_with_permission(self):
self._assert(
username="allowed",
action=self.action_read,
should_be_approved=2,
)

def test_action_read_without_permission(self):
self._assert(
username="not_allowed",
action=self.action_read,
should_be_approved=1,
)

def test_action_disapprove_with_permission(self):
self._assert(
username="allowed",
action=self.action_unread,
should_be_approved=0,
)

def test_action_disapprove_without_permission(self):
self._assert(
username="not_allowed",
action=self.action_unread,
should_be_approved=1,
)
Loading

0 comments on commit 3d80e5d

Please sign in to comment.