Use project contact form handler and superuser-only snippet access

This commit is contained in:
2026-05-10 11:00:18 +02:00
parent c6965c422b
commit 2e81970427
8 changed files with 112 additions and 127 deletions

View File

@@ -4,4 +4,3 @@ Ocyan loads contact form handlers via module labels like `contact_form.views`.
By shipping this package in the project repository we can extend behavior
without forking the upstream plugin.
"""

View File

@@ -12,9 +12,10 @@ from django.utils.translation import gettext as _
from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView
from oscar.core.utils import redirect_to_referrer
from wagtail.models import Locale, Site
from oscar.core.utils import redirect_to_referrer
from ocyan.core.fender import config
from ocyan.plugin.contact_form.forms import ContactForm
from ocyan.plugin.contact_form.utils import get_from_email, get_to_email
@@ -30,6 +31,7 @@ def _client_ip(request) -> str | None:
return forwarded_for.split(",")[0].strip() or None
return request.META.get("REMOTE_ADDR")
def _active_locale(request) -> Locale:
language_code = (getattr(request, "LANGUAGE_CODE", "") or "").split("-")[0]
if language_code:
@@ -51,7 +53,11 @@ def post_contact_form(request):
message_obj = ContactMessage.objects.create(
site=site,
locale=locale,
user=request.user if getattr(request.user, "is_authenticated", False) else None,
user=(
request.user
if getattr(request.user, "is_authenticated", False)
else None
),
ip_address=_client_ip(request),
path=request.path or "",
name=str(cleaned.get("name", "")),
@@ -59,7 +65,11 @@ def post_contact_form(request):
phone_number=str(cleaned.get("phonenumber") or ""),
message=str(cleaned.get("message", "")),
)
logger.info("Saved ContactMessage id=%s email=%s", message_obj.id, message_obj.email)
logger.info(
"Saved ContactMessage id=%s email=%s",
message_obj.id,
message_obj.email,
)
context = {
"website_url": request.build_absolute_uri(),

View File

@@ -1,40 +0,0 @@
from django.db import migrations
def grant_contactmessage_permissions(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Permission = apps.get_model("auth", "Permission")
ContentType = apps.get_model("contenttypes", "ContentType")
try:
group = Group.objects.get(name="Editors")
except Group.DoesNotExist:
return
content_type = ContentType.objects.get(app_label="mandelstudio", model="contactmessage")
perms = Permission.objects.filter(
content_type=content_type,
codename__in=[
"add_contactmessage",
"change_contactmessage",
"delete_contactmessage",
"view_contactmessage",
],
)
group.permissions.add(*perms)
class Migration(migrations.Migration):
dependencies = [
("mandelstudio", "0004_contact_messages"),
("contenttypes", "0002_remove_content_type_name"),
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.RunPython(
grant_contactmessage_permissions,
reverse_code=migrations.RunPython.noop,
),
]

View File

@@ -1,43 +0,0 @@
from django.db import migrations
def grant_contactmessage_permissions_to_staff(apps, schema_editor):
Permission = apps.get_model("auth", "Permission")
ContentType = apps.get_model("contenttypes", "ContentType")
# Default Django user model in this project.
User = apps.get_model("auth", "User")
content_type = ContentType.objects.get(app_label="mandelstudio", model="contactmessage")
perms = list(
Permission.objects.filter(
content_type=content_type,
codename__in=[
"add_contactmessage",
"change_contactmessage",
"delete_contactmessage",
"view_contactmessage",
],
)
)
if not perms:
return
for user in User.objects.filter(is_staff=True, is_active=True).iterator():
user.user_permissions.add(*perms)
class Migration(migrations.Migration):
dependencies = [
("mandelstudio", "0005_grant_contactmessage_permissions"),
("contenttypes", "0002_remove_content_type_name"),
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.RunPython(
grant_contactmessage_permissions_to_staff,
reverse_code=migrations.RunPython.noop,
),
]

View File

@@ -1,40 +0,0 @@
from django.db import migrations
def remove_contactmessage_permissions_from_editors(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Permission = apps.get_model("auth", "Permission")
ContentType = apps.get_model("contenttypes", "ContentType")
try:
group = Group.objects.get(name="Editors")
except Group.DoesNotExist:
return
content_type = ContentType.objects.get(app_label="mandelstudio", model="contactmessage")
perms = Permission.objects.filter(
content_type=content_type,
codename__in=[
"add_contactmessage",
"change_contactmessage",
"delete_contactmessage",
"view_contactmessage",
],
)
group.permissions.remove(*perms)
class Migration(migrations.Migration):
dependencies = [
("mandelstudio", "0006_grant_contactmessage_permissions_to_staff"),
("contenttypes", "0002_remove_content_type_name"),
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.RunPython(
remove_contactmessage_permissions_from_editors,
reverse_code=migrations.RunPython.noop,
),
]

View File

@@ -0,0 +1,37 @@
from django.contrib.auth import get_user_model
from django.test import SimpleTestCase, TestCase
from django.urls import resolve
from ocyan.plugin.contact_form.entrypoint import SHOP_BASE_URL
from contact_form.views import post_contact_form
from mandelstudio.models import ContactMessage
from mandelstudio.wagtail_hooks import SuperuserOnlyPermissionPolicy
class ContactFormRoutingTests(SimpleTestCase):
def test_shop_contact_form_uses_project_handler(self):
match = resolve(f"/{SHOP_BASE_URL}/contact-form/")
self.assertIs(match.func, post_contact_form)
class ContactMessagePermissionPolicyTests(TestCase):
def test_only_superusers_have_contact_message_permissions(self):
user_model = get_user_model()
superuser = user_model.objects.create_superuser(
"contact-superuser",
"superuser@example.com",
"password",
)
staff_user = user_model.objects.create_user(
"contact-staff",
"staff@example.com",
"password",
is_staff=True,
)
policy = SuperuserOnlyPermissionPolicy(ContactMessage)
self.assertTrue(policy.user_has_permission(superuser, "view"))
self.assertFalse(policy.user_has_permission(staff_user, "view"))
self.assertFalse(policy.user_has_any_permission(staff_user, {"view", "change"}))

View File

@@ -1,9 +1,14 @@
from django.conf.urls.i18n import i18n_patterns
from django.urls import path
from django.views.decorators.cache import cache_page
from ocyan.core.fender import config
from ocyan.main.urls import urlpatterns as ocyan_urlpatterns
from ocyan.plugin.contact_form.entrypoint import SHOP_BASE_URL
from ocyan.plugin.wagtail_oscar_integration.constants import CACHE_DURATION
from contact_form.views import post_contact_form
from .i18n_views import set_language_normalized
from .sitemaps import robots_txt, sitemap_index, sitemap_section
@@ -22,4 +27,20 @@ urlpatterns = [
),
]
contact_form_urlpatterns = [
path(
f"{SHOP_BASE_URL}/contact-form/",
post_contact_form,
name="project-contact-form-handler",
),
]
if config.i18n_enabled:
urlpatterns += i18n_patterns(
*contact_form_urlpatterns,
prefix_default_language=False,
)
else:
urlpatterns += contact_form_urlpatterns
urlpatterns += ocyan_urlpatterns

View File

@@ -1,3 +1,6 @@
from django.contrib.auth import get_user_model
from wagtail.permissions import ModelPermissionPolicy
from wagtail.snippets.models import register_snippet
from wagtail.snippets.views.snippets import SnippetViewSet
@@ -5,6 +8,40 @@ from mandelblog_content_guard.hooks import * # noqa: F401,F403
from mandelstudio.models import ContactMessage
class SuperuserOnlyPermissionPolicy(ModelPermissionPolicy):
def user_has_permission(self, user, action):
return user.is_active and user.is_superuser
def user_has_any_permission(self, user, actions):
return user.is_active and user.is_superuser
def user_has_permission_for_instance(self, user, action, instance):
return self.user_has_permission(user, action)
def user_has_any_permission_for_instance(self, user, actions, instance):
return self.user_has_any_permission(user, actions)
def instances_user_has_any_permission_for(self, user, actions):
if self.user_has_any_permission(user, actions):
return self.model._default_manager.all()
return self.model._default_manager.none()
def instances_user_has_permission_for(self, user, action):
return self.instances_user_has_any_permission_for(user, [action])
def users_with_any_permission(self, actions):
return get_user_model().objects.filter(is_active=True, is_superuser=True)
def users_with_permission(self, action):
return self.users_with_any_permission([action])
def users_with_any_permission_for_instance(self, actions, instance):
return self.users_with_any_permission(actions)
def users_with_permission_for_instance(self, action, instance):
return self.users_with_any_permission_for_instance([action], instance)
@register_snippet
class ContactMessageViewSet(SnippetViewSet):
model = ContactMessage
@@ -19,3 +56,7 @@ class ContactMessageViewSet(SnippetViewSet):
list_filter = ("locale", "site")
search_fields = ("name", "email", "message", "phone_number")
ordering = ("-created_at",)
@property
def permission_policy(self):
return SuperuserOnlyPermissionPolicy(self.model)