Commit a57f0d74 by Christopher Lee

Merge pull request #7794 from edx/clee/gated_content_mobile_api

MA-635 Block Mobile Content for unfulfilled milestones
parents 3e247cd6 5cef287c
...@@ -7,7 +7,8 @@ from django.conf import settings ...@@ -7,7 +7,8 @@ from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
NAMESPACE_CHOICES = { NAMESPACE_CHOICES = {
...@@ -26,7 +27,7 @@ def add_prerequisite_course(course_key, prerequisite_course_key): ...@@ -26,7 +27,7 @@ def add_prerequisite_course(course_key, prerequisite_course_key):
""" """
It would create a milestone, then it would set newly created It would create a milestone, then it would set newly created
milestones as requirement for course referred by `course_key` milestones as requirement for course referred by `course_key`
and it would set newly created milestone as fulfilment and it would set newly created milestone as fulfillment
milestone for course referred by `prerequisite_course_key`. milestone for course referred by `prerequisite_course_key`.
""" """
if not settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False): if not settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False):
...@@ -313,6 +314,15 @@ def remove_content_references(content_id): ...@@ -313,6 +314,15 @@ def remove_content_references(content_id):
return milestones_api.remove_content_references(content_id) return milestones_api.remove_content_references(content_id)
def any_unfulfilled_milestones(course_id, user_id):
""" Returns a boolean if user has any unfulfilled milestones """
if not settings.FEATURES.get('MILESTONES_APP', False):
return False
return bool(
get_course_milestones_fulfillment_paths(course_id, {"id": user_id})
)
def get_course_milestones_fulfillment_paths(course_id, user_id): def get_course_milestones_fulfillment_paths(course_id, user_id):
""" """
Client API operation adapter/wrapper Client API operation adapter/wrapper
......
...@@ -3,6 +3,8 @@ Tests for the milestones helpers library, which is the integration point for the ...@@ -3,6 +3,8 @@ Tests for the milestones helpers library, which is the integration point for the
""" """
from mock import patch from mock import patch
from milestones.exceptions import InvalidCourseKeyException, InvalidUserException
from util import milestones_helpers from util import milestones_helpers
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
...@@ -85,3 +87,11 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase): ...@@ -85,3 +87,11 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase):
def test_add_user_milestone_returns_none_when_app_disabled(self): def test_add_user_milestone_returns_none_when_app_disabled(self):
response = milestones_helpers.add_user_milestone(self.user, self.milestone) response = milestones_helpers.add_user_milestone(self.user, self.milestone)
self.assertIsNone(response) self.assertIsNone(response)
@patch.dict('django.conf.settings.FEATURES', {'MILESTONES_APP': True})
def test_any_unfulfilled_milestones(self):
""" Tests any_unfulfilled_milestones for invalid arguments """
with self.assertRaises(InvalidCourseKeyException):
milestones_helpers.any_unfulfilled_milestones(None, self.user)
with self.assertRaises(InvalidUserException):
milestones_helpers.any_unfulfilled_milestones(self.course.id, None)
...@@ -32,7 +32,10 @@ from student.roles import ( ...@@ -32,7 +32,10 @@ from student.roles import (
GlobalStaff, CourseStaffRole, CourseInstructorRole, GlobalStaff, CourseStaffRole, CourseInstructorRole,
OrgStaffRole, OrgInstructorRole, CourseBetaTesterRole OrgStaffRole, OrgInstructorRole, CourseBetaTesterRole
) )
from util.milestones_helpers import get_pre_requisite_courses_not_completed from util.milestones_helpers import (
get_pre_requisite_courses_not_completed,
any_unfulfilled_milestones,
)
import dogstats_wrapper as dog_stats_api import dogstats_wrapper as dog_stats_api
...@@ -173,7 +176,12 @@ def _has_access_course_desc(user, action, course): ...@@ -173,7 +176,12 @@ def _has_access_course_desc(user, action, course):
# check start date # check start date
can_load() and can_load() and
# check mobile_available flag # check mobile_available flag
is_mobile_available_for_user(user, course) is_mobile_available_for_user(user, course) and
# check staff access, if not then check for unfulfilled milestones
(
_has_staff_access_to_descriptor(user, course, course.id) or
not any_unfulfilled_milestones(course.id, user.id)
)
) )
def can_enroll(): def can_enroll():
......
...@@ -2,16 +2,14 @@ import json ...@@ -2,16 +2,14 @@ import json
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from student.models import Registration from student.models import Registration
from django.test import TestCase
def get_request_for_user(user): def get_request_for_user(user):
"""Create a request object for user.""" """Create a request object for user."""
request = RequestFactory() request = RequestFactory()
request.user = user request.user = user
request.COOKIES = {} request.COOKIES = {}
......
""" """
Tests use cases related to LMS Entrance Exam behavior, such as gated content access (TOC) Tests use cases related to LMS Entrance Exam behavior, such as gated content access (TOC)
""" """
from django.conf import settings from mock import patch, Mock
from django.test.client import RequestFactory
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from courseware.module_render import get_module, toc_for_course from courseware.module_render import toc_for_course, get_module
from courseware.tests.factories import UserFactory, InstructorFactory, StaffFactory from courseware.tests.factories import UserFactory, InstructorFactory, StaffFactory
from courseware.tests.helpers import LoginEnrollmentTestCase from courseware.tests.helpers import (
LoginEnrollmentTestCase,
get_request_for_user
)
from courseware.entrance_exams import ( from courseware.entrance_exams import (
course_has_entrance_exam, course_has_entrance_exam,
get_entrance_exam_content, get_entrance_exam_content,
...@@ -17,9 +20,8 @@ from courseware.entrance_exams import ( ...@@ -17,9 +20,8 @@ from courseware.entrance_exams import (
user_can_skip_entrance_exam, user_can_skip_entrance_exam,
user_has_passed_entrance_exam, user_has_passed_entrance_exam,
) )
from xmodule.modulestore.django import modulestore from student.models import CourseEnrollment
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from student.tests.factories import CourseEnrollmentFactory, AnonymousUserFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from util.milestones_helpers import ( from util.milestones_helpers import (
add_milestone, add_milestone,
add_course_milestone, add_course_milestone,
...@@ -29,20 +31,21 @@ from util.milestones_helpers import ( ...@@ -29,20 +31,21 @@ from util.milestones_helpers import (
get_milestone_relationship_types, get_milestone_relationship_types,
seed_milestone_relationship_types, seed_milestone_relationship_types,
) )
from student.models import CourseEnrollment from xmodule.modulestore.django import modulestore
from student.tests.factories import CourseEnrollmentFactory, AnonymousUserFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from mock import patch, Mock from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
import mock
@attr('shard_1') @attr('shard_1')
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True, 'MILESTONES_APP': True})
class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
""" """
Check that content is properly gated. Create a test course from scratch to mess with. Check that content is properly gated.
We typically assume that the Entrance Exam feature flag is set to True in test.py
However, the tests below are designed to execute workflows regardless of the setting Creates a test course from scratch. The tests below are designed to execute
If set to False, we are essentially confirming that the workflows do not cause exceptions workflows regardless of the feature flag settings.
""" """
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True, 'MILESTONES_APP': True})
def setUp(self): def setUp(self):
""" """
Test case scaffolding Test case scaffolding
...@@ -122,54 +125,17 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -122,54 +125,17 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
category="problem", category="problem",
display_name="Exam Problem - Problem 2" display_name="Exam Problem - Problem 2"
) )
self.problem_3 = ItemFactory.create(
parent=subsection, seed_milestone_relationship_types()
category="problem", add_entrance_exam_milestone(self.course, self.entrance_exam)
display_name="Exam Problem - Problem 3"
)
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
namespace_choices = get_namespace_choices()
milestone_namespace = generate_milestone_namespace(
namespace_choices.get('ENTRANCE_EXAM'),
self.course.id
)
self.milestone = {
'name': 'Test Milestone',
'namespace': milestone_namespace,
'description': 'Testing Courseware Entrance Exam Chapter',
}
seed_milestone_relationship_types()
self.milestone_relationship_types = get_milestone_relationship_types()
self.milestone = add_milestone(self.milestone)
add_course_milestone(
unicode(self.course.id),
self.milestone_relationship_types['REQUIRES'],
self.milestone
)
add_course_content_milestone(
unicode(self.course.id),
unicode(self.entrance_exam.location),
self.milestone_relationship_types['FULFILLS'],
self.milestone
)
self.anonymous_user = AnonymousUserFactory()
user = UserFactory()
self.request = RequestFactory()
self.request.user = user
self.request.COOKIES = {}
self.request.META = {}
self.request.is_secure = lambda: True
self.request.get_host = lambda: "edx.org"
self.request.method = 'GET'
self.field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course.id,
user,
self.entrance_exam
)
self.course.entrance_exam_enabled = True self.course.entrance_exam_enabled = True
self.course.entrance_exam_minimum_score_pct = 0.50 self.course.entrance_exam_minimum_score_pct = 0.50
self.course.entrance_exam_id = unicode(self.entrance_exam.scope_ids.usage_id) self.course.entrance_exam_id = unicode(self.entrance_exam.scope_ids.usage_id)
modulestore().update_item(self.course, user.id) # pylint: disable=no-member
self.anonymous_user = AnonymousUserFactory()
self.request = get_request_for_user(UserFactory())
modulestore().update_item(self.course, self.request.user.id) # pylint: disable=no-member
self.client.login(username=self.request.user.username, password="test") self.client.login(username=self.request.user.username, password="test")
CourseEnrollment.enroll(self.request.user, self.course.id) CourseEnrollment.enroll(self.request.user, self.course.id)
...@@ -260,8 +226,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -260,8 +226,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
'section': self.exam_1.location.name 'section': self.exam_1.location.name
}) })
resp = self.client.get(url) resp = self.client.get(url)
if settings.FEATURES.get('ENTRANCE_EXAMS', False): self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': False}) @patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': False})
def test_entrance_exam_content_absence(self): def test_entrance_exam_content_absence(self):
...@@ -294,53 +259,26 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -294,53 +259,26 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
'section': self.exam_1.location.name 'section': self.exam_1.location.name
}) })
resp = self.client.get(url) resp = self.client.get(url)
if settings.FEATURES.get('ENTRANCE_EXAMS', False): self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200) resp = self.client.get(expected_url)
resp = self.client.get(expected_url) self.assertIn('Exam Problem - Problem 1', resp.content)
self.assertIn('Exam Problem - Problem 1', resp.content) self.assertIn('Exam Problem - Problem 2', resp.content)
self.assertIn('Exam Problem - Problem 2', resp.content)
def test_get_entrance_exam_content(self): def test_get_entrance_exam_content(self):
""" """
test get entrance exam content method test get entrance exam content method
""" """
exam_chapter = get_entrance_exam_content(self.request, self.course) exam_chapter = get_entrance_exam_content(self.request, self.course)
if settings.FEATURES.get('ENTRANCE_EXAMS', False): self.assertEqual(exam_chapter.url_name, self.entrance_exam.url_name)
self.assertEqual(exam_chapter.url_name, self.entrance_exam.url_name) self.assertFalse(user_has_passed_entrance_exam(self.request, self.course))
self.assertFalse(user_has_passed_entrance_exam(self.request, self.course))
answer_entrance_exam_problem(self.course, self.request, self.problem_1)
# Pass the entrance exam answer_entrance_exam_problem(self.course, self.request, self.problem_2)
# pylint: disable=maybe-no-member,no-member
grade_dict = {'value': 1, 'max_value': 1, 'user_id': self.request.user.id} exam_chapter = get_entrance_exam_content(self.request, self.course)
field_data_cache = FieldDataCache.cache_for_descriptor_descendents( self.assertEqual(exam_chapter, None)
self.course.id, self.assertTrue(user_has_passed_entrance_exam(self.request, self.course))
self.request.user,
self.course,
depth=2
)
# pylint: disable=protected-access
module = get_module(
self.request.user,
self.request,
self.problem_1.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_1, 'grade', grade_dict)
# pylint: disable=protected-access
module = get_module(
self.request.user,
self.request,
self.problem_2.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_2, 'grade', grade_dict)
exam_chapter = get_entrance_exam_content(self.request, self.course)
self.assertEqual(exam_chapter, None)
self.assertTrue(user_has_passed_entrance_exam(self.request, self.course))
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
def test_entrance_exam_score(self): def test_entrance_exam_score(self):
""" """
test entrance exam score. we will hit the method get_entrance_exam_score to verify exam score. test entrance exam score. we will hit the method get_entrance_exam_score to verify exam score.
...@@ -348,32 +286,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -348,32 +286,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
exam_score = get_entrance_exam_score(self.request, self.course) exam_score = get_entrance_exam_score(self.request, self.course)
self.assertEqual(exam_score, 0) self.assertEqual(exam_score, 0)
# Pass the entrance exam answer_entrance_exam_problem(self.course, self.request, self.problem_1)
# pylint: disable=maybe-no-member,no-member answer_entrance_exam_problem(self.course, self.request, self.problem_2)
grade_dict = {'value': 1, 'max_value': 1, 'user_id': self.request.user.id}
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course.id,
self.request.user,
self.course,
depth=2
)
# pylint: disable=protected-access
module = get_module(
self.request.user,
self.request,
self.problem_1.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_1, 'grade', grade_dict)
# pylint: disable=protected-access
module = get_module(
self.request.user,
self.request,
self.problem_2.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_2, 'grade', grade_dict)
exam_score = get_entrance_exam_score(self.request, self.course) exam_score = get_entrance_exam_score(self.request, self.course)
# 50 percent exam score should be achieved. # 50 percent exam score should be achieved.
...@@ -392,9 +306,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -392,9 +306,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
} }
) )
resp = self.client.get(url) resp = self.client.get(url)
if settings.FEATURES.get('ENTRANCE_EXAMS', False): self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.status_code, 200) self.assertIn('To access course materials, you must score', resp.content)
self.assertIn('To access course materials, you must score', resp.content)
def test_entrance_exam_requirement_message_hidden(self): def test_entrance_exam_requirement_message_hidden(self):
""" """
...@@ -416,9 +329,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -416,9 +329,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
) )
resp = self.client.get(url) resp = self.client.get(url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
if settings.FEATURES.get('ENTRANCE_EXAMS', False): self.assertNotIn('To access course materials, you must score', resp.content)
self.assertNotIn('To access course materials, you must score', resp.content) self.assertNotIn('You have passed the entrance exam.', resp.content)
self.assertNotIn('You have passed the entrance exam.', resp.content)
def test_entrance_exam_passed_message_and_course_content(self): def test_entrance_exam_passed_message_and_course_content(self):
""" """
...@@ -434,121 +346,40 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -434,121 +346,40 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
} }
) )
# pylint: disable=maybe-no-member,no-member answer_entrance_exam_problem(self.course, self.request, self.problem_1)
grade_dict = {'value': 1, 'max_value': 1, 'user_id': self.request.user.id} answer_entrance_exam_problem(self.course, self.request, self.problem_2)
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course.id,
self.request.user,
self.course,
depth=2
)
# pylint: disable=protected-access
module = get_module(
self.request.user,
self.request,
self.problem_1.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_1, 'grade', grade_dict)
# pylint: disable=protected-access
module = get_module(
self.request.user,
self.request,
self.problem_2.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_2, 'grade', grade_dict)
resp = self.client.get(url) resp = self.client.get(url)
if settings.FEATURES.get('ENTRANCE_EXAMS', False): self.assertNotIn('To access course materials, you must score', resp.content)
self.assertNotIn('To access course materials, you must score', resp.content) self.assertIn('You have passed the entrance exam.', resp.content)
self.assertIn('You have passed the entrance exam.', resp.content) self.assertIn('Lesson 1', resp.content)
self.assertIn('Lesson 1', resp.content)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True, 'MILESTONES_APP': True})
def test_entrance_exam_gating(self): def test_entrance_exam_gating(self):
""" """
Unit Test: test_entrance_exam_gating Unit Test: test_entrance_exam_gating
""" """
# This user helps to cover a discovered bug in the milestone fulfillment logic # This user helps to cover a discovered bug in the milestone fulfillment logic
chaos_user = UserFactory() chaos_user = UserFactory()
locked_toc = toc_for_course( locked_toc = self._return_table_of_contents()
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
for toc_section in self.expected_locked_toc: for toc_section in self.expected_locked_toc:
self.assertIn(toc_section, locked_toc) self.assertIn(toc_section, locked_toc)
# Set up the chaos user # Set up the chaos user
# pylint: disable=maybe-no-member,no-member answer_entrance_exam_problem(self.course, self.request, self.problem_1, chaos_user)
grade_dict = {'value': 1, 'max_value': 1, 'user_id': chaos_user.id} answer_entrance_exam_problem(self.course, self.request, self.problem_1)
field_data_cache = FieldDataCache.cache_for_descriptor_descendents( answer_entrance_exam_problem(self.course, self.request, self.problem_2)
self.course.id,
chaos_user,
self.course,
depth=2
)
# pylint: disable=protected-access
module = get_module(
chaos_user,
self.request,
self.problem_1.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_1, 'grade', grade_dict)
# pylint: disable=maybe-no-member,no-member
grade_dict = {'value': 1, 'max_value': 1, 'user_id': self.request.user.id}
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course.id,
self.request.user,
self.course,
depth=2
)
# pylint: disable=protected-access
module = get_module(
self.request.user,
self.request,
self.problem_1.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(self.problem_1, 'grade', grade_dict)
module = get_module( unlocked_toc = self._return_table_of_contents()
self.request.user,
self.request,
self.problem_2.scope_ids.usage_id,
field_data_cache,
)._xmodule # pylint: disable=protected-access
module.system.publish(self.problem_2, 'grade', grade_dict)
unlocked_toc = toc_for_course(
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
for toc_section in self.expected_unlocked_toc: for toc_section in self.expected_unlocked_toc:
self.assertIn(toc_section, unlocked_toc) self.assertIn(toc_section, unlocked_toc)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
def test_skip_entrance_exam_gating(self): def test_skip_entrance_exam_gating(self):
""" """
Tests gating is disabled if skip entrance exam is set for a user. Tests gating is disabled if skip entrance exam is set for a user.
""" """
# make sure toc is locked before allowing user to skip entrance exam # make sure toc is locked before allowing user to skip entrance exam
locked_toc = toc_for_course( locked_toc = self._return_table_of_contents()
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
for toc_section in self.expected_locked_toc: for toc_section in self.expected_locked_toc:
self.assertIn(toc_section, locked_toc) self.assertIn(toc_section, locked_toc)
...@@ -561,13 +392,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -561,13 +392,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
}) })
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
unlocked_toc = toc_for_course( unlocked_toc = self._return_table_of_contents()
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
for toc_section in self.expected_unlocked_toc: for toc_section in self.expected_unlocked_toc:
self.assertIn(toc_section, unlocked_toc) self.assertIn(toc_section, unlocked_toc)
...@@ -584,17 +409,10 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -584,17 +409,10 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
# assert staff has access to all toc # assert staff has access to all toc
self.request.user = staff_user self.request.user = staff_user
unlocked_toc = toc_for_course( unlocked_toc = self._return_table_of_contents()
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
for toc_section in self.expected_unlocked_toc: for toc_section in self.expected_unlocked_toc:
self.assertIn(toc_section, unlocked_toc) self.assertIn(toc_section, unlocked_toc)
@patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': True})
@patch('courseware.entrance_exams.user_has_passed_entrance_exam', Mock(return_value=False)) @patch('courseware.entrance_exams.user_has_passed_entrance_exam', Mock(return_value=False))
def test_courseware_page_access_without_passing_entrance_exam(self): def test_courseware_page_access_without_passing_entrance_exam(self):
""" """
...@@ -611,7 +429,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -611,7 +429,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
exam_url = response.get('Location') exam_url = response.get('Location')
self.assertRedirects(response, exam_url) self.assertRedirects(response, exam_url)
@patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': True})
@patch('courseware.entrance_exams.user_has_passed_entrance_exam', Mock(return_value=False)) @patch('courseware.entrance_exams.user_has_passed_entrance_exam', Mock(return_value=False))
def test_courseinfo_page_access_without_passing_entrance_exam(self): def test_courseinfo_page_access_without_passing_entrance_exam(self):
""" """
...@@ -625,7 +442,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -625,7 +442,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
exam_url = response.get('Location') exam_url = response.get('Location')
self.assertRedirects(response, exam_url) self.assertRedirects(response, exam_url)
@patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': True})
@patch('courseware.entrance_exams.user_has_passed_entrance_exam', Mock(return_value=True)) @patch('courseware.entrance_exams.user_has_passed_entrance_exam', Mock(return_value=True))
def test_courseware_page_access_after_passing_entrance_exam(self): def test_courseware_page_access_after_passing_entrance_exam(self):
""" """
...@@ -634,7 +450,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -634,7 +450,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
# Mocking get_required_content with empty list to assume user has passed entrance exam # Mocking get_required_content with empty list to assume user has passed entrance exam
self._assert_chapter_loaded(self.course, self.chapter) self._assert_chapter_loaded(self.course, self.chapter)
@patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': True})
@patch('util.milestones_helpers.get_required_content', Mock(return_value=['a value'])) @patch('util.milestones_helpers.get_required_content', Mock(return_value=['a value']))
def test_courseware_page_access_with_staff_user_without_passing_entrance_exam(self): def test_courseware_page_access_with_staff_user_without_passing_entrance_exam(self):
""" """
...@@ -646,7 +461,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -646,7 +461,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
CourseEnrollmentFactory(user=staff_user, course_id=self.course.id) CourseEnrollmentFactory(user=staff_user, course_id=self.course.id)
self._assert_chapter_loaded(self.course, self.chapter) self._assert_chapter_loaded(self.course, self.chapter)
@patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': True})
def test_courseware_page_access_with_staff_user_after_passing_entrance_exam(self): def test_courseware_page_access_with_staff_user_after_passing_entrance_exam(self):
""" """
Test courseware access page after passing entrance exam but with staff user Test courseware access page after passing entrance exam but with staff user
...@@ -664,14 +478,12 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -664,14 +478,12 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
""" """
self._assert_chapter_loaded(self.course, self.chapter) self._assert_chapter_loaded(self.course, self.chapter)
@patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': True})
def test_can_skip_entrance_exam_with_anonymous_user(self): def test_can_skip_entrance_exam_with_anonymous_user(self):
""" """
Test can_skip_entrance_exam method with anonymous user Test can_skip_entrance_exam method with anonymous user
""" """
self.assertFalse(user_can_skip_entrance_exam(self.request, self.anonymous_user, self.course)) self.assertFalse(user_can_skip_entrance_exam(self.request, self.anonymous_user, self.course))
@patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': True})
def test_has_passed_entrance_exam_with_anonymous_user(self): def test_has_passed_entrance_exam_with_anonymous_user(self):
""" """
Test has_passed_entrance_exam method with anonymous user Test has_passed_entrance_exam method with anonymous user
...@@ -679,7 +491,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -679,7 +491,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
self.request.user = self.anonymous_user self.request.user = self.anonymous_user
self.assertFalse(user_has_passed_entrance_exam(self.request, self.course)) self.assertFalse(user_has_passed_entrance_exam(self.request, self.course))
@patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': True})
def test_course_has_entrance_exam_missing_exam_id(self): def test_course_has_entrance_exam_missing_exam_id(self):
course = CourseFactory.create( course = CourseFactory.create(
metadata={ metadata={
...@@ -688,7 +499,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -688,7 +499,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
) )
self.assertFalse(course_has_entrance_exam(course)) self.assertFalse(course_has_entrance_exam(course))
@patch.dict("django.conf.settings.FEATURES", {'ENTRANCE_EXAMS': True})
def test_user_has_passed_entrance_exam_short_circuit_missing_exam(self): def test_user_has_passed_entrance_exam_short_circuit_missing_exam(self):
course = CourseFactory.create( course = CourseFactory.create(
) )
...@@ -704,3 +514,89 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -704,3 +514,89 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def _return_table_of_contents(self):
"""
Returns table of content for the entrance exam specific to this test
Returns the table of contents for course self.course, for chapter
self.entrance_exam, and for section self.exam1
"""
self.field_data_cache = FieldDataCache.cache_for_descriptor_descendents( # pylint: disable=attribute-defined-outside-init
self.course.id,
self.request.user,
self.entrance_exam
)
return toc_for_course(
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
def answer_entrance_exam_problem(course, request, problem, user=None):
"""
Takes a required milestone `problem` in a `course` and fulfills it.
Args:
course (Course): Course object, the course the required problem is in
request (Request): request Object
problem (xblock): xblock object, the problem to be fulfilled
user (User): User object in case it is different from request.user
"""
if not user:
user = request.user
# pylint: disable=maybe-no-member,no-member
grade_dict = {'value': 1, 'max_value': 1, 'user_id': user.id}
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id,
user,
course,
depth=2
)
# pylint: disable=protected-access
module = get_module(
user,
request,
problem.scope_ids.usage_id,
field_data_cache,
)._xmodule
module.system.publish(problem, 'grade', grade_dict)
def add_entrance_exam_milestone(course, entrance_exam):
"""
Adds the milestone for given `entrance_exam` in `course`
Args:
course (Course): Course object in which the extrance_exam is located
entrance_exam (xblock): the entrance exam to be added as a milestone
"""
namespace_choices = get_namespace_choices()
milestone_relationship_types = get_milestone_relationship_types()
milestone_namespace = generate_milestone_namespace(
namespace_choices.get('ENTRANCE_EXAM'),
course.id
)
milestone = add_milestone(
{
'name': 'Test Milestone',
'namespace': milestone_namespace,
'description': 'Testing Courseware Entrance Exam Chapter',
}
)
add_course_milestone(
unicode(course.id),
milestone_relationship_types['REQUIRES'],
milestone
)
add_course_content_milestone(
unicode(course.id),
unicode(entrance_exam.location),
milestone_relationship_types['FULFILLS'],
milestone
)
"""
Milestone related tests for the mobile_api
"""
from mock import patch
from courseware.tests.helpers import get_request_for_user
from courseware.tests.test_entrance_exam import answer_entrance_exam_problem, add_entrance_exam_milestone
from util.milestones_helpers import (
add_prerequisite_course,
fulfill_course_milestone,
seed_milestone_relationship_types,
)
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
class MobileAPIMilestonesMixin(object):
"""
Tests the Mobile API decorators for milestones.
The two milestones currently supported in these tests are entrance exams and
pre-requisite courses. If either of these milestones are unfulfilled,
the mobile api will appropriately block content until the milestone is
fulfilled.
"""
MILESTONE_MESSAGE = {
'developer_message':
'Cannot access content with unfulfilled pre-requisites or unpassed entrance exam.'
}
ALLOW_ACCESS_TO_MILESTONE_COURSE = False # pylint: disable=invalid-name
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_unfulfilled_prerequisite_course(self):
""" Tests the case for an unfulfilled pre-requisite course """
self._add_prerequisite_course()
self.init_course_access()
self._verify_unfulfilled_milestone_response()
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_unfulfilled_prerequisite_course_for_staff(self):
self._add_prerequisite_course()
self.user.is_staff = True
self.user.save()
self.init_course_access()
self.api_response()
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_fulfilled_prerequisite_course(self):
"""
Tests the case when a user fulfills existing pre-requisite course
"""
self._add_prerequisite_course()
add_prerequisite_course(self.course.id, self.prereq_course.id)
fulfill_course_milestone(self.prereq_course.id, self.user)
self.init_course_access()
self.api_response()
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True, 'MILESTONES_APP': True})
def test_unpassed_entrance_exam(self):
"""
Tests the case where the user has not passed the entrance exam
"""
self._add_entrance_exam()
self.init_course_access()
self._verify_unfulfilled_milestone_response()
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True, 'MILESTONES_APP': True})
def test_unpassed_entrance_exam_for_staff(self):
self._add_entrance_exam()
self.user.is_staff = True
self.user.save()
self.init_course_access()
self.api_response()
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True, 'MILESTONES_APP': True})
def test_passed_entrance_exam(self):
"""
Tests access when user has passed the entrance exam
"""
self._add_entrance_exam()
self._pass_entrance_exam()
self.init_course_access()
self.api_response()
def _add_entrance_exam(self):
""" Sets up entrance exam """
seed_milestone_relationship_types()
self.course.entrance_exam_enabled = True
self.entrance_exam = ItemFactory.create( # pylint: disable=attribute-defined-outside-init
parent=self.course,
category="chapter",
display_name="Entrance Exam Chapter",
is_entrance_exam=True,
in_entrance_exam=True
)
self.problem_1 = ItemFactory.create( # pylint: disable=attribute-defined-outside-init
parent=self.entrance_exam,
category='problem',
display_name="The Only Exam Problem",
graded=True,
in_entrance_exam=True
)
add_entrance_exam_milestone(self.course, self.entrance_exam)
self.course.entrance_exam_minimum_score_pct = 0.50
self.course.entrance_exam_id = unicode(self.entrance_exam.location)
modulestore().update_item(self.course, self.user.id)
def _add_prerequisite_course(self):
""" Helper method to set up the prerequisite course """
seed_milestone_relationship_types()
self.prereq_course = CourseFactory.create() # pylint: disable=attribute-defined-outside-init
add_prerequisite_course(self.course.id, self.prereq_course.id)
def _pass_entrance_exam(self):
""" Helper function to pass the entrance exam """
request = get_request_for_user(self.user)
answer_entrance_exam_problem(self.course, request, self.problem_1)
def _verify_unfulfilled_milestone_response(self):
"""
Verifies the response depending on ALLOW_ACCESS_TO_MILESTONE_COURSE
Since different endpoints will have different behaviours towards milestones,
setting ALLOW_ACCESS_TO_MILESTONE_COURSE (default is False) to True, will
not return a 204. For example, when getting a list of courses a user is
enrolled in, although a user may have unfulfilled milestones, the course
should still show up in the course enrollments list.
"""
if self.ALLOW_ACCESS_TO_MILESTONE_COURSE:
self.api_response()
else:
response = self.api_response(expected_response_code=204)
self.assertEqual(response.data, self.MILESTONE_MESSAGE)
...@@ -13,18 +13,21 @@ Test utilities for mobile API tests: ...@@ -13,18 +13,21 @@ Test utilities for mobile API tests:
# pylint: disable=no-member # pylint: disable=no-member
import ddt import ddt
from mock import patch from mock import patch
from rest_framework.test import APITestCase
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from courseware.tests.factories import UserFactory
from courseware.tests.factories import UserFactory
from student import auth from student import auth
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from mobile_api.test_milestones import MobileAPIMilestonesMixin
class MobileAPITestCase(ModuleStoreTestCase, APITestCase): class MobileAPITestCase(ModuleStoreTestCase, APITestCase):
""" """
...@@ -124,7 +127,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin): ...@@ -124,7 +127,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin):
@ddt.ddt @ddt.ddt
class MobileCourseAccessTestMixin(object): class MobileCourseAccessTestMixin(MobileAPIMilestonesMixin):
""" """
Test Mixin for testing APIs marked with mobile_course_access. Test Mixin for testing APIs marked with mobile_course_access.
(Use MobileEnrolledCourseAccessTestMixin when verify_enrolled is set to True.) (Use MobileEnrolledCourseAccessTestMixin when verify_enrolled is set to True.)
......
...@@ -49,6 +49,7 @@ class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin, MobileEn ...@@ -49,6 +49,7 @@ class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin, MobileEn
""" """
REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username']} REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username']}
ALLOW_ACCESS_TO_UNRELEASED_COURSE = True ALLOW_ACCESS_TO_UNRELEASED_COURSE = True
ALLOW_ACCESS_TO_MILESTONE_COURSE = True
def verify_success(self, response): def verify_success(self, response):
super(TestUserEnrollmentApi, self).verify_success(response) super(TestUserEnrollmentApi, self).verify_success(response)
......
...@@ -3,16 +3,21 @@ Common utility methods and decorators for Mobile APIs. ...@@ -3,16 +3,21 @@ Common utility methods and decorators for Mobile APIs.
""" """
import functools import functools
from rest_framework import permissions
from django.http import Http404
from rest_framework import permissions, status, response
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from openedx.core.lib.api.permissions import IsUserInUrl
from openedx.core.lib.api.authentication import ( from openedx.core.lib.api.authentication import (
SessionAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser,
OAuth2AuthenticationAllowInactiveUser, OAuth2AuthenticationAllowInactiveUser,
) )
from openedx.core.lib.api.permissions import IsUserInUrl from util.milestones_helpers import any_unfulfilled_milestones
from xmodule.modulestore.django import modulestore
def mobile_course_access(depth=0, verify_enrolled=True): def mobile_course_access(depth=0, verify_enrolled=True):
...@@ -30,12 +35,25 @@ def mobile_course_access(depth=0, verify_enrolled=True): ...@@ -30,12 +35,25 @@ def mobile_course_access(depth=0, verify_enrolled=True):
""" """
course_id = CourseKey.from_string(kwargs.pop('course_id')) course_id = CourseKey.from_string(kwargs.pop('course_id'))
with modulestore().bulk_operations(course_id): with modulestore().bulk_operations(course_id):
course = get_course_with_access( try:
request.user, course = get_course_with_access(
'load_mobile' if verify_enrolled else 'load_mobile_no_enrollment_check', request.user,
course_id, 'load_mobile' if verify_enrolled else 'load_mobile_no_enrollment_check',
depth=depth course_id,
) depth=depth
)
except Http404:
# any_unfulfilled_milestones called a second time since get_course_with_access returns a bool
if any_unfulfilled_milestones(course_id, request.user.id):
message = {
"developer_message": "Cannot access content with unfulfilled pre-requisites or unpassed entrance exam." # pylint: disable=line-too-long
}
return response.Response(
data=message,
status=status.HTTP_204_NO_CONTENT
)
else:
raise
return func(self, request, course=course, *args, **kwargs) return func(self, request, course=course, *args, **kwargs)
return _wrapper return _wrapper
return _decorator return _decorator
......
...@@ -452,12 +452,6 @@ FEATURES['ENABLE_EDXNOTES'] = True ...@@ -452,12 +452,6 @@ FEATURES['ENABLE_EDXNOTES'] = True
# Add milestones to Installed apps for testing # Add milestones to Installed apps for testing
INSTALLED_APPS += ('milestones', ) INSTALLED_APPS += ('milestones', )
# MILESTONES
FEATURES['MILESTONES_APP'] = True
# ENTRANCE EXAMS
FEATURES['ENTRANCE_EXAMS'] = True
# Enable courseware search for tests # Enable courseware search for tests
FEATURES['ENABLE_COURSEWARE_SEARCH'] = True FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
......
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