Commit 92153752 by Clinton Blackburn

Rewrote Credit API

- API built atop Django REST Framework
- Added support for OAuth 2.0 and session authentication
- Added permissions around eligibility data

ECOM-2609
parent 5043b465
......@@ -393,9 +393,6 @@ FEATURES = {
# Enable OpenBadge support. See the BADGR_* settings later in this file.
'ENABLE_OPENBADGES': False,
# Credit course API
'ENABLE_CREDIT_API': True,
# The block types to disable need to be specified in "x block disable config" in django admin.
'ENABLE_DISABLING_XBLOCK_TYPES': True,
......
......@@ -68,8 +68,6 @@ FEATURES['ENABLE_SHOPPING_CART'] = True
FEATURES['ENABLE_VERIFIED_CERTIFICATES'] = True
FEATURES['ENABLE_CREDIT_API'] = True
# Enable this feature for course staff grade downloads, to enable acceptance tests
FEATURES['ENABLE_S3_GRADE_DOWNLOADS'] = True
FEATURES['ALLOW_COURSE_STAFF_GRADE_DOWNLOADS'] = True
......
......@@ -16,6 +16,8 @@ var edx = edx || {};
headers: {
'X-CSRFToken': $.cookie('csrftoken')
},
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({
'course_key': courseKey,
'username': username
......
......@@ -140,7 +140,11 @@ var edx = edx || {};
return $.ajax({
url: _.sprintf(providerUrl, providerId),
type: 'GET',
dataType: 'json'
dataType: 'json',
contentType: 'application/json',
headers: {
'X-CSRFToken': $.cookie('csrftoken')
}
}).retry({times: 5, timeout: 2000, statusCodes: [404]});
},
/**
......
......@@ -7,8 +7,8 @@
</div>
<div class="provider-more-info">
<%= interpolate(
gettext("To finalize course credit, %(provider_id)s requires %(platform_name)s learners to submit a credit request."),
{ provider_id: provider_id.toUpperCase(), platform_name: platformName }, true
gettext("To finalize course credit, %(display_name)s requires %(platform_name)s learners to submit a credit request."),
{ display_name: display_name, platform_name: platformName }, true
) %>
</div>
<div class="provider-instructions">
......@@ -21,7 +21,7 @@
<%= interpolate("<img src='%s' alt='%s'></image>", [thumbnail_url, display_name]) %>
</div>
<div class="complete-order">
<%= interpolate('<button data-provider="%s" data-course-key="%s" data-username="%s" class="complete-course" onClick=completeOrder(this)>%s</button>', [provider_id, course_key, username,
<%= interpolate('<button data-provider="%s" data-course-key="%s" data-username="%s" class="complete-course" onClick=completeOrder(this)>%s</button>', [id, course_key, username,
gettext( "Get Credit")]) %>
</div>
</div>
......@@ -98,6 +98,7 @@ urlpatterns = (
url(r'^api/val/v0/', include('edxval.urls')),
url(r'^api/commerce/', include('commerce.api.urls', namespace='commerce_api')),
url(r'^api/credit/', include('openedx.core.djangoapps.credit.urls', app_name="credit", namespace='credit')),
)
if settings.FEATURES["ENABLE_COMBINED_LOGIN_REGISTRATION"]:
......@@ -115,12 +116,6 @@ else:
url(r'^register$', 'student.views.register_user', name="register_user"),
)
if settings.FEATURES.get("ENABLE_CREDIT_API"):
# Credit API end-points
urlpatterns += (
url(r'^api/credit/', include('openedx.core.djangoapps.credit.urls', app_name="credit", namespace='credit')),
)
if settings.FEATURES["ENABLE_MOBILE_REST_API"]:
urlpatterns += (
url(r'^api/mobile/v0.5/', include('mobile_api.urls')),
......
......@@ -5,6 +5,8 @@ whether a user has satisfied those requirements.
import logging
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.credit.exceptions import InvalidCreditRequirements, InvalidCreditCourse
from openedx.core.djangoapps.credit.email_utils import send_credit_notifications
from openedx.core.djangoapps.credit.models import (
......@@ -14,8 +16,7 @@ from openedx.core.djangoapps.credit.models import (
CreditEligibility,
)
from opaque_keys.edx.keys import CourseKey
# TODO: Cleanup this mess! ECOM-2908
log = logging.getLogger(__name__)
......@@ -246,8 +247,7 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name,
# Find the requirement we're trying to set
req_to_update = next((
req for req in reqs
if req.namespace == req_namespace
and req.name == req_name
if req.namespace == req_namespace and req.name == req_name
), None)
# If we can't find the requirement, then the most likely explanation
......
......@@ -4,12 +4,12 @@ API for initiating and tracking requests for credit from a provider.
import datetime
import logging
import pytz
import uuid
import pytz
from django.db import transaction
from lms.djangoapps.django_comment_client.utils import JsonResponse
from lms.djangoapps.django_comment_client.utils import JsonResponse
from openedx.core.djangoapps.credit.exceptions import (
UserIsNotEligible,
CreditProviderNotConfigured,
......@@ -28,6 +28,8 @@ from student.models import User
from util.date_utils import to_timestamp
# TODO: Cleanup this mess! ECOM-2908
log = logging.getLogger(__name__)
......@@ -257,12 +259,10 @@ def create_credit_request(course_key, provider_id, username):
final_grade = unicode(final_grade)
except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError):
log.exception(
"Could not retrieve final grade from the credit eligibility table "
"for user %s in course %s.",
user.id, course_key
)
raise UserIsNotEligible
msg = 'Could not retrieve final grade from the credit eligibility table for ' \
'user [{user_id}] in course [{course_key}].'.format(user_id=user.id, course_key=course_key)
log.exception(msg)
raise UserIsNotEligible(msg)
parameters = {
"request_uuid": credit_request.uuid,
......
"""Exceptions raised by the credit API. """
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from rest_framework import status
from rest_framework.exceptions import APIException
# TODO: Cleanup this mess! ECOM-2908
class CreditApiBadRequest(Exception):
......@@ -56,3 +62,25 @@ class InvalidCreditStatus(CreditApiBadRequest):
The status is not either "approved" or "rejected".
"""
pass
class InvalidCreditRequest(APIException):
""" API request is invalid. """
status_code = status.HTTP_400_BAD_REQUEST
class UserNotEligibleException(InvalidCreditRequest):
""" User not eligible for credit for a given course. """
def __init__(self, course_key, username):
detail = _('[{username}] is not eligible for credit for [{course_key}].').format(username=username,
course_key=course_key)
super(UserNotEligibleException, self).__init__(detail)
class InvalidCourseKey(InvalidCreditRequest):
""" Course key is invalid. """
def __init__(self, course_key):
detail = _('[{course_key}] is not a valid course key.').format(course_key=course_key)
super(InvalidCourseKey, self).__init__(detail)
......@@ -25,6 +25,7 @@ from xmodule_django.models import CourseKeyField
from django.utils.translation import ugettext_lazy
CREDIT_PROVIDER_ID_REGEX = r"[a-z,A-Z,0-9,\-]+"
log = logging.getLogger(__name__)
......@@ -42,7 +43,7 @@ class CreditProvider(TimeStampedModel):
unique=True,
validators=[
RegexValidator(
regex=r"^[a-z,A-Z,0-9,\-]+$",
regex=CREDIT_PROVIDER_ID_REGEX,
message="Only alphanumeric characters and hyphens (-) are allowed",
code="invalid_provider_id",
)
......@@ -498,10 +499,7 @@ def default_deadline_for_credit_eligibility(): # pylint: disable=invalid-name
class CreditEligibility(TimeStampedModel):
"""
A record of a user's eligibility for credit from a specific credit
provider for a specific course.
"""
""" A record of a user's eligibility for credit for a specific course. """
username = models.CharField(max_length=255, db_index=True)
course = models.ForeignKey(CreditCourse, related_name="eligibilities")
......
""" Credit API Serializers """
from rest_framework import serializers
from __future__ import unicode_literals
import datetime
import logging
from opaque_keys.edx.keys import CourseKey
from django.conf import settings
from opaque_keys import InvalidKeyError
from openedx.core.djangoapps.credit.models import CreditCourse
from opaque_keys.edx.keys import CourseKey
import pytz
from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider, CreditEligibility, CreditRequest
from openedx.core.djangoapps.credit.signature import get_shared_secret_key, signature
from util.date_utils import from_timestamp
log = logging.getLogger(__name__)
class CourseKeyField(serializers.Field):
"""
Serializer field for a model CourseKey field.
"""
""" Serializer field for a model CourseKey field. """
def to_representation(self, data):
"""Convert a course key to unicode. """
......@@ -32,3 +41,81 @@ class CreditCourseSerializer(serializers.ModelSerializer):
class Meta(object):
model = CreditCourse
exclude = ('id',)
class CreditProviderSerializer(serializers.ModelSerializer):
""" CreditProvider """
id = serializers.CharField(source='provider_id') # pylint:disable=invalid-name
description = serializers.CharField(source='provider_description')
status_url = serializers.URLField(source='provider_status_url')
url = serializers.URLField(source='provider_url')
class Meta(object):
model = CreditProvider
fields = ('id', 'display_name', 'url', 'status_url', 'description', 'enable_integration',
'fulfillment_instructions', 'thumbnail_url',)
class CreditEligibilitySerializer(serializers.ModelSerializer):
""" CreditEligibility serializer. """
course_key = serializers.SerializerMethodField()
def get_course_key(self, obj):
""" Returns the course key associated with the course. """
return unicode(obj.course.course_key)
class Meta(object):
model = CreditEligibility
fields = ('username', 'course_key', 'deadline',)
class CreditProviderCallbackSerializer(serializers.Serializer): # pylint:disable=abstract-method
"""
Serializer for input to the CreditProviderCallback view.
This is used solely for validating the input.
"""
request_uuid = serializers.CharField(required=True)
status = serializers.ChoiceField(required=True, choices=CreditRequest.REQUEST_STATUS_CHOICES)
timestamp = serializers.IntegerField(required=True)
signature = serializers.CharField(required=True)
def __init__(self, **kwargs):
self.provider = kwargs.pop('provider', None)
super(CreditProviderCallbackSerializer, self).__init__(**kwargs)
def validate_timestamp(self, value):
""" Ensure the request has been received in a timely manner. """
date_time = from_timestamp(value)
# Ensure we converted the timestamp to a datetime
if not date_time:
msg = '[{}] is not a valid timestamp'.format(value)
log.warning(msg)
raise serializers.ValidationError(msg)
elapsed = (datetime.datetime.now(pytz.UTC) - date_time).total_seconds()
if elapsed > settings.CREDIT_PROVIDER_TIMESTAMP_EXPIRATION:
msg = '[{value}] is too far in the past (over [{elapsed}] seconds).'.format(value=value, elapsed=elapsed)
log.warning(msg)
raise serializers.ValidationError(msg)
return value
def validate_signature(self, value):
""" Validate the signature and ensure the provider is setup properly. """
provider_id = self.provider.provider_id
secret_key = get_shared_secret_key(provider_id)
if secret_key is None:
msg = 'Could not retrieve secret key for credit provider [{}]. ' \
'Unable to validate requests from provider.'.format(provider_id)
log.error(msg)
raise PermissionDenied(msg)
data = self.initial_data
actual_signature = data["signature"]
if signature(data, secret_key) != actual_signature:
msg = 'Request from credit provider [{}] had an invalid signature.'.format(provider_id)
raise PermissionDenied(msg)
return value
......@@ -59,5 +59,5 @@ def signature(params, shared_secret):
for key in sorted(params.keys())
if key != u"signature"
])
hasher = hmac.new(shared_secret, encoded_params.encode('utf-8'), hashlib.sha256)
hasher = hmac.new(shared_secret.encode('utf-8'), encoded_params.encode('utf-8'), hashlib.sha256)
return hasher.hexdigest()
......@@ -2,13 +2,9 @@
This file contains celery tasks for credit course views.
"""
import datetime
from pytz import UTC
from django.conf import settings
from celery import task
from celery.utils.log import get_task_logger
from django.conf import settings
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
......
# pylint:disable=missing-docstring,no-member
import datetime
import json
import uuid # pylint:disable=unused-import
from django.contrib.auth.models import User
import factory
from factory.fuzzy import FuzzyText
import pytz
from openedx.core.djangoapps.credit.models import CreditProvider, CreditEligibility, CreditCourse, CreditRequest
from util.date_utils import to_timestamp
class CreditCourseFactory(factory.DjangoModelFactory):
class Meta(object):
model = CreditCourse
course_key = FuzzyText(prefix='fake.org/', suffix='/fake.run')
enabled = True
class CreditProviderFactory(factory.DjangoModelFactory):
class Meta(object):
model = CreditProvider
provider_id = FuzzyText(length=5)
provider_url = FuzzyText(prefix='http://')
class CreditEligibilityFactory(factory.DjangoModelFactory):
class Meta(object):
model = CreditEligibility
course = factory.SubFactory(CreditCourseFactory)
class CreditRequestFactory(factory.DjangoModelFactory):
class Meta(object):
model = CreditRequest
uuid = factory.LazyAttribute(lambda o: uuid.uuid4().hex) # pylint: disable=undefined-variable
# pylint: disable=access-member-before-definition,attribute-defined-outside-init,no-self-argument,unused-argument
@factory.post_generation
def post(obj, create, extracted, **kwargs):
"""
Post-generation handler.
Sets up parameters field.
"""
if not obj.parameters:
course_key = obj.course.course_key
user = User.objects.get(username=obj.username)
user_profile = user.profile
# pylint:disable=access-member-before-definition
obj.parameters = json.dumps({
"request_uuid": obj.uuid,
"timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)),
"course_org": course_key.org,
"course_num": course_key.course,
"course_run": course_key.run,
"final_grade": '0.96',
"user_username": user.username,
"user_email": user.email,
"user_full_name": user_profile.name,
"user_mailing_address": "",
"user_country": user_profile.country.code or "",
})
obj.save()
......@@ -2,17 +2,13 @@
Tests for the API functions in the credit app.
"""
import datetime
import json
import unittest
import ddt
from django.conf import settings
from django.core import mail
from django.test import TestCase
from django.test.utils import override_settings
from django.db import connection, transaction
from django.core.urlresolvers import reverse, NoReverseMatch
from mock import patch
from opaque_keys.edx.keys import CourseKey
import pytz
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -39,8 +35,6 @@ from student.tests.factories import UserFactory
TEST_CREDIT_PROVIDER_SECRET_KEY = "931433d583c84ca7ba41784bad3232e6"
from util.testing import UrlResetMixin
@override_settings(CREDIT_PROVIDER_SECRET_KEYS={
"hogwarts": TEST_CREDIT_PROVIDER_SECRET_KEY,
......@@ -880,119 +874,3 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase):
"""Check the user's credit status. """
statuses = api.get_credit_requests_for_user(self.USER_INFO["username"])
self.assertEqual(statuses[0]["status"], expected_status)
class CreditApiFeatureFlagTest(UrlResetMixin, TestCase):
"""
Base class to test the credit api urls.
"""
def setUp(self, **kwargs):
enable_credit_api = kwargs.get('enable_credit_api', False)
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_CREDIT_API': enable_credit_api}):
super(CreditApiFeatureFlagTest, self).setUp('lms.urls')
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class CreditApiFeatureFlagDisabledTests(CreditApiFeatureFlagTest):
"""
Test Python API for credit provider api with feature flag
'ENABLE_CREDIT_API' disabled.
"""
PROVIDER_ID = "hogwarts"
def setUp(self):
super(CreditApiFeatureFlagDisabledTests, self).setUp(enable_credit_api=False)
def test_get_credit_provider_details(self):
"""
Test that 'get_provider_info' api url not found.
"""
with self.assertRaises(NoReverseMatch):
reverse('credit:get_provider_info', args=[self.PROVIDER_ID])
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class CreditApiFeatureFlagEnabledTests(CreditApiFeatureFlagTest, CreditApiTestBase):
"""
Test Python API for credit provider api with feature flag
'ENABLE_CREDIT_API' enabled.
"""
USER_INFO = {
"username": "bob",
"email": "bob@example.com",
"full_name": "Bob",
"mailing_address": "123 Fake Street, Cambridge MA",
"country": "US",
}
FINAL_GRADE = 0.95
def setUp(self):
super(CreditApiFeatureFlagEnabledTests, self).setUp(enable_credit_api=True)
self.user = UserFactory(
username=self.USER_INFO['username'],
email=self.USER_INFO['email'],
)
self.user.profile.name = self.USER_INFO['full_name']
self.user.profile.mailing_address = self.USER_INFO['mailing_address']
self.user.profile.country = self.USER_INFO['country']
self.user.profile.save()
# By default, configure the database so that there is a single
# credit requirement that the user has satisfied (minimum grade)
self._configure_credit()
def test_get_credit_provider_details(self):
"""Test that credit api method 'test_get_credit_provider_details'
returns dictionary data related to provided credit provider.
"""
expected_result = {
"provider_id": self.PROVIDER_ID,
"display_name": self.PROVIDER_NAME,
"provider_url": self.PROVIDER_URL,
"provider_status_url": self.PROVIDER_STATUS_URL,
"provider_description": self.PROVIDER_DESCRIPTION,
"enable_integration": self.ENABLE_INTEGRATION,
"fulfillment_instructions": self.FULFILLMENT_INSTRUCTIONS,
"thumbnail_url": self.THUMBNAIL_URL
}
path = reverse('credit:get_provider_info', kwargs={'provider_id': self.PROVIDER_ID})
result = self.client.get(path)
result = json.loads(result.content)
self.assertEqual(result, expected_result)
# now test that user gets empty dict for non existent credit provider
path = reverse('credit:get_provider_info', kwargs={'provider_id': 'fake_provider_id'})
result = self.client.get(path)
result = json.loads(result.content)
self.assertEqual(result, {})
def _configure_credit(self):
"""
Configure a credit course and its requirements.
By default, add a single requirement (minimum grade)
that the user has satisfied.
"""
credit_course = self.add_credit_course()
requirement = CreditRequirement.objects.create(
course=credit_course,
namespace="grade",
name="grade",
active=True
)
status = CreditRequirementStatus.objects.create(
username=self.USER_INFO["username"],
requirement=requirement,
)
status.status = "satisfied"
status.reason = {"final_grade": self.FINAL_GRADE}
status.save()
CreditEligibility.objects.create(
username=self.USER_INFO['username'],
course=CreditCourse.objects.get(course_key=self.course_key)
)
......@@ -5,7 +5,6 @@ Tests for credit course models.
import ddt
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.credit.models import CreditCourse, CreditRequirement
......@@ -17,7 +16,7 @@ class CreditEligibilityModelTests(TestCase):
Tests for credit models used to track credit eligibility.
"""
def setUp(self, **kwargs):
def setUp(self):
super(CreditEligibilityModelTests, self).setUp()
self.course_key = CourseKey.from_string("edX/DemoX/Demo_Course")
......
""" Tests for Credit API serializers. """
# pylint: disable=no-member
from __future__ import unicode_literals
from django.test import TestCase
from openedx.core.djangoapps.credit import serializers
from openedx.core.djangoapps.credit.tests.factories import CreditProviderFactory, CreditEligibilityFactory
from student.tests.factories import UserFactory
class CreditProviderSerializerTests(TestCase):
""" CreditProviderSerializer tests. """
def test_data(self):
""" Verify the correct fields are serialized. """
provider = CreditProviderFactory(active=False)
serializer = serializers.CreditProviderSerializer(provider)
expected = {
'id': provider.provider_id,
'display_name': provider.display_name,
'url': provider.provider_url,
'status_url': provider.provider_status_url,
'description': provider.provider_description,
'enable_integration': provider.enable_integration,
'fulfillment_instructions': provider.fulfillment_instructions,
'thumbnail_url': provider.thumbnail_url,
}
self.assertDictEqual(serializer.data, expected)
class CreditEligibilitySerializerTests(TestCase):
""" CreditEligibilitySerializer tests. """
def test_data(self):
""" Verify the correct fields are serialized. """
user = UserFactory()
eligibility = CreditEligibilityFactory(username=user.username)
serializer = serializers.CreditEligibilitySerializer(eligibility)
expected = {
'course_key': unicode(eligibility.course.course_key),
'deadline': eligibility.deadline.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'username': user.username,
}
self.assertDictEqual(serializer.data, expected)
......@@ -3,47 +3,25 @@ URLs for the credit app.
"""
from django.conf.urls import patterns, url, include
from openedx.core.djangoapps.credit import views, routers
from openedx.core.djangoapps.credit.api.provider import get_credit_provider_info
from openedx.core.djangoapps.credit import views, routers, models
PROVIDER_ID_PATTERN = r'(?P<provider_id>[^/]+)'
PROVIDER_ID_PATTERN = r'(?P<provider_id>{})'.format(models.CREDIT_PROVIDER_ID_REGEX)
V1_URLS = patterns(
PROVIDER_URLS = patterns(
'',
url(r'^request/$', views.CreditProviderRequestCreateView.as_view(), name='create_request'),
url(r'^callback/?$', views.CreditProviderCallbackView.as_view(), name='provider_callback'),
)
url(
r'^providers/$',
views.get_providers_detail,
name='providers_detail'
),
url(
r'^providers/{provider_id}/$'.format(provider_id=PROVIDER_ID_PATTERN),
get_credit_provider_info,
name='get_provider_info'
),
url(
r'^providers/{provider_id}/request/$'.format(provider_id=PROVIDER_ID_PATTERN),
views.create_credit_request,
name='create_request'
),
url(
r'^providers/{provider_id}/callback/?$'.format(provider_id=PROVIDER_ID_PATTERN),
views.credit_provider_callback,
name='provider_callback'
),
url(
r'^eligibility/$',
views.get_eligibility_for_user,
name='eligibility_details'
),
V1_URLS = patterns(
'',
url(r'^providers/{}/'.format(PROVIDER_ID_PATTERN), include(PROVIDER_URLS)),
url(r'^eligibility/$', views.CreditEligibilityView.as_view(), name='eligibility_details'),
)
router = routers.SimpleRouter() # pylint: disable=invalid-name
router.register(r'courses', views.CreditCourseViewSet)
router.register(r'providers', views.CreditProviderViewSet)
V1_URLS += router.urls
urlpatterns = patterns(
......
......@@ -80,3 +80,16 @@ class IsStaffOrReadOnly(permissions.BasePermission):
return (request.user.is_staff or
CourseStaffRole(obj.course_id).has_user(request.user) or
request.method in permissions.SAFE_METHODS)
class IsStaffOrOwner(permissions.BasePermission):
"""
Permission that allows access to admin users or the owner of an object.
The owner is considered the User object represented by obj.user.
"""
def has_object_permission(self, request, view, obj):
return request.user.is_staff or obj.user == request.user
def has_permission(self, request, view):
user = request.user
return user.is_staff or (user.username == request.data.get('username'))
""" Tests for API permissions classes. """
from django.test import TestCase, RequestFactory
from openedx.core.lib.api.permissions import IsStaffOrOwner
from student.tests.factories import UserFactory
class TestObject(object):
""" Fake class for object permission tests. """
user = None
class IsStaffOrOwnerTests(TestCase):
""" Tests for IsStaffOrOwner permission class. """
def setUp(self):
super(IsStaffOrOwnerTests, self).setUp()
self.permission = IsStaffOrOwner()
self.request = RequestFactory().get('/')
self.obj = TestObject()
def assert_user_has_object_permission(self, user, permitted):
"""
Asserts whether or not the user has permission to access an object.
Arguments
user (User)
permitted (boolean)
"""
self.request.user = user
self.assertEqual(self.permission.has_object_permission(self.request, None, self.obj), permitted)
def test_staff_user(self):
""" Staff users should be permitted. """
user = UserFactory.create(is_staff=True)
self.assert_user_has_object_permission(user, True)
def test_owner(self):
""" Owners should be permitted. """
user = UserFactory.create()
self.obj.user = user
self.assert_user_has_object_permission(user, True)
def test_non_staff_test_non_owner_or_staff_user(self):
""" Non-staff and non-owner users should not be permitted. """
user = UserFactory.create()
self.assert_user_has_object_permission(user, False)
def test_has_permission_as_staff(self):
""" Staff users always have permission. """
self.request.user = UserFactory.create(is_staff=True)
self.assertTrue(self.permission.has_permission(self.request, None))
def test_has_permission_as_owner(self):
""" Owners always have permission. """
user = UserFactory.create()
request = RequestFactory().get('/?username={}'.format(user.username))
request.user = user
self.assertTrue(self.permission.has_permission(request, None))
def test_has_permission_as_non_owner(self):
""" Non-owners should not have permission. """
user = UserFactory.create()
request = RequestFactory().get('/?username={}'.format(user.username))
request.user = UserFactory.create()
self.assertFalse(self.permission.has_permission(request, None))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment