Repository: learningequality/kolibri

Issue:
Some DRF Viewsets have  typos (permission_classes vs. permissions_classes)
Some DRF Viewsets have  typos (permission_classes vs. permissions_classes). `permission_classes` is the correct property.

I fixed them in learningequality/kolibri@lessons, but reverted them [here](https://github.com/learningequality/kolibri/commit/cd92624327fcfb7609e156e637e09d02b4fa9b7b#diff-41d66ec4282b3fcf4b75a5a13946cf69) because there might be unintended consequences.

See the commit above for the files that are affected.


Discussion:
Odd... in the docs it says `permission_classes`: http://www.django-rest-framework.org/api-guide/permissions/
Yeah, my original post got it backwards. `permission_classes` is the correct property. In the code, there is use of `permissions_classes` (wrong name).
Ah, yep that's a typo!

Resolving patch:
diff --git a/kolibri/auth/api.py b/kolibri/auth/api.py
--- a/kolibri/auth/api.py
+++ b/kolibri/auth/api.py
@@ -1,25 +1,51 @@
-from __future__ import absolute_import, print_function, unicode_literals
+from __future__ import absolute_import
+from __future__ import print_function
+from __future__ import unicode_literals
 
 import time
 
-from django.contrib.auth import authenticate, get_user, login, logout, update_session_auth_hash
+from django.contrib.auth import authenticate
+from django.contrib.auth import get_user
+from django.contrib.auth import login
+from django.contrib.auth import logout
+from django.contrib.auth import update_session_auth_hash
 from django.contrib.auth.models import AnonymousUser
 from django.db import transaction
 from django.db.models import Q
 from django.db.models.query import F
-from django_filters.rest_framework import CharFilter, DjangoFilterBackend, FilterSet, ModelChoiceFilter
-from kolibri.core.mixins import BulkCreateMixin, BulkDeleteMixin
-from kolibri.logger.models import UserSessionLog
-from rest_framework import filters, permissions, status, viewsets
+from django_filters.rest_framework import CharFilter
+from django_filters.rest_framework import DjangoFilterBackend
+from django_filters.rest_framework import FilterSet
+from django_filters.rest_framework import ModelChoiceFilter
+from rest_framework import filters
+from rest_framework import permissions
+from rest_framework import status
+from rest_framework import viewsets
 from rest_framework.response import Response
 
 from .constants import collection_kinds
 from .filters import HierarchyRelationsFilter
-from .models import Classroom, Collection, Facility, FacilityDataset, FacilityUser, LearnerGroup, Membership, Role
-from .serializers import (
-    ClassroomSerializer, FacilityDatasetSerializer, FacilitySerializer, FacilityUsernameSerializer, FacilityUserSerializer, FacilityUserSignupSerializer,
-    LearnerGroupSerializer, MembershipSerializer, PublicFacilitySerializer, RoleSerializer
-)
+from .models import Classroom
+from .models import Collection
+from .models import Facility
+from .models import FacilityDataset
+from .models import FacilityUser
+from .models import LearnerGroup
+from .models import Membership
+from .models import Role
+from .serializers import ClassroomSerializer
+from .serializers import FacilityDatasetSerializer
+from .serializers import FacilitySerializer
+from .serializers import FacilityUsernameSerializer
+from .serializers import FacilityUserSerializer
+from .serializers import FacilityUserSignupSerializer
+from .serializers import LearnerGroupSerializer
+from .serializers import MembershipSerializer
+from .serializers import PublicFacilitySerializer
+from .serializers import RoleSerializer
+from kolibri.core.mixins import BulkCreateMixin
+from kolibri.core.mixins import BulkDeleteMixin
+from kolibri.logger.models import UserSessionLog
 
 
 class KolibriAuthPermissionsFilter(filters.BaseFilterBackend):
@@ -83,7 +109,7 @@ def has_object_permission(self, request, view, obj):
 
 
 class FacilityDatasetViewSet(viewsets.ModelViewSet):
-    permissions_classes = (KolibriAuthPermissions,)
+    permission_classes = (KolibriAuthPermissions,)
     filter_backends = (KolibriAuthPermissionsFilter,)
     serializer_class = FacilityDatasetSerializer
 
diff --git a/kolibri/content/api.py b/kolibri/content/api.py
--- a/kolibri/content/api.py
+++ b/kolibri/content/api.py
@@ -4,22 +4,33 @@
 
 import requests
 from django.core.cache import cache
-from django.db.models import Q, Sum
+from django.db.models import Q
+from django.db.models import Sum
 from django.db.models.aggregates import Count
 from django.http import Http404
 from django.utils.translation import ugettext as _
-from django_filters.rest_framework import BooleanFilter, CharFilter, ChoiceFilter, DjangoFilterBackend, FilterSet
-from kolibri.content import models, serializers
-from kolibri.content.permissions import CanManageContent
-from kolibri.content.utils.paths import get_channel_lookup_url
-from kolibri.logger.models import ContentSessionLog, ContentSummaryLog
-from le_utils.constants import content_kinds, languages
-from rest_framework import mixins, pagination, viewsets
-from rest_framework.decorators import detail_route, list_route
+from django_filters.rest_framework import BooleanFilter
+from django_filters.rest_framework import CharFilter
+from django_filters.rest_framework import ChoiceFilter
+from django_filters.rest_framework import DjangoFilterBackend
+from django_filters.rest_framework import FilterSet
+from le_utils.constants import content_kinds
+from le_utils.constants import languages
+from rest_framework import mixins
+from rest_framework import pagination
+from rest_framework import viewsets
+from rest_framework.decorators import detail_route
+from rest_framework.decorators import list_route
 from rest_framework.generics import get_object_or_404
 from rest_framework.response import Response
 
 from .utils.search import fuzz
+from kolibri.content import models
+from kolibri.content import serializers
+from kolibri.content.permissions import CanManageContent
+from kolibri.content.utils.paths import get_channel_lookup_url
+from kolibri.logger.models import ContentSessionLog
+from kolibri.logger.models import ContentSummaryLog
 
 logger = logging.getLogger(__name__)
 
@@ -333,7 +344,7 @@ def get_queryset(self):
 
 
 class RemoteChannelViewSet(viewsets.ViewSet):
-    permissions_classes = (CanManageContent,)
+    permission_classes = (CanManageContent,)
 
     http_method_names = ['get']
 
diff --git a/kolibri/core/exams/api.py b/kolibri/core/exams/api.py
--- a/kolibri/core/exams/api.py
+++ b/kolibri/core/exams/api.py
@@ -1,13 +1,18 @@
 from django.db.models.query import F
 from django.shortcuts import get_object_or_404
 from django.utils.timezone import now
-from django_filters.rest_framework import DjangoFilterBackend, FilterSet
-from kolibri.auth.api import KolibriAuthPermissions, KolibriAuthPermissionsFilter
-from kolibri.auth.filters import HierarchyRelationsFilter
-from kolibri.core.exams import models, serializers
-from rest_framework import pagination, viewsets
+from django_filters.rest_framework import DjangoFilterBackend
+from django_filters.rest_framework import FilterSet
+from rest_framework import pagination
+from rest_framework import viewsets
 from rest_framework.response import Response
 
+from kolibri.auth.api import KolibriAuthPermissions
+from kolibri.auth.api import KolibriAuthPermissionsFilter
+from kolibri.auth.filters import HierarchyRelationsFilter
+from kolibri.core.exams import models
+from kolibri.core.exams import serializers
+
 
 class OptionalPageNumberPagination(pagination.PageNumberPagination):
     """
@@ -27,7 +32,7 @@ class Meta:
 class ExamViewset(viewsets.ModelViewSet):
     serializer_class = serializers.ExamSerializer
     pagination_class = OptionalPageNumberPagination
-    permissions_classes = (KolibriAuthPermissions,)
+    permission_classes = (KolibriAuthPermissions,)
     filter_backends = (KolibriAuthPermissionsFilter, DjangoFilterBackend)
     filter_class = ExamFilter
 
@@ -45,7 +50,7 @@ def perform_update(self, serializer):
 class ExamAssignmentViewset(viewsets.ModelViewSet):
     serializer_class = serializers.ExamAssignmentSerializer
     pagination_class = OptionalPageNumberPagination
-    permissions_classes = (KolibriAuthPermissions,)
+    permission_classes = (KolibriAuthPermissions,)
     filter_backends = (KolibriAuthPermissionsFilter,)
 
     def get_queryset(self):
@@ -55,7 +60,7 @@ def get_queryset(self):
 class UserExamViewset(viewsets.ModelViewSet):
     serializer_class = serializers.UserExamSerializer
     pagination_class = OptionalPageNumberPagination
-    permissions_classes = (KolibriAuthPermissions,)
+    permission_classes = (KolibriAuthPermissions,)
     filter_backends = (KolibriAuthPermissionsFilter,)
 
     def get_queryset(self):


Test patch:
diff --git a/kolibri/auth/test/test_api.py b/kolibri/auth/test/test_api.py
--- a/kolibri/auth/test/test_api.py
+++ b/kolibri/auth/test/test_api.py
@@ -1,18 +1,19 @@
-from __future__ import absolute_import, print_function, unicode_literals
+from __future__ import absolute_import
+from __future__ import print_function
+from __future__ import unicode_literals
 
 import collections
-import factory
 import sys
 
+import factory
+from django.contrib.sessions.models import Session
 from django.core.urlresolvers import reverse
-
 from rest_framework import status
 from rest_framework.test import APITestCase as BaseTestCase
-from django.contrib.sessions.models import Session
-
-from .helpers import create_superuser, provision_device
 
 from .. import models
+from .helpers import create_superuser
+from .helpers import provision_device
 
 DUMMY_PASSWORD = "password"
 
@@ -379,23 +380,40 @@ class FacilityDatasetAPITestCase(APITestCase):
     def setUp(self):
         provision_device()
         self.facility = FacilityFactory.create()
+        self.facility2 = FacilityFactory.create()
         self.superuser = create_superuser(self.facility)
         self.admin = FacilityUserFactory.create(facility=self.facility)
         self.user = FacilityUserFactory.create(facility=self.facility)
         self.facility.add_admin(self.admin)
 
-    def test_return_dataset_that_user_is_an_admin_for(self):
+    def test_return_all_datasets_for_an_admin(self):
         self.client.login(username=self.admin.username, password=DUMMY_PASSWORD)
         response = self.client.get(reverse('facilitydataset-list'))
         self.assertEqual(len(response.data), len(models.FacilityDataset.objects.all()))
-        self.assertEqual(self.admin.dataset_id, response.data[0]['id'])
+
+    def test_admin_can_edit_dataset_for_which_they_are_admin(self):
+        self.client.login(username=self.admin.username, password=DUMMY_PASSWORD)
+        response = self.client.patch(reverse('facilitydataset-detail', kwargs={'pk': self.facility.dataset_id}),
+                                     {'description': 'This is not a drill'}, format="json")
+        self.assertEqual(response.status_code, 200)
+
+    def test_admin_cant_edit_dataset_for_which_they_are_not_admin(self):
+        self.client.login(username=self.admin.username, password=DUMMY_PASSWORD)
+        response = self.client.delete(reverse('facilitydataset-detail', kwargs={'pk': self.facility2.dataset_id}),
+                                      {'description': 'This is not a drill'}, format="json")
+        self.assertEqual(response.status_code, 403)
 
     def test_return_all_datasets_for_superuser(self):
         self.client.login(username=self.superuser.username, password=DUMMY_PASSWORD, facility=self.facility)
         response = self.client.get(reverse('facilitydataset-list'))
         self.assertEqual(len(response.data), len(models.FacilityDataset.objects.all()))
 
-    def test_return_dataset_for_facility_user(self):
+    def test_return_all_datasets_for_facility_user(self):
         self.client.login(username=self.user.username, password=DUMMY_PASSWORD)
         response = self.client.get(reverse('facilitydataset-list'))
         self.assertEqual(len(response.data), len(models.FacilityDataset.objects.all()))
+
+    def test_facility_user_cannot_delete_dataset(self):
+        self.client.login(username=self.user.username, password=DUMMY_PASSWORD)
+        response = self.client.delete(reverse('facilitydataset-detail', kwargs={'pk': self.facility.dataset_id}), format="json")
+        self.assertEqual(response.status_code, 403)
diff --git a/kolibri/content/test/test_content_app.py b/kolibri/content/test/test_content_app.py
--- a/kolibri/content/test/test_content_app.py
+++ b/kolibri/content/test/test_content_app.py
@@ -9,15 +9,18 @@
 from django.core.cache import cache
 from django.core.urlresolvers import reverse
 from django.test import TestCase
-from kolibri.auth.models import Facility, FacilityUser
-from kolibri.auth.test.helpers import provision_device
-from kolibri.content import models as content
-from kolibri.core.device.models import DevicePermissions, DeviceSettings
-from kolibri.logger.models import ContentSummaryLog
 from le_utils.constants import content_kinds
 from rest_framework import status
 from rest_framework.test import APITestCase
 
+from kolibri.auth.models import Facility
+from kolibri.auth.models import FacilityUser
+from kolibri.auth.test.helpers import provision_device
+from kolibri.content import models as content
+from kolibri.core.device.models import DevicePermissions
+from kolibri.core.device.models import DeviceSettings
+from kolibri.logger.models import ContentSummaryLog
+
 DUMMY_PASSWORD = "password"
 
 
@@ -475,8 +478,8 @@ class KolibriStudioAPITestCase(APITestCase):
 
     def setUp(self):
         DeviceSettings.objects.create(is_provisioned=True)
-        facility = Facility.objects.create(name='facility')
-        superuser = FacilityUser.objects.create(username='superuser', facility=facility)
+        self.facility = Facility.objects.create(name='facility')
+        superuser = FacilityUser.objects.create(username='superuser', facility=self.facility)
         superuser.set_password(DUMMY_PASSWORD)
         superuser.save()
         DevicePermissions.objects.create(user=superuser, is_superuser=True)
@@ -487,6 +490,16 @@ def test_channel_list(self):
         response = self.client.get(reverse('remotechannel-list'), format='json')
         self.assertEqual(response.data[0]['id'], 1)
 
+    @mock_patch_decorator
+    def test_no_permission_non_superuser_channel_list(self):
+        user = FacilityUser.objects.create(username='user', facility=self.facility)
+        user.set_password(DUMMY_PASSWORD)
+        user.save()
+        self.client.logout()
+        self.client.login(username=user.username, password=DUMMY_PASSWORD)
+        response = self.client.get(reverse('remotechannel-list'), format='json')
+        self.assertEqual(response.status_code, 403)
+
     @mock_patch_decorator
     def test_channel_retrieve(self):
         response = self.client.get(reverse('remotechannel-detail', kwargs={'pk': 'abc'}), format='json')
diff --git a/kolibri/core/exams/test/test_exam_api.py b/kolibri/core/exams/test/test_exam_api.py
--- a/kolibri/core/exams/test/test_exam_api.py
+++ b/kolibri/core/exams/test/test_exam_api.py
@@ -1,13 +1,14 @@
-from __future__ import absolute_import, print_function, unicode_literals
+from __future__ import absolute_import
+from __future__ import print_function
+from __future__ import unicode_literals
 
 from django.core.urlresolvers import reverse
-
-from kolibri.auth.models import Facility, FacilityUser
-from kolibri.auth.test.helpers import provision_device
-
 from rest_framework.test import APITestCase
 
 from .. import models
+from kolibri.auth.models import Facility
+from kolibri.auth.models import FacilityUser
+from kolibri.auth.test.helpers import provision_device
 
 class UserExamAPITestCase(APITestCase):
 
@@ -40,6 +41,76 @@ def test_logged_in_userexam_list(self):
         response = self.client.get(reverse("userexam-list"))
         self.assertEqual(len(response.data), 1)
 
+    def test_logged_in_user_userexam_no_delete(self):
+
+        user = FacilityUser.objects.create(username="learner", facility=self.facility)
+        user.set_password("pass")
+        user.save()
+
+        self.client.login(username=user.username, password="pass")
+
+        response = self.client.delete(reverse("userexam-detail", kwargs={'pk': self.assignment.id}))
+        self.assertEqual(response.status_code, 403)
+
     def test_anonymous_userexam_list(self):
         response = self.client.get(reverse("userexam-list"))
         self.assertEqual(len(response.data), 0)
+
+
+class ExamAPITestCase(APITestCase):
+
+    def setUp(self):
+        provision_device()
+        self.facility = Facility.objects.create(name="MyFac")
+        user = FacilityUser.objects.create(username="admin", facility=self.facility)
+        self.exam = models.Exam.objects.create(
+            title="title",
+            channel_id="test",
+            question_count=1,
+            active=True,
+            collection=self.facility,
+            creator=user
+        )
+
+    def test_logged_in_user_exam_no_delete(self):
+
+        user = FacilityUser.objects.create(username="learner", facility=self.facility)
+        user.set_password("pass")
+        user.save()
+
+        self.client.login(username=user.username, password="pass")
+
+        response = self.client.delete(reverse("exam-detail", kwargs={'pk': self.exam.id}))
+        self.assertEqual(response.status_code, 403)
+
+
+class ExamAssignmentAPITestCase(APITestCase):
+
+    def setUp(self):
+        provision_device()
+        self.facility = Facility.objects.create(name="MyFac")
+        user = FacilityUser.objects.create(username="admin", facility=self.facility)
+        self.exam = models.Exam.objects.create(
+            title="title",
+            channel_id="test",
+            question_count=1,
+            active=True,
+            collection=self.facility,
+            creator=user
+        )
+        self.assignment = models.ExamAssignment.objects.create(
+            exam=self.exam,
+            collection=self.facility,
+            assigned_by=user,
+        )
+
+    def test_logged_in_user_examassignment_no_delete(self):
+
+        user = FacilityUser.objects.create(username="learner", facility=self.facility)
+        user.set_password("pass")
+        user.save()
+
+        self.client.login(username=user.username, password="pass")
+
+        response = self.client.delete(reverse("examassignment-detail", kwargs={'pk': self.assignment.id}))
+        self.assertEqual(response.status_code, 403)