-
-
Notifications
You must be signed in to change notification settings - Fork 184
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4509 from kobotoolbox/feature/org-frontend
Organizations API
- Loading branch information
Showing
7 changed files
with
191 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
kobo/apps/organizations/migrations/0001_squashed_0004_remove_organization_uid.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
# Generated by Django 3.2.15 on 2023-06-22 20:25 | ||
|
||
from django.conf import settings | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import django.utils.timezone | ||
import kpi.fields.kpi_uid | ||
import organizations.fields | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
replaces = [('organizations', '0001_initial'), ('organizations', '0002_alter_organization_id_to_kpiuidfield'), ('organizations', '0003_copy_organization_uid_to_id'), ('organizations', '0004_remove_organization_uid')] | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='Organization', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('name', models.CharField(help_text='The name of the organization', max_length=200)), | ||
('is_active', models.BooleanField(default=True)), | ||
('created', organizations.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), | ||
('modified', organizations.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), | ||
('slug', organizations.fields.SlugField(blank=True, editable=False, help_text='The name in all lowercase, suitable for URL identification', max_length=200, populate_from='name', unique=True)), | ||
('uid', kpi.fields.kpi_uid.KpiUidField(_null=False, uid_prefix='org')), | ||
], | ||
options={ | ||
'verbose_name': 'organization', | ||
'verbose_name_plural': 'organizations', | ||
'ordering': ['name'], | ||
'abstract': False, | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='OrganizationUser', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('created', organizations.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), | ||
('modified', organizations.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), | ||
('is_admin', models.BooleanField(default=False)), | ||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organization_users', to='organizations.organization')), | ||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizations_organizationuser', to=settings.AUTH_USER_MODEL)), | ||
], | ||
options={ | ||
'verbose_name': 'organization user', | ||
'verbose_name_plural': 'organization users', | ||
'ordering': ['organization', 'user'], | ||
'abstract': False, | ||
'unique_together': {('user', 'organization')}, | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='OrganizationOwner', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('created', organizations.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), | ||
('modified', organizations.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), | ||
('organization', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to='organizations.organization')), | ||
('organization_user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='organizations.organizationuser')), | ||
], | ||
options={ | ||
'verbose_name': 'organization owner', | ||
'verbose_name_plural': 'organization owners', | ||
'abstract': False, | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='OrganizationInvitation', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('guid', models.UUIDField(editable=False)), | ||
('invitee_identifier', models.CharField(help_text='The contact identifier for the invitee, email, phone number, social media handle, etc.', max_length=1000)), | ||
('created', organizations.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)), | ||
('modified', organizations.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)), | ||
('invited_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizations_organizationinvitation_sent_invitations', to=settings.AUTH_USER_MODEL)), | ||
('invitee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organizations_organizationinvitation_invitations', to=settings.AUTH_USER_MODEL)), | ||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organization_invites', to='organizations.organization')), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
), | ||
migrations.AddField( | ||
model_name='organization', | ||
name='users', | ||
field=models.ManyToManyField(related_name='organizations_organization', through='organizations.OrganizationUser', to=settings.AUTH_USER_MODEL), | ||
), | ||
migrations.AlterField( | ||
model_name='organization', | ||
name='id', | ||
field=kpi.fields.kpi_uid.KpiUidField(_null=False, primary_key=True, uid_prefix='org'), | ||
), | ||
migrations.RemoveField( | ||
model_name='organization', | ||
name='uid', | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
from rest_framework import permissions | ||
|
||
|
||
class IsOrgAdminOrReadOnly(permissions.BasePermission): | ||
""" | ||
Object-level permission to only allow admin members of an object to edit it. | ||
Assumes the model instance has an `is_admin` attribute. | ||
""" | ||
|
||
def has_object_permission(self, request, view, obj): | ||
# Read permissions are allowed to any request, | ||
# so we'll always allow GET, HEAD or OPTIONS requests. | ||
if request.method in permissions.SAFE_METHODS: | ||
return True | ||
|
||
# Instance must have an attribute named `owner`. | ||
return obj.is_admin(request.user) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,14 @@ | ||
from rest_framework import serializers | ||
|
||
from kobo.apps.organizations.models import Organization | ||
from kobo.apps.organizations.models import Organization, create_organization | ||
|
||
|
||
class OrganizationSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = Organization | ||
fields = ['id', 'name', 'is_active', 'created', 'modified', 'slug'] | ||
read_only_fields = ["id", "slug"] | ||
|
||
def create(self, validated_data): | ||
user = self.context['request'].user | ||
return create_organization(user, validated_data['name']) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,33 @@ | ||
from django.contrib.auth.models import User | ||
from django.db.models import QuerySet | ||
|
||
from organizations.utils import create_organization | ||
from rest_framework import viewsets | ||
from rest_framework.permissions import IsAuthenticated | ||
|
||
from kobo.apps.organizations.models import Organization | ||
from kobo.apps.organizations.serializers import OrganizationSerializer | ||
from .models import Organization, create_organization | ||
from .permissions import IsOrgAdminOrReadOnly | ||
from .serializers import OrganizationSerializer | ||
|
||
|
||
class OrganizationViewSet(viewsets.ReadOnlyModelViewSet): | ||
class OrganizationViewSet(viewsets.ModelViewSet): | ||
""" | ||
todo: create documentation | ||
Organizations are groups of users with assigned permissions and configurations | ||
- Organization admins can manage the organization and it's membership | ||
- Connect to authentication mechanisms and enforce policy | ||
- Create teams and projects under the organization | ||
""" | ||
|
||
queryset = Organization.objects.all() | ||
serializer_class = OrganizationSerializer | ||
lookup_field = 'id' | ||
permission_classes = (IsAuthenticated,) | ||
extra_context = None | ||
permission_classes = (IsAuthenticated, IsOrgAdminOrReadOnly) | ||
|
||
def get_queryset(self) -> QuerySet: | ||
user = self.request.user | ||
queryset = super().get_queryset().filter(users=user) | ||
if not queryset: | ||
if self.action == "list" and not queryset: | ||
# Very inefficient get or create queryset. | ||
# It's temporary and should be removed later. | ||
create_organization(user, f"{user.username}'s organization", model=Organization) | ||
create_organization(user, f"{user.username}'s organization") | ||
queryset = queryset.all() # refresh | ||
return queryset |