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 By shipping this package in the project repository we can extend behavior
without forking the upstream plugin. 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.decorators.http import require_http_methods
from django.views.generic import TemplateView from django.views.generic import TemplateView
from oscar.core.utils import redirect_to_referrer
from wagtail.models import Locale, Site from wagtail.models import Locale, Site
from oscar.core.utils import redirect_to_referrer
from ocyan.core.fender import config from ocyan.core.fender import config
from ocyan.plugin.contact_form.forms import ContactForm from ocyan.plugin.contact_form.forms import ContactForm
from ocyan.plugin.contact_form.utils import get_from_email, get_to_email 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 forwarded_for.split(",")[0].strip() or None
return request.META.get("REMOTE_ADDR") return request.META.get("REMOTE_ADDR")
def _active_locale(request) -> Locale: def _active_locale(request) -> Locale:
language_code = (getattr(request, "LANGUAGE_CODE", "") or "").split("-")[0] language_code = (getattr(request, "LANGUAGE_CODE", "") or "").split("-")[0]
if language_code: if language_code:
@@ -51,7 +53,11 @@ def post_contact_form(request):
message_obj = ContactMessage.objects.create( message_obj = ContactMessage.objects.create(
site=site, site=site,
locale=locale, 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), ip_address=_client_ip(request),
path=request.path or "", path=request.path or "",
name=str(cleaned.get("name", "")), name=str(cleaned.get("name", "")),
@@ -59,7 +65,11 @@ def post_contact_form(request):
phone_number=str(cleaned.get("phonenumber") or ""), phone_number=str(cleaned.get("phonenumber") or ""),
message=str(cleaned.get("message", "")), 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 = { context = {
"website_url": request.build_absolute_uri(), "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.urls import path
from django.views.decorators.cache import cache_page 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.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 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 .i18n_views import set_language_normalized
from .sitemaps import robots_txt, sitemap_index, sitemap_section 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 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.models import register_snippet
from wagtail.snippets.views.snippets import SnippetViewSet 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 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 @register_snippet
class ContactMessageViewSet(SnippetViewSet): class ContactMessageViewSet(SnippetViewSet):
model = ContactMessage model = ContactMessage
@@ -19,3 +56,7 @@ class ContactMessageViewSet(SnippetViewSet):
list_filter = ("locale", "site") list_filter = ("locale", "site")
search_fields = ("name", "email", "message", "phone_number") search_fields = ("name", "email", "message", "phone_number")
ordering = ("-created_at",) ordering = ("-created_at",)
@property
def permission_policy(self):
return SuperuserOnlyPermissionPolicy(self.model)