diff --git a/lms/djangoapps/mobile_api/course_info/tests.py b/lms/djangoapps/mobile_api/course_info/tests.py new file mode 100644 index 0000000000..d7a1440228 --- /dev/null +++ b/lms/djangoapps/mobile_api/course_info/tests.py @@ -0,0 +1,37 @@ +""" +Tests for course_info +""" +from django.test.utils import override_settings +from django.core.urlresolvers import reverse +from rest_framework.test import APITestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from courseware.tests.factories import UserFactory +from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestVideoOutline(ModuleStoreTestCase, APITestCase): + def setUp(self): + super(TestVideoOutline, 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) + + def test_handouts(self): + url = reverse('course-handouts-list', kwargs={'course_id': unicode(self.course.id)}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + 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, []) + # TODO: add handouts and updates, somehow diff --git a/lms/djangoapps/mobile_api/course_info/views.py b/lms/djangoapps/mobile_api/course_info/views.py index 2b36baddd8..2b83401c38 100644 --- a/lms/djangoapps/mobile_api/course_info/views.py +++ b/lms/djangoapps/mobile_api/course_info/views.py @@ -1,3 +1,7 @@ +""" +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.response import Response @@ -25,9 +29,8 @@ class CourseUpdatesList(generics.ListAPIView): course_id = CourseKey.from_string(kwargs['course_id']) course = modulestore().get_course(course_id) course_updates_module = get_course_info_section_module(request, course, 'updates') - updates_to_show = [ - update for update in reversed(course_updates_module.items) + update for update in reversed(getattr(course_updates_module, 'items', [])) if update.get("status") != "deleted" ] return Response(updates_to_show) @@ -43,7 +46,12 @@ class CourseHandoutsList(generics.ListAPIView): course_id = CourseKey.from_string(kwargs['course_id']) course = modulestore().get_course(course_id) course_handouts_module = get_course_info_section_module(request, course, 'handouts') - return Response({'handouts_html': course_handouts_module.data}) + if course_handouts_module: + return Response({'handouts_html': course_handouts_module.data}) + 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))) class CourseAboutDetail(generics.RetrieveAPIView): diff --git a/lms/djangoapps/mobile_api/users/tests.py b/lms/djangoapps/mobile_api/users/tests.py new file mode 100644 index 0000000000..53d6e1a5eb --- /dev/null +++ b/lms/djangoapps/mobile_api/users/tests.py @@ -0,0 +1,92 @@ +""" +Tests for users API +""" +from rest_framework.test import APITestCase +from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from courseware.tests.factories import StaffFactory, UserFactory +from django.core.urlresolvers import reverse +from mobile_api.users.serializers import CourseEnrollmentSerializer +from student.models import CourseEnrollment + + +class TestUserApi(ModuleStoreTestCase, APITestCase): + """ + Test the user info API + """ + def setUp(self): + super(TestUserApi, self).setUp() + self.course = CourseFactory.create(mobile_available=True) + self.user = UserFactory.create() + self.password = 'test' + self.username = self.user.username + + def tearDown(self): + super(TestUserApi, self).tearDown() + self.client.logout() + + def enroll(self): + resp = self.client.post(reverse('change_enrollment'), { + 'enrollment_action': 'enroll', + 'course_id': self.course.id.to_deprecated_string(), + 'check_access': True, + }) + self.assertEqual(resp.status_code, 200) + + def test_user_enrollments(self): + url = reverse('courseenrollment-detail', kwargs={'username': self.user.username}) + + self.client.login(username=self.username, password=self.password) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, []) + + self.enroll() + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + courses = response.data + + self.assertTrue(len(courses), 1) + course = courses[0]['course'] + self.assertTrue('video_outline' in course) + self.assertTrue('course_handouts' in course) + self.assertEqual(course['id'], self.course.id.to_deprecated_string()) + self.assertEqual(courses[0]['mode'], 'honor') + + def test_user_overview(self): + self.client.login(username=self.username, password=self.password) + url = reverse('user-detail', kwargs={'username': self.user.username}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + data = response.data + self.assertEqual(data['username'], self.user.username) + self.assertEqual(data['email'], self.user.email) + + def test_overview_anon(self): + # anonymous disallowed + url = reverse('user-detail', kwargs={'username': self.user.username}) + response = self.client.get(url) + self.assertEqual(response.status_code, 401) + # can't get info on someone else + other = UserFactory.create() + self.client.login(username=self.username, password=self.password) + response = self.client.get(reverse('user-detail', kwargs={'username': other.username})) + self.assertEqual(response.status_code, 403) + + def test_redirect_userinfo(self): + url = '/api/mobile/v0.5/my_user_info' + response = self.client.get(url) + self.assertEqual(response.status_code, 401) + + self.client.login(username=self.username, password=self.password) + response = self.client.get(url) + self.assertEqual(response.status_code, 302) + self.assertTrue(self.username in response['location']) + + def test_course_serializer(self): + self.client.login(username=self.username, password=self.password) + self.enroll() + serialized = CourseEnrollmentSerializer(CourseEnrollment.enrollments_for_user(self.user)[0]).data + self.assertEqual(serialized['course']['video_outline'], None) + self.assertEqual(serialized['course']['name'], self.course.display_name) diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index 7a2f6fe40b..38404db8d3 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -51,19 +51,11 @@ class UserCourseEnrollmentsList(generics.ListAPIView): ).order_by('created') return mobile_course_enrollments(qset, self.request.user) - def get(self, request, *args, **kwargs): - if request.user.username != kwargs['username']: - raise PermissionDenied - - return super(UserCourseEnrollmentsList, self).get(self, request, *args, **kwargs) - @api_view(["GET"]) @authentication_classes((OAuth2Authentication, SessionAuthentication)) @permission_classes((IsAuthenticated,)) def my_user_info(request): - if not request.user: - raise PermissionDenied return redirect("user-detail", username=request.user.username) def mobile_course_enrollments(enrollments, user): diff --git a/lms/djangoapps/mobile_api/video_outlines/serializers.py b/lms/djangoapps/mobile_api/video_outlines/serializers.py index d384435f3c..676889e244 100644 --- a/lms/djangoapps/mobile_api/video_outlines/serializers.py +++ b/lms/djangoapps/mobile_api/video_outlines/serializers.py @@ -20,7 +20,7 @@ class BlockOutline(object): self.local_cache['course_videos'] = get_video_info_for_course_and_profile( unicode(course_id), "mobile_low" ) - except ValInternalError: + except ValInternalError: # pragma: nocover self.local_cache['course_videos'] = {} def __iter__(self): @@ -114,6 +114,7 @@ def video_summary(course, course_id, video_descriptor, request, local_cache): # Transcripts... transcript_langs = video_descriptor.available_translations(verify_assets=False) + transcripts = { lang: reverse( 'video-transcripts-detail', diff --git a/lms/djangoapps/mobile_api/video_outlines/tests.py b/lms/djangoapps/mobile_api/video_outlines/tests.py new file mode 100644 index 0000000000..5bf15fcc6e --- /dev/null +++ b/lms/djangoapps/mobile_api/video_outlines/tests.py @@ -0,0 +1,172 @@ +""" +Tests for video outline API +""" +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.video_module import transcripts_utils +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from courseware.tests.factories import UserFactory +from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from django.core.urlresolvers import reverse +from django.test.utils import override_settings +from django.conf import settings +from rest_framework.test import APITestCase +from edxval import api +from uuid import uuid4 +import copy + +TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) +TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, CONTENTSTORE=TEST_DATA_CONTENTSTORE) +class TestVideoOutline(ModuleStoreTestCase, APITestCase): + def setUp(self): + super(TestVideoOutline, self).setUp() + self.user = UserFactory.create() + self.course = CourseFactory.create(mobile_available=True) + section = ItemFactory.create( + parent_location=self.course.location, + category="chapter", + display_name=u"test factory section omega \u03a9", + ) + self.sub_section = ItemFactory.create( + parent_location=section.location, + category="sequential", + display_name=u"test subsection omega \u03a9", + ) + + self.unit = ItemFactory.create( + parent_location=self.sub_section.location, + category="vertical", + metadata={'graded': True, 'format': 'Homework'}, + display_name=u"test unit omega \u03a9", + ) + self.other_unit = ItemFactory.create( + parent_location=self.sub_section.location, + category="vertical", + metadata={'graded': True, 'format': 'Homework'}, + display_name=u"test unit omega 2 \u03a9", + ) + + self.edx_video_id = 'testing-123' + + self.video_url = 'http://val.edx.org/val/video.mp4' + self.html5_video_url = 'http://video.edx.org/html5/video.mp4' + + api.create_profile({ + 'profile_name': 'youtube', + 'extension': 'mp4', + 'width': 1280, + 'height': 720 + }) + api.create_profile({ + 'profile_name': 'mobile_low', + 'extension': 'mp4', + 'width': 640, + 'height': 480 + }) + + val_video = api.create_video({ + 'edx_video_id': self.edx_video_id, + 'client_video_id': u"test video omega \u03a9", + 'duration': 12, + 'courses': [unicode(self.course.id)], + 'encoded_videos': [ + { + 'profile': 'youtube', + 'url': 'xyz123', + 'file_size': 0, + 'bitrate': 1500 + }, + { + 'profile': 'mobile_low', + 'url': self.video_url, + 'file_size': 12345, + 'bitrate': 250 + } + ]}) + + subid = uuid4().hex + self.video = ItemFactory.create( + parent_location=self.unit.location, + category="video", + edx_video_id=self.edx_video_id, + display_name=u"test video omega \u03a9", + sub=subid + ) + + result_location = transcripts_utils.save_subs_to_store({ + 'start': [100, 200, 240, 390, 1000], + 'end': [200, 240, 380, 1000, 1500], + 'text': [ + 'subs #1', + 'subs #2', + 'subs #3', + 'subs #4', + 'subs #5' + ]}, + subid, + self.course) + + self.client.login(username=self.user.username, password='test') + + def test_course_not_available(self): + nonmobile = CourseFactory.create() + url = reverse('video-summary-list', kwargs={'course_id': unicode(nonmobile.id)}) + response = self.client.get(url) + self.assertEqual(response.status_code, 403) + + def test_course_list(self): + second_video = ItemFactory.create( + parent_location=self.other_unit.location, + category="video", + display_name=u"test video omega 2 \u03a9", + html5_sources=[self.html5_video_url] + ) + third_video = ItemFactory.create( + parent_location=self.other_unit.location, + category="video", + display_name=u"test video omega 3 \u03a9", + source=self.html5_video_url + ) + + draft_video = ItemFactory.create( + parent_location=self.unit.location, + category="video", + edx_video_id=self.edx_video_id, + display_name=u"test draft video omega \u03a9", + visible_to_staff_only=True, + ) + + url = reverse('video-summary-list', kwargs={'course_id': unicode(self.course.id)}) + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + course_outline = response.data + self.assertEqual(len(course_outline), 3) + vid = course_outline[0] + self.assertTrue('test_subsection_omega_%CE%A9' in vid['section_url']) + self.assertTrue('test_subsection_omega_%CE%A9/1' in vid['unit_url']) + self.assertTrue(u'test_video_omega_\u03a9' in vid['summary']['id']) + self.assertEqual(vid['summary']['video_url'], self.video_url) + self.assertEqual(vid['summary']['size'], 12345) + self.assertTrue('en' in vid['summary']['transcripts']) + self.assertEqual(course_outline[1]['summary']['video_url'], self.html5_video_url) + self.assertEqual(course_outline[1]['summary']['size'], 0) + + self.assertEqual(course_outline[2]['summary']['video_url'], self.html5_video_url) + self.assertEqual(course_outline[2]['summary']['size'], 0) + + def test_transcripts(self): + kwargs = { + 'course_id': unicode(self.course.id), + 'block_id': unicode(self.video.scope_ids.usage_id.block_id), + 'lang': 'pl' + } + url = reverse('video-transcripts-detail', kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + kwargs['lang'] = 'en' + url = reverse('video-transcripts-detail', kwargs=kwargs) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) diff --git a/lms/djangoapps/mobile_api/video_outlines/views.py b/lms/djangoapps/mobile_api/video_outlines/views.py index 7606c1c231..34251df8b9 100644 --- a/lms/djangoapps/mobile_api/video_outlines/views.py +++ b/lms/djangoapps/mobile_api/video_outlines/views.py @@ -15,6 +15,7 @@ from rest_framework import generics, permissions from rest_framework.authentication import OAuth2Authentication, SessionAuthentication from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.exceptions import PermissionDenied from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import BlockUsageLocator @@ -66,7 +67,7 @@ class VideoTranscripts(generics.RetrieveAPIView): video_descriptor = modulestore().get_item(usage_key) content, filename, mimetype = video_descriptor.get_transcript(lang=lang) except (NotFoundError, ValueError, KeyError): - raise Http404("Transcript not found for {}, lang: {}".format(block_id, lang)) + raise Http404(u"Transcript not found for {}, lang: {}".format(block_id, lang)) response = HttpResponse(content, content_type=mimetype) response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)