Skip to content

Commit

Permalink
feat(organization): restrict user management to organization form (#5314
Browse files Browse the repository at this point in the history
)

Limit user management (add/edit) exclusively to the 
organization form in (Django) admin interface
  • Loading branch information
noliveleger authored Dec 3, 2024
1 parent 7acd5aa commit 4040ecc
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 21 deletions.
3 changes: 1 addition & 2 deletions kobo/apps/organizations/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from .organization import OrgAdmin
from .organization_invite import OrgInvitationAdmin
from .organization_owner import OrgOwnerAdmin
from .organization_user import OrgUserAdmin

__all__ = ['OrgAdmin', 'OrgOwnerAdmin', 'OrgInvitationAdmin', 'OrgUserAdmin']
__all__ = ['OrgAdmin', 'OrgOwnerAdmin', 'OrgUserAdmin']
32 changes: 30 additions & 2 deletions kobo/apps/organizations/admin/organization.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.contrib import admin, messages
from django.db.models import Count
from django.urls import reverse
from django.utils.safestring import mark_safe
from organizations.base_admin import BaseOrganizationAdmin

Expand All @@ -9,15 +10,42 @@
from ..tasks import transfer_user_ownership_to_org
from ..utils import revoke_org_asset_perms
from .organization_owner import OwnerInline
from .organization_user import OrgUserInline
from .organization_user import OrgUserInline, max_users_for_edit_mode


@admin.register(Organization)
class OrgAdmin(BaseOrganizationAdmin):
inlines = [OwnerInline, OrgUserInline]
view_on_site = False
readonly_fields = ['id']
fields = ['id', 'name', 'slug', 'is_active', 'mmo_override']
fields = ['id', 'name', 'mmo_override']
search_fields = ['name']

# parent overrides
list_display = ['name']
list_filter = ()
prepopulated_fields = {}

def change_view(self, request, object_id, form_url='', extra_context=None):
organization = self.get_object(request, object_id)
if (
organization
and organization.organization_users.count() > max_users_for_edit_mode()
and request.method == 'GET'
):
link = reverse('admin:organizations_organizationuser_changelist')
message = (
f'Note: Adding/Editing/Removing users is disabled on this page due '
f'to the size of the organization. Please use the Import/Export '
f'feature available in the <a href="{link}">Organization Users</a> '
f'section instead.'
)
self.message_user(
request,
mark_safe(message),
level=messages.WARNING,
)
return super().change_view(request, object_id, form_url, extra_context)

def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
Expand Down
8 changes: 0 additions & 8 deletions kobo/apps/organizations/admin/organization_invite.py

This file was deleted.

29 changes: 22 additions & 7 deletions kobo/apps/organizations/admin/organization_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@
from ..utils import revoke_org_asset_perms


def _max_users_for_edit_mode():
def max_users_for_edit_mode():
"""
This function represents an arbitrary limit
to prevent the form's POST request from exceeding
`settings.DATA_UPLOAD_MAX_NUMBER_FIELDS`.
"""
return settings.DATA_UPLOAD_MAX_NUMBER_FIELDS // 3
return int(settings.DATA_UPLOAD_MAX_NUMBER_FIELDS * 0.4)


class OrgUserInlineFormSet(forms.models.BaseInlineFormSet):
def clean(self):
if self.is_valid():
members = 0
users = []
if len(self.forms) >= _max_users_for_edit_mode():
if len(self.forms) > max_users_for_edit_mode():
return

for form in self.forms:
Expand Down Expand Up @@ -63,9 +63,17 @@ def clean(self):
)


class OrgUserInlineForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.fields['user'].disabled = True


class OrgUserInline(admin.StackedInline):
model = OrganizationUser
formset = OrgUserInlineFormSet
form = OrgUserInlineForm
raw_id_fields = ('user',)
view_on_site = False
extra = 0
Expand All @@ -85,22 +93,22 @@ def get_readonly_fields(self, request, obj=None):
if not obj:
return []

if obj.organization_users.count() >= _max_users_for_edit_mode():
return ['user', 'is_admin']
if obj.organization_users.count() > max_users_for_edit_mode():
return ['is_admin']

return []

def has_add_permission(self, request, obj=None):
if not obj:
return True

return obj.organization_users.count() < _max_users_for_edit_mode()
return obj.organization_users.count() <= max_users_for_edit_mode()

def has_delete_permission(self, request, obj=None):
if not obj:
return True

return obj.organization_users.count() < _max_users_for_edit_mode()
return obj.organization_users.count() <= max_users_for_edit_mode()


class OrgUserResource(resources.ModelResource):
Expand Down Expand Up @@ -156,6 +164,13 @@ class OrgUserAdmin(ImportExportModelAdmin, BaseOrganizationUserAdmin):
search_fields = ('user__username',)
autocomplete_fields = ['user', 'organization']
form = OrgUserAdminForm
view_on_site = False

def has_add_permission(self, request):
return False

def has_change_permission(self, request, obj=None):
return False

def get_search_results(self, request, queryset, search_term):
auto_complete = request.path == '/admin/autocomplete/'
Expand Down
5 changes: 3 additions & 2 deletions kobo/apps/organizations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ class OrganizationType(models.TextChoices):
class Organization(AbstractOrganization):
id = KpiUidField(uid_prefix='org', primary_key=True)
mmo_override = models.BooleanField(
default=False, verbose_name='Multi-members override'
default=False,
verbose_name='Make organization multi-member (necessary for adding users)'
)
website = models.CharField(default='', max_length=255)
organization_type = models.CharField(
Expand Down Expand Up @@ -235,7 +236,7 @@ def owner_user_object(self) -> 'User':
class OrganizationUser(AbstractOrganizationUser):

def __str__(self):
return f'<OrganizationUser #{self.pk}: {self.user.username}>'
return f'{self.user.username} (#{self.pk})'

@property
def active_subscription_statuses(self):
Expand Down

0 comments on commit 4040ecc

Please sign in to comment.