Commit cd053913 by Matt Drayer

Cleaned up milestones API references

parent cce00e69
......@@ -6,6 +6,7 @@ import json
import copy
import mock
from mock import patch
import unittest
from django.utils.timezone import UTC
from django.test.utils import override_settings
......@@ -139,19 +140,9 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertNotContains(response, "Course Introduction Video")
self.assertNotContains(response, "Requirements")
def _seed_milestone_relationship_types(self):
"""
Helper method to prepopulate MRTs so the tests can run
Note the settings check -- exams feature must be enabled for the tests to run correctly
"""
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
from milestones.models import MilestoneRelationshipType
MilestoneRelationshipType.objects.create(name='requires')
MilestoneRelationshipType.objects.create(name='fulfills')
@patch.dict(settings.FEATURES, {'ENTRANCE_EXAMS': True})
@unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True)
def test_entrance_exam_created_updated_and_deleted_successfully(self):
self._seed_milestone_relationship_types()
seed_milestone_relationship_types()
settings_details_url = get_url(self.course.id)
data = {
'entrance_exam_enabled': 'true',
......@@ -196,13 +187,13 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertFalse(course.entrance_exam_enabled)
self.assertEquals(course.entrance_exam_minimum_score_pct, None)
@patch.dict(settings.FEATURES, {'ENTRANCE_EXAMS': True})
@unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True)
def test_entrance_exam_store_default_min_score(self):
"""
test that creating an entrance exam should store the default value, if key missing in json request
or entrance_exam_minimum_score_pct is an empty string
"""
self._seed_milestone_relationship_types()
seed_milestone_relationship_types()
settings_details_url = get_url(self.course.id)
test_data_1 = {
'entrance_exam_enabled': 'true',
......
......@@ -2,22 +2,22 @@
Entrance Exams view module -- handles all requests related to entrance exam management via Studio
Intended to be utilized as an AJAX callback handler, versus a proper view/screen
"""
from functools import wraps
import json
import logging
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.http import HttpResponse
from django.http import HttpResponse, HttpResponseBadRequest
from django.test import RequestFactory
from contentstore.views.helpers import create_xblock
from contentstore.views.item import delete_item
from milestones import api as milestones_api
from models.settings.course_metadata import CourseMetadata
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys import InvalidKeyError
from student.auth import has_course_author_access
from util.milestones_helpers import generate_milestone_namespace, NAMESPACE_CHOICES
from util import milestones_helpers
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from django.conf import settings
......@@ -40,8 +40,24 @@ def _get_default_entrance_exam_minimum_pct():
return entrance_exam_minimum_score_pct
# pylint: disable=missing-docstring
def check_feature_enabled(feature_name):
"""
Ensure the specified feature is turned on. Return an HTTP 400 code if not.
"""
def _check_feature_enabled(view_func):
def _decorator(request, *args, **kwargs):
# Deny access if the entrance exam feature is disabled
if not settings.FEATURES.get(feature_name, False):
return HttpResponseBadRequest()
return view_func(request, *args, **kwargs)
return wraps(view_func)(_decorator)
return _check_feature_enabled
@login_required
@ensure_csrf_cookie
@check_feature_enabled(feature_name='ENTRANCE_EXAMS')
def entrance_exam(request, course_key_string):
"""
The restful handler for entrance exams.
......@@ -88,6 +104,7 @@ def entrance_exam(request, course_key_string):
return HttpResponse(status=405)
@check_feature_enabled(feature_name='ENTRANCE_EXAMS')
def create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct):
"""
api method to create an entrance exam.
......@@ -150,27 +167,28 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
)
# Add an entrance exam milestone if one does not already exist
milestone_namespace = generate_milestone_namespace(
NAMESPACE_CHOICES['ENTRANCE_EXAM'],
namespace_choices = milestones_helpers.get_namespace_choices()
milestone_namespace = milestones_helpers.generate_milestone_namespace(
namespace_choices.get('ENTRANCE_EXAM'),
course_key
)
milestones = milestones_api.get_milestones(milestone_namespace)
milestones = milestones_helpers.get_milestones(milestone_namespace)
if len(milestones):
milestone = milestones[0]
else:
description = 'Autogenerated during {} entrance exam creation.'.format(unicode(course.id))
milestone = milestones_api.add_milestone({
milestone = milestones_helpers.add_milestone({
'name': 'Completed Course Entrance Exam',
'namespace': milestone_namespace,
'description': description
})
relationship_types = milestones_api.get_milestone_relationship_types()
milestones_api.add_course_milestone(
relationship_types = milestones_helpers.get_milestone_relationship_types()
milestones_helpers.add_course_milestone(
unicode(course.id),
relationship_types['REQUIRES'],
milestone
)
milestones_api.add_course_content_milestone(
milestones_helpers.add_course_content_milestone(
unicode(course.id),
unicode(created_block.location),
relationship_types['FULFILLS'],
......@@ -202,6 +220,7 @@ def _get_entrance_exam(request, course_key): # pylint: disable=W0613
return HttpResponse(status=404)
@check_feature_enabled(feature_name='ENTRANCE_EXAMS')
def update_entrance_exam(request, course_key, exam_data):
"""
Operation to update course fields pertaining to entrance exams
......@@ -215,6 +234,7 @@ def update_entrance_exam(request, course_key, exam_data):
CourseMetadata.update_from_dict(metadata, course, request.user)
@check_feature_enabled(feature_name='ENTRANCE_EXAMS')
def delete_entrance_exam(request, course_key):
"""
api method to delete an entrance exam
......@@ -238,7 +258,7 @@ def _delete_entrance_exam(request, course_key):
for course_child in course_children:
if course_child.is_entrance_exam:
delete_item(request, course_child.scope_ids.usage_id)
milestones_api.remove_content_references(unicode(course_child.scope_ids.usage_id))
milestones_helpers.remove_content_references(unicode(course_child.scope_ids.usage_id))
# Reset the entrance exam flags on the course
# Reload the course so we have the latest state
......
......@@ -2,6 +2,7 @@
Test module for Entrance Exams AJAX callback handler workflows
"""
import json
from mock import patch
from django.conf import settings
from django.contrib.auth.models import User
......@@ -9,18 +10,14 @@ from django.test.client import RequestFactory
from contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase
from contentstore.utils import reverse_url
from contentstore.views.entrance_exam import create_entrance_exam
from contentstore.views.entrance_exam import create_entrance_exam, update_entrance_exam, delete_entrance_exam
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
from opaque_keys.edx.keys import UsageKey
from student.tests.factories import UserFactory
from util import milestones_helpers
from xmodule.modulestore.django import modulestore
if settings.FEATURES.get('MILESTONES_APP', False):
from milestones import api as milestones_api
from milestones.models import MilestoneRelationshipType
from util.milestones_helpers import serialize_user
class EntranceExamHandlerTests(CourseTestCase):
"""
......@@ -36,9 +33,8 @@ class EntranceExamHandlerTests(CourseTestCase):
self.usage_key = self.course.location
self.course_url = '/course/{}'.format(unicode(self.course.id))
self.exam_url = '/course/{}/entrance_exam/'.format(unicode(self.course.id))
MilestoneRelationshipType.objects.create(name='requires', active=True)
MilestoneRelationshipType.objects.create(name='fulfills', active=True)
self.milestone_relationship_types = milestones_api.get_milestone_relationship_types()
milestones_helpers.seed_milestone_relationship_types()
self.milestone_relationship_types = milestones_helpers.get_milestone_relationship_types()
def test_contentstore_views_entrance_exam_post(self):
"""
......@@ -55,8 +51,8 @@ class EntranceExamHandlerTests(CourseTestCase):
self.assertTrue(metadata['entrance_exam_enabled'])
self.assertIsNotNone(metadata['entrance_exam_minimum_score_pct'])
self.assertIsNotNone(metadata['entrance_exam_id']['value'])
self.assertTrue(len(milestones_api.get_course_milestones(unicode(self.course.id))))
content_milestones = milestones_api.get_course_content_milestones(
self.assertTrue(len(milestones_helpers.get_course_milestones(unicode(self.course.id))))
content_milestones = milestones_helpers.get_course_content_milestones(
unicode(self.course.id),
metadata['entrance_exam_id']['value'],
self.milestone_relationship_types['FULFILLS']
......@@ -123,12 +119,12 @@ class EntranceExamHandlerTests(CourseTestCase):
)
user.set_password('test')
user.save()
milestones = milestones_api.get_course_milestones(unicode(self.course_key))
milestones = milestones_helpers.get_course_milestones(unicode(self.course_key))
self.assertEqual(len(milestones), 1)
milestone_key = '{}.{}'.format(milestones[0]['namespace'], milestones[0]['name'])
paths = milestones_api.get_course_milestones_fulfillment_paths(
paths = milestones_helpers.get_course_milestones_fulfillment_paths(
unicode(self.course_key),
serialize_user(user)
milestones_helpers.serialize_user(user)
)
# What we have now is a course milestone requirement and no valid fulfillment
......@@ -250,3 +246,22 @@ class EntranceExamHandlerTests(CourseTestCase):
resp = create_entrance_exam(request, self.course.id, None)
self.assertEqual(resp.status_code, 201)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': False})
def test_entrance_exam_feature_flag_gating(self):
user = UserFactory()
user.is_staff = True
request = RequestFactory()
request.user = user
resp = self.client.get(self.exam_url)
self.assertEqual(resp.status_code, 400)
resp = create_entrance_exam(request, self.course.id, None)
self.assertEqual(resp.status_code, 400)
resp = delete_entrance_exam(request, self.course.id)
self.assertEqual(resp.status_code, 400)
# No return, so we'll just ensure no exception is thrown
update_entrance_exam(request, self.course.id, {})
......@@ -5,30 +5,24 @@ Utility library for working with the edx-milestones app
from django.conf import settings
from django.utils.translation import ugettext as _
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from courseware.models import StudentModule
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from xmodule.modulestore.django import modulestore
from milestones.api import (
get_course_milestones,
add_milestone,
add_course_milestone,
remove_course_milestone,
get_course_milestones_fulfillment_paths,
add_user_milestone,
get_user_milestones,
)
from milestones.models import MilestoneRelationshipType
from milestones.exceptions import InvalidMilestoneRelationshipTypeException
from opaque_keys.edx.keys import UsageKey
NAMESPACE_CHOICES = {
'ENTRANCE_EXAM': 'entrance_exams'
}
def get_namespace_choices():
"""
Return the enum to the caller
"""
return NAMESPACE_CHOICES
def add_prerequisite_course(course_key, prerequisite_course_key):
"""
It would create a milestone, then it would set newly created
......@@ -36,18 +30,23 @@ def add_prerequisite_course(course_key, prerequisite_course_key):
and it would set newly created milestone as fulfilment
milestone for course referred by `prerequisite_course_key`.
"""
if settings.FEATURES.get('MILESTONES_APP', False):
# create a milestone
milestone = add_milestone({
'name': _('Course {} requires {}'.format(unicode(course_key), unicode(prerequisite_course_key))),
'namespace': unicode(prerequisite_course_key),
'description': _('System defined milestone'),
})
# add requirement course milestone
add_course_milestone(course_key, 'requires', milestone)
if not settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False):
return None
from milestones import api as milestones_api
milestone_name = _('Course {course_id} requires {prerequisite_course_id}').format(
course_id=unicode(course_key),
prerequisite_course_id=unicode(prerequisite_course_key)
)
milestone = milestones_api.add_milestone({
'name': milestone_name,
'namespace': unicode(prerequisite_course_key),
'description': _('System defined milestone'),
})
# add requirement course milestone
milestones_api.add_course_milestone(course_key, 'requires', milestone)
# add fulfillment course milestone
add_course_milestone(prerequisite_course_key, 'fulfills', milestone)
# add fulfillment course milestone
milestones_api.add_course_milestone(prerequisite_course_key, 'fulfills', milestone)
def remove_prerequisite_course(course_key, milestone):
......@@ -55,11 +54,13 @@ def remove_prerequisite_course(course_key, milestone):
It would remove pre-requisite course milestone for course
referred by `course_key`.
"""
if settings.FEATURES.get('MILESTONES_APP', False):
remove_course_milestone(
course_key,
milestone,
)
if not settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False):
return None
from milestones import api as milestones_api
milestones_api.remove_course_milestone(
course_key,
milestone,
)
def set_prerequisite_courses(course_key, prerequisite_course_keys):
......@@ -69,18 +70,20 @@ def set_prerequisite_courses(course_key, prerequisite_course_keys):
To only remove course milestones pass `course_key` and empty list or
None as `prerequisite_course_keys` .
"""
if settings.FEATURES.get('MILESTONES_APP', False):
#remove any existing requirement milestones with this pre-requisite course as requirement
course_milestones = get_course_milestones(course_key=course_key, relationship="requires")
if course_milestones:
for milestone in course_milestones:
remove_prerequisite_course(course_key, milestone)
if not settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False):
return None
from milestones import api as milestones_api
#remove any existing requirement milestones with this pre-requisite course as requirement
course_milestones = milestones_api.get_course_milestones(course_key=course_key, relationship="requires")
if course_milestones:
for milestone in course_milestones:
remove_prerequisite_course(course_key, milestone)
# add milestones if pre-requisite course is selected
if prerequisite_course_keys:
for prerequisite_course_key_string in prerequisite_course_keys:
prerequisite_course_key = CourseKey.from_string(prerequisite_course_key_string)
add_prerequisite_course(course_key, prerequisite_course_key)
# add milestones if pre-requisite course is selected
if prerequisite_course_keys:
for prerequisite_course_key_string in prerequisite_course_keys:
prerequisite_course_key = CourseKey.from_string(prerequisite_course_key_string)
add_prerequisite_course(course_key, prerequisite_course_key)
def get_pre_requisite_courses_not_completed(user, enrolled_courses):
......@@ -91,10 +94,11 @@ def get_pre_requisite_courses_not_completed(user, enrolled_courses):
prerequisite courses yet to be completed.
"""
pre_requisite_courses = {}
if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES'):
if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False):
from milestones import api as milestones_api
for course_key in enrolled_courses:
required_courses = []
fulfilment_paths = get_course_milestones_fulfillment_paths(course_key, {'id': user.id})
fulfilment_paths = milestones_api.get_course_milestones_fulfillment_paths(course_key, {'id': user.id})
for milestone_key, milestone_value in fulfilment_paths.items(): # pylint: disable=unused-variable
for key, value in milestone_value.items():
if key == 'courses' and value:
......@@ -146,10 +150,12 @@ def fulfill_course_milestone(course_key, user):
Marks the course specified by the given course_key as complete for the given user.
If any other courses require this course as a prerequisite, their milestones will be appropriately updated.
"""
if settings.FEATURES.get('MILESTONES_APP', False):
course_milestones = get_course_milestones(course_key=course_key, relationship="fulfills")
for milestone in course_milestones:
add_user_milestone({'id': user.id}, milestone)
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
from milestones import api as milestones_api
course_milestones = milestones_api.get_course_milestones(course_key=course_key, relationship="fulfills")
for milestone in course_milestones:
milestones_api.add_user_milestone({'id': user.id}, milestone)
def get_required_content(course, user):
......@@ -159,9 +165,12 @@ def get_required_content(course, user):
"""
required_content = []
if settings.FEATURES.get('MILESTONES_APP', False):
from milestones import api as milestones_api
from milestones.exceptions import InvalidMilestoneRelationshipTypeException
# Get all of the outstanding milestones for this course, for this user
try:
milestone_paths = get_course_milestones_fulfillment_paths(
milestone_paths = milestones_api.get_course_milestones_fulfillment_paths(
unicode(course.id),
serialize_user(user)
)
......@@ -221,8 +230,10 @@ def milestones_achieved_by_user(user, namespace):
"""
It would fetch list of milestones completed by user
"""
if settings.FEATURES.get('MILESTONES_APP', False):
return get_user_milestones({'id': user.id}, namespace)
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
from milestones import api as milestones_api
return milestones_api.get_user_milestones({'id': user.id}, namespace)
def is_valid_course_key(key):
......@@ -240,9 +251,11 @@ def seed_milestone_relationship_types():
"""
Helper method to pre-populate MRTs so the tests can run
"""
if settings.FEATURES.get('MILESTONES_APP', False):
MilestoneRelationshipType.objects.create(name='requires')
MilestoneRelationshipType.objects.create(name='fulfills')
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
from milestones.models import MilestoneRelationshipType
MilestoneRelationshipType.objects.create(name='requires')
MilestoneRelationshipType.objects.create(name='fulfills')
def generate_milestone_namespace(namespace, course_key=None):
......@@ -261,3 +274,106 @@ def serialize_user(user):
return {
'id': user.id,
}
def add_milestone(milestone_data):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
from milestones import api as milestones_api
return milestones_api.add_milestone(milestone_data)
def get_milestones(namespace):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return []
from milestones import api as milestones_api
return milestones_api.get_milestones(namespace)
def get_milestone_relationship_types():
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return {}
from milestones import api as milestones_api
return milestones_api.get_milestone_relationship_types()
def add_course_milestone(course_id, relationship, milestone):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
from milestones import api as milestones_api
return milestones_api.add_course_milestone(course_id, relationship, milestone)
def get_course_milestones(course_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return []
from milestones import api as milestones_api
return milestones_api.get_course_milestones(course_id)
def add_course_content_milestone(course_id, content_id, relationship, milestone):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
from milestones import api as milestones_api
return milestones_api.add_course_content_milestone(course_id, content_id, relationship, milestone)
def get_course_content_milestones(course_id, content_id, relationship):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return []
from milestones import api as milestones_api
return milestones_api.get_course_content_milestones(course_id, content_id, relationship)
def remove_content_references(content_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
from milestones import api as milestones_api
return milestones_api.remove_content_references(content_id)
def get_course_milestones_fulfillment_paths(course_id, user_id):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
from milestones import api as milestones_api
return milestones_api.get_course_milestones_fulfillment_paths(
course_id,
user_id
)
def add_user_milestone(user, milestone):
"""
Client API operation adapter/wrapper
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
from milestones import api as milestones_api
return milestones_api.add_user_milestone(user, milestone)
"""
Tests for the milestones helpers library, which is the integration point for the edx_milestones API
"""
from mock import patch
from util import milestones_helpers
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@patch.dict('django.conf.settings.FEATURES', {'MILESTONES_APP': False})
class MilestonesHelpersTestCase(ModuleStoreTestCase):
"""
Main test suite for Milestones API client library
"""
def setUp(self):
"""
Test case scaffolding
"""
super(MilestonesHelpersTestCase, self).setUp(create_user=False)
self.course = CourseFactory.create(
metadata={
'entrance_exam_enabled': True,
}
)
self.user = {'id': '123'}
self.milestone = {
'name': 'Test Milestone',
'namespace': 'doesnt.matter',
'description': 'Testing Milestones Helpers Library',
}
def test_add_milestone_returns_none_when_app_disabled(self):
response = milestones_helpers.add_milestone(milestone_data=self.milestone)
self.assertIsNone(response)
def test_get_milestones_returns_none_when_app_disabled(self):
response = milestones_helpers.get_milestones(namespace="whatever")
self.assertEqual(len(response), 0)
def test_get_milestone_relationship_types_returns_none_when_app_disabled(self):
response = milestones_helpers.get_milestone_relationship_types()
self.assertEqual(len(response), 0)
def test_add_course_milestone_returns_none_when_app_disabled(self):
response = milestones_helpers.add_course_milestone(unicode(self.course.id), 'requires', self.milestone)
self.assertIsNone(response)
def test_get_course_milestones_returns_none_when_app_disabled(self):
response = milestones_helpers.get_course_milestones(unicode(self.course.id))
self.assertEqual(len(response), 0)
def test_add_course_content_milestone_returns_none_when_app_disabled(self):
response = milestones_helpers.add_course_content_milestone(
unicode(self.course.id),
'i4x://any/content/id',
'requires',
self.milestone
)
self.assertIsNone(response)
def test_get_course_content_milestones_returns_none_when_app_disabled(self):
response = milestones_helpers.get_course_content_milestones(
unicode(self.course.id),
'i4x://doesnt/matter/for/this/test',
'requires'
)
self.assertEqual(len(response), 0)
def test_remove_content_references_returns_none_when_app_disabled(self):
response = milestones_helpers.remove_content_references("i4x://any/content/id/will/do")
self.assertIsNone(response)
def test_get_namespace_choices_returns_values_when_app_disabled(self):
response = milestones_helpers.get_namespace_choices()
self.assertIn('ENTRANCE_EXAM', response)
def test_get_course_milestones_fulfillment_paths_returns_none_when_app_disabled(self):
response = milestones_helpers.get_course_milestones_fulfillment_paths(unicode(self.course.id), self.user)
self.assertIsNone(response)
def test_add_user_milestone_returns_none_when_app_disabled(self):
response = milestones_helpers.add_user_milestone(self.user, self.milestone)
self.assertIsNone(response)
......@@ -63,10 +63,8 @@ from xmodule.x_module import XModuleDescriptor
from xblock_django.user_service import DjangoXBlockUserService
from util.json_request import JsonResponse
from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
if settings.FEATURES.get('MILESTONES_APP', False):
from milestones import api as milestones_api
from util.milestones_helpers import calculate_entrance_exam_score, get_required_content
from util.module_utils import yield_dynamic_descriptor_descendents
from util import milestones_helpers
from util.module_utils import yield_dynamic_descriptor_descendents
log = logging.getLogger(__name__)
......@@ -136,14 +134,14 @@ def toc_for_course(request, course, active_chapter, active_section, field_data_c
return None
# Check to see if the course is gated on milestone-required content (such as an Entrance Exam)
required_content = get_required_content(course, request.user)
required_content = milestones_helpers.get_required_content(course, request.user)
chapters = list()
for chapter in course_module.get_display_items():
# Only show required content, if there is required content
# chapter.hide_from_toc is read-only (boo)
local_hide_from_toc = False
if len(required_content):
if required_content:
if unicode(chapter.location) not in required_content:
local_hide_from_toc = True
......@@ -375,7 +373,7 @@ def get_module_system_for_user(user, field_data_cache,
inner_get_module
)
exam_modules = [module for module in exam_module_generators]
exam_score = calculate_entrance_exam_score(user, course_descriptor, exam_modules)
exam_score = milestones_helpers.calculate_entrance_exam_score(user, course_descriptor, exam_modules)
return exam_score
def _fulfill_content_milestones(user, course_key, content_key):
......@@ -394,8 +392,8 @@ def get_module_system_for_user(user, field_data_cache,
exam_pct = _calculate_entrance_exam_score(user, course)
if exam_pct >= course.entrance_exam_minimum_score_pct:
exam_key = UsageKey.from_string(course.entrance_exam_id)
relationship_types = milestones_api.get_milestone_relationship_types()
content_milestones = milestones_api.get_course_content_milestones(
relationship_types = milestones_helpers.get_milestone_relationship_types()
content_milestones = milestones_helpers.get_course_content_milestones(
course_key,
exam_key,
relationship=relationship_types['FULFILLS']
......@@ -403,7 +401,7 @@ def get_module_system_for_user(user, field_data_cache,
# Add each milestone to the user's set...
user = {'id': user.id}
for milestone in content_milestones:
milestones_api.add_user_milestone(user, milestone)
milestones_helpers.add_user_milestone(user, milestone)
def handle_grade_event(block, event_type, event): # pylint: disable=unused-argument
"""
......
......@@ -9,9 +9,7 @@ from courseware.access import has_access
from student.models import CourseEnrollment, EntranceExamConfiguration
from xmodule.tabs import CourseTabList
if settings.FEATURES.get('MILESTONES_APP', False):
from milestones.api import get_course_milestones_fulfillment_paths
from util.milestones_helpers import serialize_user
from util import milestones_helpers
def get_course_tab_list(course, user):
......@@ -33,9 +31,9 @@ def get_course_tab_list(course, user):
entrance_exam_mode = False
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
if getattr(course, 'entrance_exam_enabled', False):
course_milestones_paths = get_course_milestones_fulfillment_paths(
course_milestones_paths = milestones_helpers.get_course_milestones_fulfillment_paths(
unicode(course.id),
serialize_user(user)
milestones_helpers.serialize_user(user)
)
for __, value in course_milestones_paths.iteritems():
if len(value.get('content', [])):
......
"""
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 django.test.utils import override_settings
from django.core.urlresolvers import reverse
......@@ -9,12 +10,11 @@ from courseware.model_data import FieldDataCache
from courseware.module_render import get_module, toc_for_course
from courseware.tests.factories import UserFactory, InstructorFactory
from courseware.courses import get_entrance_exam_content_info, get_entrance_exam_score
from milestones import api as milestones_api
from milestones.models import MilestoneRelationshipType
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DATA_MOCK_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from util.milestones_helpers import generate_milestone_namespace, NAMESPACE_CHOICES
from util import milestones_helpers
from student.models import CourseEnrollment
from mock import patch
import mock
......@@ -23,8 +23,10 @@ import mock
class EntranceExamTestCases(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
"""
def setUp(self):
"""
Test case scaffolding
......@@ -109,30 +111,31 @@ class EntranceExamTestCases(ModuleStoreTestCase):
category="problem",
display_name="Exam Problem - Problem 3"
)
milestone_namespace = generate_milestone_namespace(
NAMESPACE_CHOICES['ENTRANCE_EXAM'],
self.course.id
)
self.milestone = {
'name': 'Test Milestone',
'namespace': milestone_namespace,
'description': 'Testing Courseware Entrance Exam Chapter',
}
MilestoneRelationshipType.objects.create(name='requires', active=True)
MilestoneRelationshipType.objects.create(name='fulfills', active=True)
self.milestone_relationship_types = milestones_api.get_milestone_relationship_types()
self.milestone = milestones_api.add_milestone(self.milestone)
milestones_api.add_course_milestone(
unicode(self.course.id),
self.milestone_relationship_types['REQUIRES'],
self.milestone
)
milestones_api.add_course_content_milestone(
unicode(self.course.id),
unicode(self.entrance_exam.location),
self.milestone_relationship_types['FULFILLS'],
self.milestone
)
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
namespace_choices = milestones_helpers.get_namespace_choices()
milestone_namespace = milestones_helpers.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',
}
milestones_helpers.seed_milestone_relationship_types()
self.milestone_relationship_types = milestones_helpers.get_milestone_relationship_types()
self.milestone = milestones_helpers.add_milestone(self.milestone)
milestones_helpers.add_course_milestone(
unicode(self.course.id),
self.milestone_relationship_types['REQUIRES'],
self.milestone
)
milestones_helpers.add_course_content_milestone(
unicode(self.course.id),
unicode(self.entrance_exam.location),
self.milestone_relationship_types['FULFILLS'],
self.milestone
)
user = UserFactory()
self.request = RequestFactory()
self.request.user = user
......@@ -241,7 +244,8 @@ class EntranceExamTestCases(ModuleStoreTestCase):
'section': self.exam_1.location.name
})
resp = self.client.get(url)
self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
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):
......@@ -261,7 +265,6 @@ class EntranceExamTestCases(ModuleStoreTestCase):
self.assertNotIn('Exam Problem - Problem 1', resp.content)
self.assertNotIn('Exam Problem - Problem 2', resp.content)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
def test_entrance_exam_content_presence(self):
"""
Unit Test: If entrance exam is enabled then its content e.g. problems should be loaded and redirection will
......@@ -275,41 +278,44 @@ class EntranceExamTestCases(ModuleStoreTestCase):
'section': self.exam_1.location.name
})
resp = self.client.get(url)
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)
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)
def test_entrance_exam_content_info(self):
"""
test entrance exam content info method
"""
exam_chapter, is_exam_passed = get_entrance_exam_content_info(self.request, self.course)
self.assertEqual(exam_chapter.url_name, self.entrance_exam.url_name)
self.assertEqual(is_exam_passed, False)
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
self.assertEqual(exam_chapter.url_name, self.entrance_exam.url_name)
self.assertEqual(is_exam_passed, False)
# 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)
# 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)
exam_chapter, is_exam_passed = get_entrance_exam_content_info(self.request, self.course)
self.assertEqual(exam_chapter, None)
self.assertEqual(is_exam_passed, True)
exam_chapter, is_exam_passed = get_entrance_exam_content_info(self.request, self.course)
self.assertEqual(exam_chapter, None)
self.assertEqual(is_exam_passed, True)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
def test_entrance_exam_score(self):
"""
test entrance exam score. we will hit the method get_entrance_exam_score to verify exam score.
......@@ -352,8 +358,9 @@ class EntranceExamTestCases(ModuleStoreTestCase):
}
)
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn('To access course materials, you must score', resp.content)
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
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):
"""
......@@ -369,8 +376,9 @@ class EntranceExamTestCases(ModuleStoreTestCase):
)
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertNotIn('To access course materials, you must score', resp.content)
self.assertNotIn('You have passed the entrance exam.', resp.content)
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)
def test_entrance_exam_passed_message_and_course_content(self):
"""
......@@ -404,10 +412,12 @@ class EntranceExamTestCases(ModuleStoreTestCase):
module.system.publish(self.problem_1, 'grade', grade_dict)
resp = self.client.get(url)
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)
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)
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
def test_entrance_exam_gating(self):
"""
Unit Test: test_entrance_exam_gating
......@@ -477,6 +487,7 @@ class EntranceExamTestCases(ModuleStoreTestCase):
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_exame_gating(self):
"""
Tests gating is disabled if skip entrance exam is set for a user.
......
......@@ -15,16 +15,14 @@ from xmodule import tabs
from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MIXED_TOY_MODULESTORE, TEST_DATA_MIXED_CLOSED_MODULESTORE
)
from courseware.tabs import get_course_tab_list
from courseware.views import get_static_tab_contents, static_tab
from student.tests.factories import UserFactory
from util import milestones_helpers
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
if settings.FEATURES.get('MILESTONES_APP', False):
from courseware.tabs import get_course_tab_list
from milestones import api as milestones_api
from milestones.models import MilestoneRelationshipType
class StaticTabDateTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
"""Test cases for Static Tab Dates."""
......@@ -140,9 +138,8 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
self.setup_user()
self.enroll(self.course)
self.user.is_staff = True
self.relationship_types = milestones_api.get_milestone_relationship_types()
MilestoneRelationshipType.objects.create(name='requires')
MilestoneRelationshipType.objects.create(name='fulfills')
self.relationship_types = milestones_helpers.get_milestone_relationship_types()
milestones_helpers.seed_milestone_relationship_types()
def test_get_course_tabs_list_entrance_exam_enabled(self):
"""
......@@ -160,13 +157,13 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
}
self.course.entrance_exam_enabled = True
self.course.entrance_exam_id = unicode(entrance_exam.location)
milestone = milestones_api.add_milestone(milestone)
milestones_api.add_course_milestone(
milestone = milestones_helpers.add_milestone(milestone)
milestones_helpers.add_course_milestone(
unicode(self.course.id),
self.relationship_types['REQUIRES'],
milestone
)
milestones_api.add_course_content_milestone(
milestones_helpers.add_course_content_milestone(
unicode(self.course.id),
unicode(entrance_exam.location),
self.relationship_types['FULFILLS'],
......
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