Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Endpoints for plio's, user's and org's settings #282

Merged
merged 27 commits into from
Feb 9, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
24b7e73
NEW: endpoints for plio's settings and user's settings
deepansh96 Dec 22, 2021
79c5a40
Merge branch 'master' into feature/settings-menu-skip-configurable
deepansh96 Dec 22, 2021
7aa074e
FIX: pre commit fixes
deepansh96 Dec 22, 2021
c300dc6
plio settings test
deepansh96 Dec 23, 2021
0e3b96a
Test cases fixed
deepansh96 Dec 23, 2021
0533d11
Test cases fixed
deepansh96 Dec 23, 2021
e8fc720
Test cases fixed
deepansh96 Dec 23, 2021
cac68ef
Test cases fixed finally
deepansh96 Dec 23, 2021
8471984
org config and settings endpoint
deepansh96 Jan 14, 2022
3383e28
pre commit errors
deepansh96 Jan 15, 2022
fb56f64
tests fix
deepansh96 Jan 15, 2022
4060687
permissions bug fixed
deepansh96 Jan 21, 2022
c4f6b9c
Merge branch 'master' into feature/settings-menu-skip-configurable
deepansh96 Jan 21, 2022
3a65d78
pre commit errors
deepansh96 Jan 21, 2022
fc6e51a
Tests for org settings
deepansh96 Jan 21, 2022
da410f7
Merge branch 'master' into feature/settings-menu-skip-configurable
dalmia Jan 25, 2022
32c5205
Update organizations/permissions.py
deepansh96 Jan 27, 2022
73446df
Merge branch 'master' into feature/settings-menu-skip-configurable
dalmia Jan 28, 2022
cfc7975
some feedback
deepansh96 Jan 28, 2022
a874df6
review feedback
deepansh96 Jan 28, 2022
f78c2ba
final level of feedback
deepansh96 Jan 28, 2022
228721d
test cases fix
deepansh96 Jan 28, 2022
0a568f0
Update organizations/views.py
deepansh96 Jan 28, 2022
e86fee8
Update users/views.py
deepansh96 Jan 28, 2022
b49b2c2
Update organizations/views.py
deepansh96 Jan 31, 2022
83cf2d1
Update organizations/views.py
deepansh96 Jan 31, 2022
bb5f274
PR feedback
deepansh96 Jan 31, 2022
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
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()
deepansh96 marked this conversation as resolved.
Show resolved Hide resolved
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
)
20 changes: 19 additions & 1 deletion organizations/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from rest_framework import viewsets
from rest_framework import viewsets, status
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,19 @@ 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):
deepansh96 marked this conversation as resolved.
Show resolved Hide resolved
"""Updates an org's settings"""
org = self.get_object()
config = org.config if org.config is not None else {}
config["settings"] = self.request.data
org.config = config
deepansh96 marked this conversation as resolved.
Show resolved Hide resolved
org.save()
return Response(
self.get_serializer(org).data["config"], status=status.HTTP_200_OK
deepansh96 marked this conversation as resolved.
Show resolved Hide resolved
deepansh96 marked this conversation as resolved.
Show resolved Hide resolved
)
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
16 changes: 16 additions & 0 deletions plio/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,22 @@ 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):
deepansh96 marked this conversation as resolved.
Show resolved Hide resolved
"""Updates a plio's settings"""
plio = self.get_object()
config = plio.config if plio.config is not None else {}
config["settings"] = self.request.data
deepansh96 marked this conversation as resolved.
Show resolved Hide resolved
plio.config = config
plio.save()
return Response(
self.get_serializer(plio).data["config"], status=status.HTTP_200_OK
)

@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