Skip to content

Commit

Permalink
Permissions for newsletter models and actions (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgax authored Jun 21, 2024
1 parent 1bbe587 commit 30a64d9
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 66 deletions.
18 changes: 18 additions & 0 deletions demo/migrations/0003_alter_articlepage_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-06-20 15:41

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("demo", "0002_articlepage_newsletter_mixin"),
]

operations = [
migrations.AlterModelOptions(
name="articlepage",
options={
"permissions": [("sendnewsletter_articlepage", "Can send newsletter")]
},
),
]
17 changes: 17 additions & 0 deletions demo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from wagtail.admin.panels import FieldPanel
from wagtail.fields import RichTextField
from wagtail.models import Page
from wagtail.permission_policies.base import ModelPermissionPolicy

from wagtail_newsletter.models import NewsletterPageMixin, NewsletterRecipientsBase

Expand All @@ -24,6 +25,22 @@ class ArticlePage(NewsletterPageMixin, Page): # type: ignore

newsletter_template = "demo/article_page_newsletter.html"

class Meta: # type: ignore
permissions = [
("sendnewsletter_articlepage", "Can send newsletter"),
]

def has_newsletter_permission(self, user, action):
permission_policy = ModelPermissionPolicy(type(self))
return permission_policy.user_has_permission(user, "sendnewsletter")

@classmethod
def get_newsletter_panels(cls):
panels = [panel.clone() for panel in super().get_newsletter_panels()]
for panel in panels:
panel.permission = "demo.sendnewsletter_articlepage"
return panels


class CustomRecipients(NewsletterRecipientsBase):
greeting = RichTextField(blank=True)
Expand Down
14 changes: 14 additions & 0 deletions demo/wagtail_hooks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.contrib.auth.models import Permission
from wagtail import hooks

from . import viewsets
Expand All @@ -6,3 +7,16 @@
@hooks.register("register_admin_viewset") # type: ignore
def register_admin_viewset():
return viewsets.custom_recipients_viewset


@hooks.register("register_permissions") # type: ignore
def register_permissions(): # pragma: no cover
return Permission.objects.filter(
content_type__app_label="demo",
codename__in=[
"add_customrecipients",
"change_customrecipients",
"delete_customrecipients",
"sendnewsletter_articlepage",
],
)
39 changes: 39 additions & 0 deletions tests/test_campaign_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,42 @@ def test_send_campaign(
call(campaign_id="", recipients=None, subject=page.title, html=ANY)
]
assert memory_backend.send_campaign.mock_calls == [call(CAMPAIGN_ID)]


@pytest.mark.parametrize(
"action", ["save_campaign", "send_test_email", "send_campaign"]
)
def test_action_restricted(
page: ArticlePage,
admin_client: Client,
memory_backend: MemoryCampaignBackend,
monkeypatch: pytest.MonkeyPatch,
action: str,
):
memory_backend.save_campaign = Mock(return_value=CAMPAIGN_ID)
memory_backend.get_campaign = Mock(return_value=Mock(url=CAMPAIGN_URL))
memory_backend.send_test_email = Mock()
memory_backend.send_campaign = Mock()

monkeypatch.setattr(
ArticlePage, "has_newsletter_permission", Mock(return_value=False)
)

url = reverse("wagtailadmin_pages:edit", kwargs={"page_id": page.pk})
data = {
"title": page.title,
"slug": page.slug,
"newsletter-action": action,
}
response = admin_client.post(url, data, follow=True)

html = response.content.decode()
assert f"Page '{page.title}' has been updated" in html
assert (
"You do not have permission to perform "
f"the newsletter action '{action}'" in html
)

assert memory_backend.save_campaign.mock_calls == []
assert memory_backend.send_test_email.mock_calls == []
assert memory_backend.send_campaign.mock_calls == []
34 changes: 26 additions & 8 deletions tests/test_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,40 @@ def test_warn_backend_error(
assert BACKEND_ERROR_TEXT in response.content.decode()


def test_campaign_report(admin_client: Client, memory_backend: MemoryCampaignBackend):
@pytest.mark.parametrize("has_permission", [True, False])
def test_campaign_report(
admin_client: Client,
memory_backend: MemoryCampaignBackend,
monkeypatch: pytest.MonkeyPatch,
has_permission: bool,
):
monkeypatch.setattr(
ArticlePage, "has_newsletter_permission", Mock(return_value=has_permission)
)
campaign = Mock(status="sent")
memory_backend.get_campaign = Mock(return_value=campaign)
campaign.get_report.return_value = {
report = {
"bounces": 6,
"clicks": 3,
"emails_sent": 13,
"opens": 5,
"send_time": datetime(2024, 6, 17, 12, 51, 46, tzinfo=timezone.utc),
}
campaign.get_report.return_value = report
page = ArticlePage(title="Page title", newsletter_campaign="test-campaign-id")
Site.objects.get().root_page.add_child(instance=page)
url = reverse("wagtailadmin_pages:edit", kwargs={"page_id": page.pk})
html = admin_client.get(url).content.decode()
assert re.search(r"<b>Status:</b>\s*sent", html)
assert re.search(r"<b>Send time:</b>\s*June 17, 2024, 12:51 p\.m\.", html)
assert re.search(r"<b>Emails sent:</b>\s*13 \(6 bounces\)", html)
assert re.search(r"<b>Opens:</b>\s*5", html)
assert re.search(r"<b>Clicks:</b>\s*3", html)
response = admin_client.get(url)
context = dict(response.context)
html = response.content.decode()

if has_permission:
assert re.search(r"<b>Status:</b>\s*sent", html)
assert "report" in context
assert re.search(r"<b>Send time:</b>\s*June 17, 2024, 12:51 p\.m\.", html)
assert re.search(r"<b>Emails sent:</b>\s*13 \(6 bounces\)", html)
assert re.search(r"<b>Opens:</b>\s*5", html)
assert re.search(r"<b>Clicks:</b>\s*3", html)

else:
assert "report" not in context
5 changes: 5 additions & 0 deletions wagtail_newsletter/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.utils.translation import gettext_lazy as _
from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface
from wagtail.models import Page
from wagtail.permissions import ModelPermissionPolicy

from . import audiences, get_recipients_model_string, panels

Expand Down Expand Up @@ -143,6 +144,10 @@ def save_revision(self, *args, **kwargs):
)
return revision

def has_newsletter_permission(self, user, action):
permission_policy = ModelPermissionPolicy(type(self))
return permission_policy.user_has_permission(user, "publish")

newsletter_template: str

def get_newsletter_template(self) -> str:
Expand Down
32 changes: 27 additions & 5 deletions wagtail_newsletter/panels.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

from django.core.exceptions import ImproperlyConfigured
from django.utils.functional import cached_property
from django.utils.html import format_html
from wagtail.admin.panels import Panel

Expand All @@ -18,6 +19,24 @@ class BoundPanel(Panel.BoundPanel):

instance: "models.NewsletterPageMixin"

class Media:
js = [
"wagtail_newsletter/js/wagtail_newsletter.js",
]

@cached_property
def permissions(self):
return frozenset(
action
for action in [
"save_campaign",
"send_test_email",
"send_campaign",
"get_report",
]
if self.instance.has_newsletter_permission(self.request.user, action)
)

def get_context_data(self, parent_context=None):
context = super().get_context_data(parent_context) or {}
backend = campaign_backends.get_backend()
Expand Down Expand Up @@ -59,11 +78,14 @@ def get_context_data(self, parent_context=None):

if campaign is not None and campaign.sent:
context["sent"] = True
context["report"] = campaign.get_report()
if "get_report" in self.permissions:
context["report"] = campaign.get_report()

context["has_action_permission"] = {
permission: True for permission in self.permissions
}

return context

class Media:
js = [
"wagtail_newsletter/js/wagtail_newsletter.js",
]
def is_shown(self): # type: ignore
return bool(self.permissions)
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,40 @@
{{ campaign.status }}
</p>

{% if report.send_time %}
{% if report %}
{% if report.send_time %}
<p>
<b>Send time:</b>
{{ report.send_time }}
({{ report.send_time|timesince }} ago).
</p>
{% endif %}

<p>
<b>Send time:</b>
{{ report.send_time }}
({{ report.send_time|timesince }} ago).
<b>Emails sent:</b>
{{ report.emails_sent }} ({{ report.bounces }} bounces)
</p>
{% endif %}

<p>
<b>Emails sent:</b>
{{ report.emails_sent }} ({{ report.bounces }} bounces)
</p>

<p>
<b>Opens:</b>
{{ report.opens }}
</p>
<p>
<b>Opens:</b>
{{ report.opens }}
</p>

<p>
<b>Clicks:</b>
{{ report.clicks }}
</p>
<p>
<b>Clicks:</b>
{{ report.clicks }}
</p>
{% endif %}
{% endblock %}

{% else %}

<div class="help-block help-info">
{% icon name="help" %}
These actions will save a new page revision.
</div>
{% if has_action_permission %}
<div class="help-block help-info">
{% icon name="help" %}
These actions will save a new page revision.
</div>
{% endif %}

<p>
<input
Expand All @@ -61,39 +65,45 @@
data-wn-panel-target="testAddress"
>

<button
type="submit"
class="button button-secondary button-longrunning"
name="newsletter-action"
value="save_campaign"
data-controller="w-progress"
data-action="w-progress#activate"
>
{% icon name="spinner" %}
Save campaign to {{ backend_name }}
</button>
{% if has_action_permission.save_campaign %}
<button
type="submit"
class="button button-secondary button-longrunning"
name="newsletter-action"
value="save_campaign"
data-controller="w-progress"
data-action="w-progress#activate"
>
{% icon name="spinner" %}
Save campaign to {{ backend_name }}
</button>
{% endif %}

<button
type="button"
class="button button-secondary button-longrunning"
data-a11y-dialog-show="wn-test-dialog"
data-controller="w-progress"
data-wn-panel-target="testButton"
>
{% icon name="spinner" %}
Send test email
</button>
{% if has_action_permission.send_test_email %}
<button
type="button"
class="button button-secondary button-longrunning"
data-a11y-dialog-show="wn-test-dialog"
data-controller="w-progress"
data-wn-panel-target="testButton"
>
{% icon name="spinner" %}
Send test email
</button>
{% endif %}

<button
type="button"
class="button button-primary button-longrunning"
data-controller="w-progress"
data-wn-panel-target="sendButton"
data-action="wn-panel#clickSend"
>
{% icon name="spinner" %}
Send campaign
</button>
{% if has_action_permission.send_campaign %}
<button
type="button"
class="button button-primary button-longrunning"
data-controller="w-progress"
data-wn-panel-target="sendButton"
data-action="wn-panel#clickSend"
>
{% icon name="spinner" %}
Send campaign
</button>
{% endif %}

{% dialog icon_name="mail" id="wn-test-dialog" title="Send test email" %}
<form
Expand Down
Loading

0 comments on commit 30a64d9

Please sign in to comment.