diff --git a/src/servala/frontend/templates/frontend/forms/errors.html b/src/servala/frontend/templates/frontend/forms/errors.html
index 964687d..1d31737 100644
--- a/src/servala/frontend/templates/frontend/forms/errors.html
+++ b/src/servala/frontend/templates/frontend/forms/errors.html
@@ -11,7 +11,7 @@
{{ form.non_field_errors.0 }}
{% endif %}
{% else %}
- {% translate "We could not save your changes." %}
+ {% translate "Please review and correct the errors highlighted in the form below." %}
{% endif %}
diff --git a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html b/src/servala/frontend/templates/frontend/organizations/service_instance_create.html
similarity index 99%
rename from src/servala/frontend/templates/frontend/organizations/service_offering_detail.html
rename to src/servala/frontend/templates/frontend/organizations/service_instance_create.html
index 39b69a8..8743abf 100644
--- a/src/servala/frontend/templates/frontend/organizations/service_offering_detail.html
+++ b/src/servala/frontend/templates/frontend/organizations/service_instance_create.html
@@ -26,7 +26,7 @@
{% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
{% else %}
- {% include "includes/tabbed_fieldset_form.html" with form=custom_service_form expert_form=service_form %}
+ {% include "includes/tabbed_fieldset_form.html" with form=custom_service_form expert_form=service_form hide_form_errors=True %}
{% endif %}
diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html
index 021be3c..c528474 100644
--- a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html
+++ b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html
@@ -22,7 +22,7 @@
{% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
{% else %}
- {% include "includes/tabbed_fieldset_form.html" with form=custom_form expert_form=form %}
+ {% include "includes/tabbed_fieldset_form.html" with form=custom_form expert_form=form hide_form_errors=True %}
{% endif %}
diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html
index f54b1ae..c72976a 100644
--- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html
+++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html
@@ -1,7 +1,9 @@
{% load i18n %}
{% load get_field %}
{% load static %}
-{% include "frontend/forms/errors.html" %}
+{% if not hide_form_errors %}
+ {% include "frontend/forms/errors.html" %}
+{% endif %}
{% if form and expert_form and not hide_expert_mode %}
/offering//",
- views.ServiceOfferingDetailView.as_view(),
- name="organization.offering",
+ views.ServiceInstanceCreateView.as_view(),
+ name="organization.instance.create",
),
path(
"",
diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py
index 33b0560..3307754 100644
--- a/src/servala/frontend/views/__init__.py
+++ b/src/servala/frontend/views/__init__.py
@@ -16,12 +16,12 @@ from .organization import (
)
from .service import (
ServiceDetailView,
+ ServiceInstanceCreateView,
ServiceInstanceDeleteView,
ServiceInstanceDetailView,
ServiceInstanceListView,
ServiceInstanceUpdateView,
ServiceListView,
- ServiceOfferingDetailView,
)
from .support import SupportView
@@ -35,12 +35,12 @@ __all__ = [
"OrganizationSelectionView",
"OrganizationUpdateView",
"ServiceDetailView",
+ "ServiceInstanceCreateView",
"ServiceInstanceDeleteView",
"ServiceInstanceDetailView",
"ServiceInstanceListView",
"ServiceInstanceUpdateView",
"ServiceListView",
- "ServiceOfferingDetailView",
"ProfileView",
"SupportView",
"custom_404",
diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py
index b9f7d56..66e9f75 100644
--- a/src/servala/frontend/views/service.py
+++ b/src/servala/frontend/views/service.py
@@ -83,7 +83,7 @@ class ServiceDetailView(OrganizationViewMixin, DetailView):
if self.visible_offerings.count() == 1:
offering = self.visible_offerings.first()
return redirect(
- "frontend:organization.offering",
+ "frontend:organization.instance.create",
organization=self.request.organization.slug,
slug=self.object.slug,
pk=offering.pk,
@@ -97,8 +97,8 @@ class ServiceDetailView(OrganizationViewMixin, DetailView):
return context
-class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView):
- template_name = "frontend/organizations/service_offering_detail.html"
+class ServiceInstanceCreateView(OrganizationViewMixin, HtmxViewMixin, DetailView):
+ template_name = "frontend/organizations/service_instance_create.html"
context_object_name = "offering"
model = ServiceOffering
permission_type = "view"
diff --git a/src/tests/conftest.py b/src/tests/conftest.py
index 859b4ca..d80aed8 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -42,13 +42,36 @@ def other_organization(origin):
return Organization.objects.create(name="Test Org Alternate", origin=origin)
+@pytest.fixture
+def user():
+ return User.objects.create(email="testuser@example.org", password="test")
+
+
@pytest.fixture
def org_owner(organization):
- user = User.objects.create(email="user@example.org", password="example")
+ owner = User.objects.create(email="owner@example.org", password="example")
OrganizationMembership.objects.create(
- organization=organization, user=user, role="owner"
+ organization=organization, user=owner, role="owner"
)
- return user
+ return owner
+
+
+@pytest.fixture
+def org_admin(organization):
+ admin = User.objects.create(email="admin@example.org", password="example")
+ OrganizationMembership.objects.create(
+ organization=organization, user=admin, role="admin"
+ )
+ return admin
+
+
+@pytest.fixture
+def org_member(organization):
+ member = User.objects.create(email="member@example.org", password="example")
+ OrganizationMembership.objects.create(
+ organization=organization, user=member, role="member"
+ )
+ return member
@pytest.fixture
diff --git a/src/tests/test_rules.py b/src/tests/test_rules.py
new file mode 100644
index 0000000..9a83163
--- /dev/null
+++ b/src/tests/test_rules.py
@@ -0,0 +1,151 @@
+import pytest
+from django_scopes import scope
+
+from servala.core.models.organization import OrganizationRole
+from servala.core.rules import (
+ has_organization_role,
+ is_organization_admin,
+ is_organization_member,
+ is_organization_owner,
+)
+
+pytestmark = pytest.mark.django_db
+
+
+def test_has_organization_role_returns_false_for_non_organization_object(user):
+ assert has_organization_role(user, "not an organization", None) is False
+
+
+def test_has_organization_role_returns_false_for_non_member(user, organization):
+ with scope(organization=organization):
+ assert has_organization_role(user, organization, None) is False
+
+
+@pytest.mark.parametrize("user_fixture", ["org_owner", "org_admin", "org_member"])
+def test_has_organization_role_returns_true_for_any_member(
+ user_fixture, organization, request
+):
+ user = request.getfixturevalue(user_fixture)
+ with scope(organization=organization):
+ assert has_organization_role(user, organization, None) is True
+
+
+@pytest.mark.parametrize(
+ "user_fixture,roles,expected",
+ [
+ ("org_owner", [OrganizationRole.OWNER], True),
+ ("org_owner", [OrganizationRole.ADMIN], False),
+ ("org_owner", [OrganizationRole.MEMBER], False),
+ ("org_owner", [OrganizationRole.OWNER, OrganizationRole.ADMIN], True),
+ ("org_admin", [OrganizationRole.ADMIN], True),
+ ("org_admin", [OrganizationRole.OWNER], False),
+ ("org_admin", [OrganizationRole.OWNER, OrganizationRole.ADMIN], True),
+ ("org_member", [OrganizationRole.MEMBER], True),
+ ("org_member", [OrganizationRole.OWNER], False),
+ ("org_member", [OrganizationRole.ADMIN], False),
+ ],
+)
+def test_has_organization_role_filters_by_roles(
+ user_fixture, roles, expected, organization, request
+):
+ user = request.getfixturevalue(user_fixture)
+ with scope(organization=organization):
+ assert has_organization_role(user, organization, roles) is expected
+
+
+@pytest.mark.parametrize(
+ "user_fixture,expected",
+ [
+ ("org_owner", True),
+ ("org_admin", False),
+ ("org_member", False),
+ ],
+)
+def test_is_organization_owner(user_fixture, expected, organization, request):
+ user = request.getfixturevalue(user_fixture)
+ with scope(organization=organization):
+ assert is_organization_owner(user, organization) is expected
+
+
+@pytest.mark.parametrize(
+ "user_fixture,expected",
+ [
+ ("org_owner", True),
+ ("org_admin", False),
+ ("org_member", False),
+ ],
+)
+def test_is_organization_owner_with_related_object(
+ user_fixture, expected, organization, mocker, request
+):
+ user = request.getfixturevalue(user_fixture)
+ obj = mocker.MagicMock()
+ obj.organization = organization
+ with scope(organization=organization):
+ assert is_organization_owner(user, obj) is expected
+
+
+@pytest.mark.parametrize(
+ "user_fixture,expected",
+ [
+ ("org_owner", True),
+ ("org_admin", True),
+ ("org_member", False),
+ ],
+)
+def test_is_organization_admin(user_fixture, expected, organization, request):
+ user = request.getfixturevalue(user_fixture)
+ with scope(organization=organization):
+ assert is_organization_admin(user, organization) is expected
+
+
+@pytest.mark.parametrize(
+ "user_fixture,expected",
+ [
+ ("org_owner", True),
+ ("org_admin", True),
+ ("org_member", False),
+ ],
+)
+def test_is_organization_admin_with_related_object(
+ user_fixture, expected, organization, mocker, request
+):
+ user = request.getfixturevalue(user_fixture)
+ obj = mocker.MagicMock()
+ obj.organization = organization
+ with scope(organization=organization):
+ assert is_organization_admin(user, obj) is expected
+
+
+@pytest.mark.parametrize("user_fixture", ["org_owner", "org_admin", "org_member"])
+def test_is_organization_member_returns_true_for_members(
+ user_fixture, organization, request
+):
+ user = request.getfixturevalue(user_fixture)
+ with scope(organization=organization):
+ assert is_organization_member(user, organization) is True
+
+
+def test_is_organization_member_returns_false_for_non_member(user, organization):
+ with scope(organization=organization):
+ assert is_organization_member(user, organization) is False
+
+
+@pytest.mark.parametrize("user_fixture", ["org_owner", "org_admin", "org_member"])
+def test_is_organization_member_with_related_object(
+ user_fixture, organization, mocker, request
+):
+ user = request.getfixturevalue(user_fixture)
+ obj = mocker.MagicMock()
+ obj.organization = organization
+ with scope(organization=organization):
+ assert is_organization_member(user, obj) is True
+
+
+def test_is_organization_member_with_related_object_returns_false_for_non_member(
+ user, organization, mocker
+):
+ obj = mocker.MagicMock()
+ obj.organization = organization
+ with scope(organization=organization):
+ assert is_organization_member(user, obj) is False