Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions docs/user/basic-concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -160,30 +160,33 @@ Here's a summary of the default organization roles.
Organization Manager
~~~~~~~~~~~~~~~~~~~~

.. image:: https://github.com/openwisp/openwisp-users/raw/docs/docs/images/org-manager.png
:target: https://github.com/openwisp/openwisp-users/raw/docs/docs/images/org-manager.png
.. image:: https://github.com/openwisp/openwisp-users/raw/docs/docs/images/1.3/org-manager.png
:target: https://github.com/openwisp/openwisp-users/raw/docs/docs/images/1.3/org-manager.png
:alt: Organization Manager

Any user with the "Is admin" flag enabled for a specific organization (as
shown in the screenshot above) is considered by the system a manager of
that organization. Organization managers have the authority to view and
interact with the data belonging to that organization according to their
set of permissions (as defined in :ref:`Permission Groups
<default_permission_groups>`).
Any user with the "Organization manager" flag enabled for a specific
organization (as shown in the screenshot above) is considered by the
system a manager of that organization. Organization managers have the
authority to view and interact with the data belonging to that
organization according to their set of permissions (as defined in
:ref:`Permission Groups <default_permission_groups>`).

In the REST API and internal model field, this flag is represented as
``is_admin``.

To modify this flag, navigate to the "ORGANIZATION USERS" section on the
"Change user" page.

Organization Members (End-Users)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. image:: https://github.com/openwisp/openwisp-users/raw/docs/docs/images/org-member.png
:target: https://github.com/openwisp/openwisp-users/raw/docs/docs/images/org-member.png
.. image:: https://github.com/openwisp/openwisp-users/raw/docs/docs/images/1.3/org-member.png
:target: https://github.com/openwisp/openwisp-users/raw/docs/docs/images/1.3/org-member.png
:alt: Organization Member

Any user with the "Is admin" flag disabled for a specific organization (as
shown in the screenshot above) is considered by the system a regular
end-user of that organization.
Any user with the "Organization manager" flag disabled for a specific
organization (``is_admin=False`` in the REST API and internal model field)
is considered by the system a regular end-user of that organization.

These users are consumers of a service provided by the organization. They
will not be able to see or interact with any object of that organization
Expand Down
16 changes: 16 additions & 0 deletions docs/user/rest-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ Create User
users with their email address flagged as verified. This will also
skip sending the verification link to their email address.

When creating organization memberships, the organization manager flag
is represented internally by the ``is_admin`` field in the
``organization_users`` payload.

Get User Detail
~~~~~~~~~~~~~~~

Expand All @@ -279,13 +283,25 @@ Change User Detail

PUT /api/v1/users/user/{id}/

.. note::

When editing organization memberships, the organization manager flag
is represented internally by the ``is_admin`` field in the
``organization_users`` payload.

Patch User Detail
~~~~~~~~~~~~~~~~~

.. code-block:: text

PATCH /api/v1/users/user/{id}/

.. note::

When patching organization memberships, the organization manager flag
is represented internally by the ``is_admin`` field in the
``organization_users`` payload.

Delete User
~~~~~~~~~~~

Expand Down
2 changes: 2 additions & 0 deletions openwisp_users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ class OrganizationUserInline(admin.StackedInline):
model = OrganizationUser
formset = RequiredInlineFormSet
view_on_site = False
fields = ("organization", "is_admin")
autocomplete_fields = ("organization",)

def get_formset(self, request, obj=None, **kwargs):
Expand Down Expand Up @@ -591,6 +592,7 @@ class OrganizationUserAdmin(
):
view_on_site = False
actions = ["delete_selected_overridden"]
fields = ("user", "organization", "is_admin")
search_fields = ["user__username", "organization__name"]

def get_readonly_fields(self, request, obj=None):
Expand Down
2 changes: 1 addition & 1 deletion openwisp_users/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ class OrganizationUserSerializer(serializers.ModelSerializer):
class Meta:
model = OrganizationUser
fields = (
"is_admin",
"organization",
"is_admin",
)

def to_internal_value(self, data):
Expand Down
1 change: 1 addition & 0 deletions openwisp_users/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ class BaseOrganizationUser(models.Model):
"""

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
is_admin = models.BooleanField(_("Organization manager"), default=False)

class Meta:
abstract = True
Expand Down
20 changes: 20 additions & 0 deletions openwisp_users/migrations/0025_alter_organizationuser_is_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.2.13 on 2026-07-06 16:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("openwisp_users", "0024_apikey"),
]

operations = [
migrations.AlterField(
model_name="organizationuser",
name="is_admin",
field=models.BooleanField(
default=False, verbose_name="Organization manager"
),
),
]
21 changes: 20 additions & 1 deletion openwisp_users/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,25 @@ def test_organization_user_view_on_site(self):
)
self.assertNotContains(response, "viewsitelink")

def test_organization_user_is_admin_label(self):
admin = self._create_admin()
self.client.force_login(admin)
org = self._create_org()
ou = self._create_org_user(organization=org, user=admin)
response = self.client.get(
reverse(f"admin:{self.app_label}_organizationuser_change", args=[ou.pk])
)
self.assertContains(
response,
'<label class="vCheckboxLabel" for="id_is_admin">'
"Organization manager</label>",
)
content = response.content.decode()
self.assertLess(
content.index('class="form-row field-organization"'),
content.index('class="form-row field-is_admin"'),
)

def test_admin_change_user_is_superuser_editable(self):
admin = self._create_admin()
self.client.force_login(admin)
Expand Down Expand Up @@ -712,7 +731,7 @@ def test_operator_change_org_is_admin(self):
self.assertNotContains(
response,
'<input type="checkbox" name="is_admin" id="id_is_admin">'
'<label class="vCheckboxLabel" for="id_is_admin">Is admin'
'<label class="vCheckboxLabel" for="id_is_admin">Organization manager'
"</label>",
)
response = self.client.get(
Expand Down
6 changes: 6 additions & 0 deletions openwisp_users/tests/test_api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from openwisp_utils.tests import AssertNumQueriesSubTestMixin

from ...api.serializers import OrganizationUserSerializer
from ..utils import TestOrganizationMixin

Organization = load_model("openwisp_users", "Organization")
Expand All @@ -30,6 +31,11 @@ def setUp(self):
)
self.client.force_login(user)

def test_organization_user_is_admin_label(self):
serializer = OrganizationUserSerializer()
self.assertEqual(list(serializer.fields), ["organization", "is_admin"])
self.assertEqual(serializer.fields["is_admin"].label, "Organization manager")

# Tests for Organization Model API endpoints
def test_organization_list_api(self):
path = reverse("users:organization_list")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.2.13 on 2026-07-06 16:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("sample_users", "0005_apikey"),
]

operations = [
migrations.AlterField(
model_name="organizationuser",
name="is_admin",
field=models.BooleanField(
default=False, verbose_name="Organization manager"
),
),
]
Loading