Skip to content

Commit

Permalink
Merge pull request #88 from edx/ndalfonso/DISCO-1414-org-source-of-truth
Browse files Browse the repository at this point in the history
Ndalfonso/disco 1414 org source of truth
  • Loading branch information
xnick421 authored Dec 18, 2019
2 parents 1b5e8d0 + 2c92c55 commit d0ebb58
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 7 deletions.
2 changes: 1 addition & 1 deletion organizations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""
edx-organizations app initialization module
"""
__version__ = '2.1.1' # pragma: no cover
__version__ = '2.2.0' # pragma: no cover
14 changes: 14 additions & 0 deletions organizations/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
Permissions for organization viewsets.
"""
from rest_framework.permissions import BasePermission


class UserIsStaff(BasePermission):
"""
Custom Permission class to check user is a publisher user or a staff user.
"""
def has_permission(self, request, view):
if request.method == 'PUT':
return request.user.is_staff or request.user.is_superuser
return True
25 changes: 24 additions & 1 deletion organizations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
Data layer serialization operations. Converts querysets to simple
python containers (mainly arrays and dicts).
"""
import requests

from django.core.files.base import ContentFile
from rest_framework import serializers

from organizations import models
Expand All @@ -10,9 +13,29 @@
# pylint: disable=too-few-public-methods
class OrganizationSerializer(serializers.ModelSerializer):
""" Serializes the Organization object."""
logo_url = serializers.CharField(write_only=True, required=False)

class Meta(object): # pylint: disable=missing-docstring
model = models.Organization
fields = '__all__'
fields = ('id', 'created', 'modified', 'name', 'short_name', 'description', 'logo',
'active', 'logo_url',)

def update_logo(self, obj, logo_url): # pylint: disable=missing-docstring
if logo_url:
logo = requests.get(logo_url)
obj.logo.save(logo_url.split('/')[-1], ContentFile(logo.content))

def create(self, validated_data):
logo_url = validated_data.pop('logo_url', None)
obj = super(OrganizationSerializer, self).create(validated_data)
self.update_logo(obj, logo_url)
return obj

def update(self, instance, validated_data):
logo_url = validated_data.pop('logo_url', None)
super(OrganizationSerializer, self).update(instance, validated_data)
self.update_logo(instance, logo_url)
return instance


def serialize_organization(organization):
Expand Down
69 changes: 68 additions & 1 deletion organizations/v0/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""
Organizations Views Test Cases.
"""
import json
from django.urls import reverse
from django.test import TestCase
from provider.constants import CONFIDENTIAL
from provider.oauth2.models import AccessToken, Client

from organizations.models import Organization
from organizations.serializers import OrganizationSerializer
from organizations.tests.factories import UserFactory, OrganizationFactory

Expand All @@ -17,7 +19,7 @@ def setUp(self):
super(TestOrganizationsView, self).setUp()

self.user_password = 'test'
self.user = UserFactory(password=self.user_password)
self.user = UserFactory(password=self.user_password, is_superuser=True)
self.organization = OrganizationFactory.create()
self.organization_list_url = reverse('v0:organization-list')
self.client.login(username=self.user.username, password=self.user_password)
Expand Down Expand Up @@ -81,3 +83,68 @@ def test_oauth2(self):
HTTP_AUTHORIZATION='Bearer {}'.format('nonexistent-access-token')
)
self.assertEqual(response.status_code, 401)

def test_create_organization(self):
""" Verify Organization can be created via PUT endpoint. """
data = {
'name': 'example-name',
'short_name': 'example-short-name',
'description': 'example-description',
}
url = reverse('v0:organization-detail', kwargs={'short_name': data['short_name']})
response = self.client.put(url, json.dumps(data), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['name'], data['name'])
self.assertEqual(response.data['short_name'], data['short_name'])
self.assertEqual(response.data['description'], data['description'])
orgs = Organization.objects.all()
self.assertEqual(len(orgs), 2)

def test_update_organization(self):
""" Verify Organization can be updated via PUT endpoint. """
org = OrganizationFactory()
data = {
'name': 'changed-name',
'short_name': org.short_name,
}
url = reverse('v0:organization-detail', kwargs={'short_name': org.short_name})
response = self.client.put(url, json.dumps(data), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['name'], data['name'])
self.assertEqual(response.data['short_name'], org.short_name)
self.assertEqual(response.data['description'], org.description)
orgs = Organization.objects.all()
self.assertEqual(len(orgs), 2)

def test_patch_endpoint(self):
""" Verify PATCH endpoint returns 405 because we use PUT for create and update"""
url = reverse('v0:organization-detail', kwargs={'short_name': 'dummy'})
response = self.client.patch(url, json={})
self.assertEqual(response.status_code, 405)

def test_create_as_only_staff_user(self):
self.user.is_staff = True
self.user.is_superuser = False
self.user.save()

data = {
'name': 'example-name',
'short_name': 'example-short-name',
'description': 'example-description',
}
url = reverse('v0:organization-detail', kwargs={'short_name': data['short_name']})
response = self.client.put(url, json.dumps(data), content_type='application/json')
self.assertEqual(response.status_code, 200)

def test_create_as_non_staff_and_non_admin_user(self):
self.user.is_superuser = False
self.user.save()

data = {
'name': 'example-name',
'short_name': 'example-short-name',
'description': 'example-description',
}
url = reverse('v0:organization-detail', kwargs={'short_name': data['short_name']})
response = self.client.put(url, json.dumps(data), content_type='application/json')
self.assertEqual(response.status_code, 403)
30 changes: 26 additions & 4 deletions organizations/v0/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,44 @@
"""
Views for organizations end points.
"""
from django.http import Http404
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from rest_framework import mixins
from rest_framework import status
from rest_framework import viewsets
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework_oauth.authentication import OAuth2Authentication

from organizations.models import Organization
from organizations.permissions import UserIsStaff
from organizations.serializers import OrganizationSerializer


class OrganizationsViewSet(viewsets.ReadOnlyModelViewSet):
"""Organization view to fetch list organization data or single organization
using organization short name.
class OrganizationsViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
"""
Organization view to:
- fetch list organization data or single organization using organization short name.
- create or update an organization via the PUT endpoint.
"""
queryset = Organization.objects.filter(active=True) # pylint: disable=no-member
serializer_class = OrganizationSerializer
lookup_field = 'short_name'
authentication_classes = (OAuth2Authentication, JwtAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, UserIsStaff)

def update(self, request, *args, **kwargs):
""" We perform both Update and Create action via the PUT method. """
try:
return super(OrganizationsViewSet, self).update(request, *args, **kwargs)
except Exception as e: # pylint: disable=broad-except, invalid-name
if isinstance(e, Http404):
serializer = OrganizationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)

def partial_update(self, request, *args, **kwargs):
""" We disable PATCH because all updates and creates should use the PUT action above. """
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)

0 comments on commit d0ebb58

Please sign in to comment.