Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 36 additions & 0 deletions lms/djangoapps/courseware/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from external_auth.models import ExternalAuthMap
from courseware.masquerade import is_masquerading_as_student
from django.utils.timezone import UTC
from student import auth
from student.roles import (
GlobalStaff, CourseStaffRole, CourseInstructorRole,
OrgStaffRole, OrgInstructorRole, CourseBetaTesterRole
Expand Down Expand Up @@ -46,6 +47,7 @@ def has_access(user, action, obj, course_key=None):
- visible_to_staff_only for modules
- DISABLE_START_DATES
- different access for instructor, staff, course staff, and students.
- mobile_available flag for course modules

user: a Django user object. May be anonymous. If none is passed,
anonymous is assumed
Expand Down Expand Up @@ -108,6 +110,8 @@ def _has_access_course_desc(user, action, course):

'load' -- load the courseware, see inside the course
'load_forum' -- can load and contribute to the forums (one access level for now)
'load_mobile' -- can load from a mobile context
'load_mobile_no_enrollment_check' -- can load from a mobile context without checking for enrollment
'enroll' -- enroll. Checks for enrollment window,
ACCESS_REQUIRE_STAFF_FOR_COURSE,
'see_exists' -- can see that the course exists.
Expand Down Expand Up @@ -136,6 +140,36 @@ def can_load_forum():
)
)

def can_load_mobile():
"""
Can this user access this course from a mobile device?
"""
return (
# check mobile requirements
can_load_mobile_no_enroll_check() and
# check enrollment
(
CourseEnrollment.is_enrolled(user, course.id) or
_has_staff_access_to_descriptor(user, course, course.id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there's a way we can make the names more obvious. Since it's not just enrollment, it's also beta users though IIRC I had the same problem and didn't come up with anything good.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The can_load_mobile method checks enrollment in addition to the other checks.
The can_load_mobile_not_enrolled method does not check enrollment, but checks release date, beta user, and eventually cohorted content (once supported).

How about can_load_mobile and can_load_mobile_no_enrollment_check?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed load_mobile_not_enrolled -> load_mobile_no_enrollment_check in latest commit.

)
)

def can_load_mobile_no_enroll_check():
"""
Can this enrolled user access this course from a mobile device?
Note: does not check for enrollment since it is assumed the caller has done so.
"""
return (
# check start date
can_load() and
# check mobile_available flag
(
course.mobile_available or
auth.has_access(user, CourseBetaTesterRole(course.id)) or
_has_staff_access_to_descriptor(user, course, course.id)
)
)

def can_enroll():
"""
First check if restriction of enrollment by login method is enabled, both
Expand Down Expand Up @@ -234,6 +268,8 @@ def can_see_about_page():
checkers = {
'load': can_load,
'load_forum': can_load_forum,
'load_mobile': can_load_mobile,
'load_mobile_no_enrollment_check': can_load_mobile_no_enroll_check,
'enroll': can_enroll,
'see_exists': see_exists,
'staff': lambda: _has_staff_access_to_descriptor(user, course, course.id),
Expand Down
120 changes: 61 additions & 59 deletions lms/djangoapps/mobile_api/course_info/tests.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,61 @@
"""
Tests for course_info
"""
import json

from django.conf import settings
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase

from courseware.tests.factories import UserFactory
from xmodule.html_module import CourseInfoModule
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.xml_importer import import_from_xml

from ..testutils import (
MobileAPITestCase, MobileCourseAccessTestMixin, MobileEnrolledCourseAccessTestMixin, MobileAuthTestMixin
)

class TestCourseInfo(APITestCase):

class TestAbout(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin):
"""
Tests for /api/mobile/v0.5/course_info/...
Tests for /api/mobile/v0.5/course_info/{course_id}/about
"""
def setUp(self):
super(TestCourseInfo, self).setUp()
self.user = UserFactory.create()
self.course = CourseFactory.create(mobile_available=True)
self.client.login(username=self.user.username, password='test')

def test_about(self):
url = reverse('course-about-detail', kwargs={'course_id': unicode(self.course.id)})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTrue('overview' in response.data) # pylint: disable=maybe-no-member

def test_updates(self):
url = reverse('course-updates-list', kwargs={'course_id': unicode(self.course.id)})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, []) # pylint: disable=maybe-no-member

def test_about_static_rewrites(self):
REVERSE_INFO = {'name': 'course-about-detail', 'params': ['course_id']}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the idea behind an empty base class? Just clearer naming?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. It's so all subclasses can share a common hierarchy and implicitly inherit from MobileCourseAPITestCase.
Also, I was thinking there may be common setUp required, but eventually didn't need it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed it in my latest commit.

def verify_success(self, response):
super(TestAbout, self).verify_success(response)
self.assertTrue('overview' in response.data)

def init_course_access(self, course_id=None):
# override this method since enrollment is not required for the About endpoint.
self.login()

def test_about_static_rewrite(self):
self.login()

about_usage_key = self.course.id.make_usage_key('about', 'overview')
about_module = modulestore().get_item(about_usage_key)
underlying_about_html = about_module.data

# check that we start with relative static assets
self.assertIn('\"/static/', underlying_about_html)

url = reverse('course-about-detail', kwargs={'course_id': unicode(self.course.id)})
response = self.client.get(url)
json_data = json.loads(response.content)
about_html = json_data['overview']

# but shouldn't finish with any
self.assertEqual(response.status_code, 200)
self.assertNotIn('\"/static/', about_html)
response = self.api_response()
self.assertNotIn('\"/static/', response.data['overview'])


class TestUpdates(MobileAPITestCase, MobileAuthTestMixin, MobileEnrolledCourseAccessTestMixin):
"""
Tests for /api/mobile/v0.5/course_info/{course_id}/updates
"""
REVERSE_INFO = {'name': 'course-updates-list', 'params': ['course_id']}

def verify_success(self, response):
super(TestUpdates, self).verify_success(response)
self.assertEqual(response.data, [])

def test_updates_static_rewrite(self):
self.login_and_enroll()

def test_updates_rewrite(self):
updates_usage_key = self.course.id.make_usage_key('course_info', 'updates')
course_updates = modulestore().create_item(
self.user.id,
Expand All @@ -72,50 +73,51 @@ def test_updates_rewrite(self):
course_updates.items = [course_update_data]
modulestore().update_item(course_updates, self.user.id)

url = reverse('course-updates-list', kwargs={'course_id': unicode(self.course.id)})
response = self.client.get(url)
response = self.api_response()
content = response.data[0]["content"] # pylint: disable=maybe-no-member
self.assertEqual(response.status_code, 200)
self.assertNotIn("\"/static/", content)

underlying_updates_module = modulestore().get_item(updates_usage_key)
self.assertIn("\"/static/", underlying_updates_module.items[0]['content'])


class TestHandoutInfo(ModuleStoreTestCase, APITestCase):
class TestHandouts(MobileAPITestCase, MobileAuthTestMixin, MobileEnrolledCourseAccessTestMixin):
"""
Tests for /api/mobile/v0.5/course_info/{course_id}/handouts
"""
REVERSE_INFO = {'name': 'course-handouts-list', 'params': ['course_id']}

def setUp(self):
super(TestHandoutInfo, self).setUp()
self.user = UserFactory.create()
self.client.login(username=self.user.username, password='test')
super(TestHandouts, self).setUp()

# use toy course with handouts, and make it mobile_available
course_items = import_from_xml(self.store, self.user.id, settings.COMMON_TEST_DATA_ROOT, ['toy'])
self.course = course_items[0]
self.course.mobile_available = True
self.store.update_item(self.course, self.user.id)

def verify_success(self, response):
super(TestHandouts, self).verify_success(response)
self.assertIn('Sample', response.data['handouts_html'])

def test_no_handouts(self):
empty_course = CourseFactory.create(mobile_available=True)
url = reverse('course-handouts-list', kwargs={'course_id': unicode(empty_course.id)})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
self.login_and_enroll()

def test_handout_exists(self):
url = reverse('course-handouts-list', kwargs={'course_id': unicode(self.course.id)})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# delete handouts in course
handouts_usage_key = self.course.id.make_usage_key('course_info', 'handouts')
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
self.store.delete_item(handouts_usage_key, self.user.id)

self.api_response(expected_response_code=404)

def test_handouts_static_rewrites(self):
self.login_and_enroll()

def test_handout_static_rewrites(self):
# check that we start with relative static assets
handouts_usage_key = self.course.id.make_usage_key('course_info', 'handouts')
underlying_handouts = self.store.get_item(handouts_usage_key)
self.assertIn('\'/static/', underlying_handouts.data)

url = reverse('course-handouts-list', kwargs={'course_id': unicode(self.course.id)})
response = self.client.get(url)

json_data = json.loads(response.content)
handouts_html = json_data['handouts_html']

# but shouldn't finish with any
self.assertNotIn('\'/static/', handouts_html)
self.assertEqual(response.status_code, 200)
response = self.api_response()
self.assertNotIn('\'/static/', response.data['handouts_html'])
40 changes: 15 additions & 25 deletions lms/djangoapps/mobile_api/course_info/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@
Views for course info API
"""
from django.http import Http404
from rest_framework import generics, permissions
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
from rest_framework import generics
from rest_framework.response import Response

from courseware.courses import get_course_about_section, get_course_info_section_module
from opaque_keys.edx.keys import CourseKey

from xmodule.modulestore.django import modulestore
from static_replace import make_static_urls_absolute, replace_static_urls

from ..utils import MobileView, mobile_course_access


@MobileView()
class CourseUpdatesList(generics.ListAPIView):
"""
**Use Case**
Expand All @@ -35,12 +34,9 @@ class CourseUpdatesList(generics.ListAPIView):

* id: The unique identifier of the update.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)

def list(self, request, *args, **kwargs):
course_id = CourseKey.from_string(kwargs['course_id'])
course = modulestore().get_course(course_id)
@mobile_course_access()
def list(self, request, course, *args, **kwargs):
course_updates_module = get_course_info_section_module(request, course, 'updates')
update_items = reversed(getattr(course_updates_module, 'items', []))

Expand All @@ -53,13 +49,14 @@ def list(self, request, *args, **kwargs):
content = item['content']
content = replace_static_urls(
content,
course_id=course_id,
course_id=course.id,
static_asset_path=course.static_asset_path)
item['content'] = make_static_urls_absolute(request, content)

return Response(updates_to_show)


@MobileView()
class CourseHandoutsList(generics.ListAPIView):
"""
**Use Case**
Expand All @@ -74,27 +71,24 @@ class CourseHandoutsList(generics.ListAPIView):

* handouts_html: The HTML for course handouts.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)

def list(self, request, *args, **kwargs):
course_id = CourseKey.from_string(kwargs['course_id'])
course = modulestore().get_course(course_id)
@mobile_course_access()
def list(self, request, course, *args, **kwargs):
course_handouts_module = get_course_info_section_module(request, course, 'handouts')
if course_handouts_module:
handouts_html = course_handouts_module.data
handouts_html = replace_static_urls(
handouts_html,
course_id=course_id,
course_id=course.id,
static_asset_path=course.static_asset_path)
handouts_html = make_static_urls_absolute(self.request, handouts_html)
return Response({'handouts_html': handouts_html})
else:
# course_handouts_module could be None if there are no handouts
# (such as while running tests)
raise Http404(u"No handouts for {}".format(unicode(course_id)))
raise Http404(u"No handouts for {}".format(unicode(course.id)))


@MobileView()
class CourseAboutDetail(generics.RetrieveAPIView):
"""
**Use Case**
Expand All @@ -109,13 +103,9 @@ class CourseAboutDetail(generics.RetrieveAPIView):

* overview: The HTML for the course About page.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (permissions.IsAuthenticated,)

def get(self, request, *args, **kwargs):
course_id = CourseKey.from_string(kwargs['course_id'])
course = modulestore().get_course(course_id)

@mobile_course_access(verify_enrolled=False)
def get(self, request, course, *args, **kwargs):
# There are other fields, but they don't seem to be in use.
# see courses.py:get_course_about_section.
#
Expand Down
Loading