diff --git a/contact_form/__init__.py b/contact_form/__init__.py index 6c2afae..31493a2 100644 --- a/contact_form/__init__.py +++ b/contact_form/__init__.py @@ -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. """ - diff --git a/contact_form/views.py b/contact_form/views.py index f941c64..9b9e452 100644 --- a/contact_form/views.py +++ b/contact_form/views.py @@ -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(), diff --git a/mandelstudio/migrations/0005_grant_contactmessage_permissions.py b/mandelstudio/migrations/0005_grant_contactmessage_permissions.py deleted file mode 100644 index ebce4e5..0000000 --- a/mandelstudio/migrations/0005_grant_contactmessage_permissions.py +++ /dev/null @@ -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, - ), - ] - diff --git a/mandelstudio/migrations/0006_grant_contactmessage_permissions_to_staff.py b/mandelstudio/migrations/0006_grant_contactmessage_permissions_to_staff.py deleted file mode 100644 index ea1a559..0000000 --- a/mandelstudio/migrations/0006_grant_contactmessage_permissions_to_staff.py +++ /dev/null @@ -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, - ), - ] - diff --git a/mandelstudio/migrations/0007_remove_contactmessage_permissions_from_editors.py b/mandelstudio/migrations/0007_remove_contactmessage_permissions_from_editors.py deleted file mode 100644 index 45bdb0f..0000000 --- a/mandelstudio/migrations/0007_remove_contactmessage_permissions_from_editors.py +++ /dev/null @@ -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, - ), - ] - diff --git a/mandelstudio/tests/test_contact_form_routing.py b/mandelstudio/tests/test_contact_form_routing.py new file mode 100644 index 0000000..e176239 --- /dev/null +++ b/mandelstudio/tests/test_contact_form_routing.py @@ -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"})) diff --git a/mandelstudio/urls.py b/mandelstudio/urls.py index 0bf4fa8..1891a7d 100644 --- a/mandelstudio/urls.py +++ b/mandelstudio/urls.py @@ -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 diff --git a/mandelstudio/wagtail_hooks.py b/mandelstudio/wagtail_hooks.py index 1033297..b3846f5 100644 --- a/mandelstudio/wagtail_hooks.py +++ b/mandelstudio/wagtail_hooks.py @@ -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)