Commit 5cef287c by christopher lee

MA-635 Block Mobile Content for unfulfilled milestones

Previously, the mobile api did not check for pre-requisite courses
or entrance exams. This change checks for these milestones and then
returns course content accordingly.
parent 36060f3f
......@@ -7,7 +7,8 @@ from django.conf import settings
from django.utils.translation import ugettext as _
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
NAMESPACE_CHOICES = {
......@@ -26,7 +27,7 @@ def add_prerequisite_course(course_key, prerequisite_course_key):
"""
It would create a milestone, then it would set newly created
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`.
"""
if not settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False):
......@@ -313,6 +314,15 @@ def 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):
"""
Client API operation adapter/wrapper
......
......@@ -3,6 +3,8 @@ Tests for the milestones helpers library, which is the integration point for the
"""
from mock import patch
from milestones.exceptions import InvalidCourseKeyException, InvalidUserException
from util import milestones_helpers
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
......@@ -85,3 +87,11 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase):
def test_add_user_milestone_returns_none_when_app_disabled(self):
response = milestones_helpers.add_user_milestone(self.user, self.milestone)
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 (
GlobalStaff, CourseStaffRole, CourseInstructorRole,
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
......@@ -173,7 +176,12 @@ def _has_access_course_desc(user, action, course):
# check start date
can_load() and
# 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():
......
......@@ -2,16 +2,14 @@ import json
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import RequestFactory
from student.models import Registration
from django.test import TestCase
def get_request_for_user(user):
"""Create a request object for user."""
request = RequestFactory()
request.user = user
request.COOKIES = {}
......
"""
Tests use cases related to LMS Entrance Exam behavior, such as gated content access (TOC)
"""
from django.conf import settings
from django.test.client import RequestFactory
from mock import patch, Mock
from django.core.urlresolvers import reverse
from nose.plugins.attrib import attr
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.helpers import LoginEnrollmentTestCase
from courseware.tests.helpers import (
LoginEnrollmentTestCase,
get_request_for_user
)
from courseware.entrance_exams import (
course_has_entrance_exam,
get_entrance_exam_content,
......@@ -17,9 +20,8 @@ from courseware.entrance_exams import (
user_can_skip_entrance_exam,
user_has_passed_entrance_exam,
)
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from student.models import CourseEnrollment
from student.tests.factories import CourseEnrollmentFactory, AnonymousUserFactory
from util.milestones_helpers import (
add_milestone,
add_course_milestone,
......@@ -29,20 +31,21 @@ from util.milestones_helpers import (
get_milestone_relationship_types,
seed_milestone_relationship_types,
)
from student.models import CourseEnrollment
from student.tests.factories import CourseEnrollmentFactory, AnonymousUserFactory
from mock import patch, Mock
import mock
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@attr('shard_1')
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True, 'MILESTONES_APP': True})
class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
"""
Check that content is properly gated. Create a test course from scratch to mess with.
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
If set to False, we are essentially confirming that the workflows do not cause exceptions
Check that content is properly gated.
Creates a test course from scratch. The tests below are designed to execute
workflows regardless of the feature flag settings.
"""
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True, 'MILESTONES_APP': True})
def setUp(self):
"""
Test case scaffolding
......@@ -122,54 +125,17 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
category="problem",
display_name="Exam Problem - Problem 2"
)
self.problem_3 = ItemFactory.create(
parent=subsection,
category="problem",
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
)
seed_milestone_relationship_types()
add_entrance_exam_milestone(self.course, self.entrance_exam)
self.course.entrance_exam_enabled = True
self.course.entrance_exam_minimum_score_pct = 0.50
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")
CourseEnrollment.enroll(self.request.user, self.course.id)
......@@ -260,8 +226,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
'section': self.exam_1.location.name
})
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})
def test_entrance_exam_content_absence(self):
......@@ -294,53 +259,26 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
'section': self.exam_1.location.name
})
resp = self.client.get(url)
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
resp = self.client.get(expected_url)
self.assertIn('Exam Problem - Problem 1', resp.content)
self.assertIn('Exam Problem - Problem 2', resp.content)
self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
resp = self.client.get(expected_url)
self.assertIn('Exam Problem - Problem 1', resp.content)
self.assertIn('Exam Problem - Problem 2', resp.content)
def test_get_entrance_exam_content(self):
"""
test get entrance exam content method
"""
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.assertFalse(user_has_passed_entrance_exam(self.request, self.course))
# Pass the entrance exam
# 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)
# 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})
self.assertEqual(exam_chapter.url_name, self.entrance_exam.url_name)
self.assertFalse(user_has_passed_entrance_exam(self.request, self.course))
answer_entrance_exam_problem(self.course, self.request, self.problem_1)
answer_entrance_exam_problem(self.course, self.request, self.problem_2)
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))
def test_entrance_exam_score(self):
"""
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):
exam_score = get_entrance_exam_score(self.request, self.course)
self.assertEqual(exam_score, 0)
# Pass the entrance exam
# 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)
# 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)
answer_entrance_exam_problem(self.course, self.request, self.problem_1)
answer_entrance_exam_problem(self.course, self.request, self.problem_2)
exam_score = get_entrance_exam_score(self.request, self.course)
# 50 percent exam score should be achieved.
......@@ -392,9 +306,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
}
)
resp = self.client.get(url)
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
self.assertEqual(resp.status_code, 200)
self.assertIn('To access course materials, you must score', resp.content)
self.assertEqual(resp.status_code, 200)
self.assertIn('To access course materials, you must score', resp.content)
def test_entrance_exam_requirement_message_hidden(self):
"""
......@@ -416,9 +329,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
)
resp = self.client.get(url)
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('You have passed the entrance exam.', resp.content)
self.assertNotIn('To access course materials, you must score', resp.content)
self.assertNotIn('You have passed the entrance exam.', resp.content)
def test_entrance_exam_passed_message_and_course_content(self):
"""
......@@ -434,121 +346,40 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
}
)
# 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)
# 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)
answer_entrance_exam_problem(self.course, self.request, self.problem_1)
answer_entrance_exam_problem(self.course, self.request, self.problem_2)
resp = self.client.get(url)
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
self.assertNotIn('To access course materials, you must score', resp.content)
self.assertIn('You have passed the entrance exam.', resp.content)
self.assertIn('Lesson 1', 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('Lesson 1', resp.content)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True, 'MILESTONES_APP': True})
def test_entrance_exam_gating(self):
"""
Unit Test: test_entrance_exam_gating
"""
# This user helps to cover a discovered bug in the milestone fulfillment logic
chaos_user = UserFactory()
locked_toc = toc_for_course(
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
locked_toc = self._return_table_of_contents()
for toc_section in self.expected_locked_toc:
self.assertIn(toc_section, locked_toc)
# Set up the chaos user
# pylint: disable=maybe-no-member,no-member
grade_dict = {'value': 1, 'max_value': 1, 'user_id': chaos_user.id}
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
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)
answer_entrance_exam_problem(self.course, self.request, self.problem_1, chaos_user)
answer_entrance_exam_problem(self.course, self.request, self.problem_1)
answer_entrance_exam_problem(self.course, self.request, self.problem_2)
module = get_module(
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
)
unlocked_toc = self._return_table_of_contents()
for toc_section in self.expected_unlocked_toc:
self.assertIn(toc_section, unlocked_toc)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
def test_skip_entrance_exam_gating(self):
"""
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
locked_toc = toc_for_course(
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
locked_toc = self._return_table_of_contents()
for toc_section in self.expected_locked_toc:
self.assertIn(toc_section, locked_toc)
......@@ -561,13 +392,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
})
self.assertEqual(response.status_code, 200)
unlocked_toc = toc_for_course(
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
unlocked_toc = self._return_table_of_contents()
for toc_section in self.expected_unlocked_toc:
self.assertIn(toc_section, unlocked_toc)
......@@ -584,17 +409,10 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
# assert staff has access to all toc
self.request.user = staff_user
unlocked_toc = toc_for_course(
self.request,
self.course,
self.entrance_exam.url_name,
self.exam_1.url_name,
self.field_data_cache
)
unlocked_toc = self._return_table_of_contents()
for toc_section in self.expected_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))
def test_courseware_page_access_without_passing_entrance_exam(self):
"""
......@@ -611,7 +429,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
exam_url = response.get('Location')
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))
def test_courseinfo_page_access_without_passing_entrance_exam(self):
"""
......@@ -625,7 +442,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
exam_url = response.get('Location')
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))
def test_courseware_page_access_after_passing_entrance_exam(self):
"""
......@@ -634,7 +450,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
# Mocking get_required_content with empty list to assume user has passed entrance exam
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']))
def test_courseware_page_access_with_staff_user_without_passing_entrance_exam(self):
"""
......@@ -646,7 +461,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
CourseEnrollmentFactory(user=staff_user, course_id=self.course.id)
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):
"""
Test courseware access page after passing entrance exam but with staff user
......@@ -664,14 +478,12 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
"""
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):
"""
Test can_skip_entrance_exam method with anonymous user
"""
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):
"""
Test has_passed_entrance_exam method with anonymous user
......@@ -679,7 +491,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
self.request.user = self.anonymous_user
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):
course = CourseFactory.create(
metadata={
......@@ -688,7 +499,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
)
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):
course = CourseFactory.create(
)
......@@ -704,3 +514,89 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
)
response = self.client.get(url)
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:
# pylint: disable=no-member
import ddt
from mock import patch
from rest_framework.test import APITestCase
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
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.models import CourseEnrollment
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from mobile_api.test_milestones import MobileAPIMilestonesMixin
class MobileAPITestCase(ModuleStoreTestCase, APITestCase):
"""
......@@ -124,7 +127,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin):
@ddt.ddt
class MobileCourseAccessTestMixin(object):
class MobileCourseAccessTestMixin(MobileAPIMilestonesMixin):
"""
Test Mixin for testing APIs marked with mobile_course_access.
(Use MobileEnrolledCourseAccessTestMixin when verify_enrolled is set to True.)
......
......@@ -49,6 +49,7 @@ class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin, MobileEn
"""
REVERSE_INFO = {'name': 'courseenrollment-detail', 'params': ['username']}
ALLOW_ACCESS_TO_UNRELEASED_COURSE = True
ALLOW_ACCESS_TO_MILESTONE_COURSE = True
def verify_success(self, response):
super(TestUserEnrollmentApi, self).verify_success(response)
......
......@@ -3,16 +3,21 @@ Common utility methods and decorators for Mobile APIs.
"""
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 xmodule.modulestore.django import modulestore
from courseware.courses import get_course_with_access
from openedx.core.lib.api.permissions import IsUserInUrl
from openedx.core.lib.api.authentication import (
SessionAuthenticationAllowInactiveUser,
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):
......@@ -30,12 +35,25 @@ def mobile_course_access(depth=0, verify_enrolled=True):
"""
course_id = CourseKey.from_string(kwargs.pop('course_id'))
with modulestore().bulk_operations(course_id):
course = get_course_with_access(
request.user,
'load_mobile' if verify_enrolled else 'load_mobile_no_enrollment_check',
course_id,
depth=depth
)
try:
course = get_course_with_access(
request.user,
'load_mobile' if verify_enrolled else 'load_mobile_no_enrollment_check',
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 _wrapper
return _decorator
......
......@@ -452,12 +452,6 @@ FEATURES['ENABLE_EDXNOTES'] = True
# Add milestones to Installed apps for testing
INSTALLED_APPS += ('milestones', )
# MILESTONES
FEATURES['MILESTONES_APP'] = True
# ENTRANCE EXAMS
FEATURES['ENTRANCE_EXAMS'] = True
# Enable courseware search for tests
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