Skip to content

Commit

Permalink
Merge pull request #282 from avantifellows/feature/settings-menu-skip…
Browse files Browse the repository at this point in the history
…-configurable

Endpoints for plio's, user's and org's settings
  • Loading branch information
dalmia authored Feb 9, 2022
2 parents 4754f4d + bb5f274 commit c5f9682
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 6 deletions.
18 changes: 18 additions & 0 deletions organizations/migrations/0004_organization_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.1.1 on 2022-01-12 04:57

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("organizations", "0003_organization_api_key"),
]

operations = [
migrations.AddField(
model_name="organization",
name="config",
field=models.JSONField(null=True),
),
]
1 change: 1 addition & 0 deletions organizations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Organization(TenantMixin, SafeDeleteModel):
name = models.CharField(max_length=255)
shortcode = models.SlugField()
api_key = models.CharField(null=True, max_length=20)
config = models.JSONField(null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

Expand Down
10 changes: 9 additions & 1 deletion organizations/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,13 @@ class OrganizationPermission(permissions.BasePermission):
"""

def has_permission(self, request, view):
"""View-level permissions for organization. This determines whether the request can access organization instances or not."""
"""View-level permissions for organization viewset. This determines whether the request can access organization viewset or not."""
return True

def has_object_permission(self, request, view, obj):
"""Object-level permissions for an organization. This determines whether the request can access an organization instance or not."""
if view.action in ["setting"]:
return request.user.is_superuser or request.user.is_org_admin(
organization_id=int(view.kwargs["pk"])
)
return request.user.is_superuser
1 change: 1 addition & 0 deletions organizations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Meta:
"name",
"shortcode",
"api_key",
"config",
"created_at",
"updated_at",
]
Expand Down
132 changes: 129 additions & 3 deletions organizations/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ def test_guest_cannot_list_organization_random_token(self):
response = self.client.get(reverse("organizations-list"))
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_non_superuser_cannot_list_organizations(self):
"""A non-superuser should not be able to list organizations"""
def test_non_superuser_can_list_organizations(self):
"""A non-superuser should be able to list organizations"""
# get organizations
response = self.client.get(reverse("organizations-list"))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_superuser_can_list_organizations(self):
"""A superuser should be able to list organizations"""
Expand Down Expand Up @@ -91,3 +91,129 @@ def test_updating_organization_recreates_user_instance_cache(self):
self.assertEqual(
cache.get(cache_key_name)["organizations"][0]["name"], org_new_name
)

def test_settings_support_only_patch_method(self):
# some dummy settings
dummy_settings = {"setting_name": "setting_value"}

# make the current user a superuser
self.user.is_superuser = True
self.user.save()

# try to list the settings
response = self.client.get(
f"/api/v1/organizations/{self.organization_1.id}/setting/"
)
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

# try a POST request to settings
response = self.client.post(
f"/api/v1/organizations/{self.organization_1.id}/setting/", dummy_settings
)
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

# try a PUT request to settings
response = self.client.put(
f"/api/v1/organizations/{self.organization_1.id}/setting/", dummy_settings
)
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

# try a PATCH request to settings
response = self.client.patch(
f"/api/v1/organizations/{self.organization_1.id}/setting/", dummy_settings
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
Organization.objects.filter(id=self.organization_1.id)
.first()
.config["settings"],
dummy_settings,
)

def test_superuser_can_update_any_org_settings(self):
# some dummy settings
dummy_settings = {"setting_name": "setting_value"}

# turn the current user into a superuser
self.user.is_superuser = True
self.user.save()

# try updating settings of org 1
response = self.client.patch(
f"/api/v1/organizations/{self.organization_1.id}/setting/", dummy_settings
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
Organization.objects.filter(id=self.organization_1.id)
.first()
.config["settings"],
dummy_settings,
)

# try updating settings of org 2
response = self.client.patch(
f"/api/v1/organizations/{self.organization_2.id}/setting/", dummy_settings
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
Organization.objects.filter(id=self.organization_2.id)
.first()
.config["settings"],
dummy_settings,
)

def test_org_admin_can_update_own_org_settings_only(self):
"""Only an admin of an org can update the org's settings"""
from rest_framework.test import APIClient
from users.models import User
from plio.tests import get_new_access_token

# some dummy settings
dummy_settings = {"setting_name": "setting_value"}

# user should NOT be able to update org 1 settings as
# the user is not an org admin
response = self.client.patch(
f"/api/v1/organizations/{self.organization_1.id}/setting/", dummy_settings
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

# create a new super user and a new APIClient that will be attached to the super user
# this is being done so we can use this superuser client to update our user and make them an org admin
superuser_client = APIClient()
superuser = User.objects.create(mobile="+919988776655", is_superuser=True)
superuser_access_token = get_new_access_token(superuser, self.application)
superuser_client.credentials(
HTTP_AUTHORIZATION="Bearer " + superuser_access_token.token
)

# Make the current user org admin for organization 1 (using the created super user above)
superuser_client.post(
reverse("organization-users-list"),
{
"user": self.user.id,
"organization": self.organization_1.id,
"role": self.org_admin_role.id,
},
)

# user should be able to update org 1 settings
response = self.client.patch(
f"/api/v1/organizations/{self.organization_1.id}/setting/", dummy_settings
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
Organization.objects.filter(id=self.organization_1.id)
.first()
.config["settings"],
dummy_settings,
)

# but the user still should NOT be able to update settings for org 2
response = self.client.patch(
f"/api/v1/organizations/{self.organization_2.id}/setting/", dummy_settings
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(
Organization.objects.filter(id=self.organization_2.id).first().config, None
)
15 changes: 15 additions & 0 deletions organizations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from organizations.models import Organization
from organizations.serializers import OrganizationSerializer
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.decorators import action
from organizations.permissions import OrganizationPermission


Expand All @@ -20,3 +22,16 @@ class OrganizationViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, OrganizationPermission]
queryset = Organization.objects.all()
serializer_class = OrganizationSerializer

@action(
detail=True,
permission_classes=[IsAuthenticated, OrganizationPermission],
methods=["patch"],
)
def setting(self, request, pk):
"""Updates an org's settings"""
org = self.get_object()
org.config = org.config if org.config is not None else {}
org.config["settings"] = self.request.data
org.save()
return Response(self.get_serializer(org).data["config"])
89 changes: 89 additions & 0 deletions plio/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ def test_items_sorted_with_time(self):
self.assertEqual(response.data["items"][1]["id"], item_1.id)

def test_retrieving_plio_sets_instance_cache(self):

# verify cache data doesn't exist
cache_key_name = get_cache_key(self.plio_1)
self.assertEqual(len(cache.keys(cache_key_name)), 0)
Expand Down Expand Up @@ -569,6 +570,94 @@ def test_updating_plio_recreates_instance_cache(self):
self.assertEqual(len(cache.keys(cache_key_name)), 1)
self.assertEqual(cache.get(cache_key_name)["name"], new_name)

def test_user_can_update_own_plio_settings(self):
test_settings = {"player": {"configuration": {"skipEnabled": False}}}

# update settings for plio_1
response = self.client.patch(
f"/api/v1/plios/{self.plio_1.uuid}/setting/",
test_settings,
format="json",
)

# 200 OK returned as status
self.assertEqual(response.status_code, status.HTTP_200_OK)
# The plio should contain the new updated settings object
self.assertEqual(
Plio.objects.filter(uuid=self.plio_1.uuid)
.first()
.config["settings"]["player"],
test_settings["player"],
)

def test_user_cannot_update_other_user_plio_settings(self):
test_settings = {"player": {"configuration": {"skipEnabled": False}}}
# user_2 creates a plio
plio = Plio.objects.create(
name="Plio_by_user_2", video=self.video, created_by=self.user_2
)

# user_1 tries updating the settings for the above created plio
response = self.client.patch(
f"/api/v1/plios/{plio.uuid}/setting/",
test_settings,
format="json",
)

# updating other user's plio's settings should be forbidden
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_org_members_can_update_org_plio_settings(self):
test_settings = {"player": {"configuration": {"skipEnabled": False}}}

# add users to organization
OrganizationUser.objects.create(
organization=self.organization, user=self.user, role=self.org_view_role
)

OrganizationUser.objects.create(
organization=self.organization, user=self.user_2, role=self.org_view_role
)

# set db connection to organization schema
connection.set_schema(self.organization.schema_name)

# create video in the org workspace
video_org = Video.objects.create(
title="Video 1", url="https://www.youtube.com/watch?v=vnISjBbrMUM"
)

# create plio within the org workspace by user 2
plio_org = Plio.objects.create(
name="Plio 1", video=video_org, created_by=self.user_2
)

# set organization in request and access token for user 1
self.client.credentials(
HTTP_ORGANIZATION=self.organization.shortcode,
HTTP_AUTHORIZATION="Bearer " + self.access_token.token,
)

# user_1 tries to update the org plio's settings which was created
# by user_2
response = self.client.patch(
f"/api/v1/plios/{plio_org.uuid}/setting/",
test_settings,
format="json",
)

# updating an org plio's settings should be possible by any org member
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
Plio.objects.filter(uuid=plio_org.uuid)
.first()
.config["settings"]["player"],
test_settings["player"],
)

# set db connection back to public (default) schema
connection.set_schema_to_public()

def test_copying_without_specifying_workspace_fails(self):
response = self.client.post(f"/api/v1/plios/{self.plio_1.uuid}/copy/")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
Expand Down
13 changes: 13 additions & 0 deletions plio/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ def perform_create(self, serializer):
def perform_update(self, serializer):
serializer.save(created_by=self.get_object().created_by)

@action(
detail=True,
permission_classes=[IsAuthenticated, PlioPermission],
methods=["patch"],
)
def setting(self, request, uuid):
"""Updates a plio's settings"""
plio = self.get_object()
plio.config = plio.config if plio.config is not None else {}
plio.config["settings"] = self.request.data
plio.save()
return Response(self.get_serializer(plio).data["config"])

@property
def organization_shortcode(self):
return OrganizationTenantMiddleware.get_organization_shortcode(self.request)
Expand Down
9 changes: 9 additions & 0 deletions users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,18 @@ def to_representation(self, instance):
return cached_response

response = super().to_representation(instance)
# add organizations the user is a part of
response["organizations"] = OrganizationSerializer(
instance.organizations, many=True
).data
# for each organization the user is part of, add the user's role in
# that organization
for org in response["organizations"]:
org_user = OrganizationUser.objects.filter(
user=instance, organization_id=org["id"]
).first()
role_name = Role.objects.filter(id=org_user.role.id).first().name
org.update({"role": role_name})

cache.set(cache_key, response) # set a cached version
return response
Expand Down
Loading

0 comments on commit c5f9682

Please sign in to comment.