Add new gating logic to restrict users from accessing courseware when an entrance exam is required
- added entrance exam check on course info - staff can by pass gating and added tests - refined gating logic - changes after rebasing with Asad's branch - check ENTRANCE_EXAMS feature is enabled - updated test to reflect new logic - catering anonymous user in entrance exam permission - fixed broken tests - change after feedback on 16/3 - fix for a broken test - created new entrance_exams module - fixed quality error and improved test coverage - put get_required_content back in milestones helper - Refactored entrance exams logic - Refactored tabs logic - Fixed broken unit test - changes after feedback from dan-f on 3/27 - removed unnecessary user.is_anonymous check - Addressed PR feedback - Addressed commit-specific feedback - Rework guard clauses - Add coverage for course info case
This commit is contained in:
@@ -6,7 +6,6 @@ Utility library for working with the edx-milestones app
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from courseware.models import StudentModule
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -165,12 +164,10 @@ 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 = milestones_api.get_course_milestones_fulfillment_paths(
|
||||
milestone_paths = get_course_milestones_fulfillment_paths(
|
||||
unicode(course.id),
|
||||
serialize_user(user)
|
||||
)
|
||||
@@ -183,49 +180,9 @@ def get_required_content(course, user):
|
||||
if milestone_path.get('content') and len(milestone_path['content']):
|
||||
for content in milestone_path['content']:
|
||||
required_content.append(content)
|
||||
|
||||
#local imports to avoid circular reference
|
||||
from student.models import EntranceExamConfiguration
|
||||
can_skip_entrance_exam = EntranceExamConfiguration.user_can_skip_entrance_exam(user, course.id)
|
||||
# check if required_content has any entrance exam and user is allowed to skip it
|
||||
# then remove it from required content
|
||||
if required_content and getattr(course, 'entrance_exam_enabled', False) and can_skip_entrance_exam:
|
||||
descriptors = [modulestore().get_item(UsageKey.from_string(content)) for content in required_content]
|
||||
entrance_exam_contents = [unicode(descriptor.location)
|
||||
for descriptor in descriptors if descriptor.is_entrance_exam]
|
||||
required_content = list(set(required_content) - set(entrance_exam_contents))
|
||||
return required_content
|
||||
|
||||
|
||||
def calculate_entrance_exam_score(user, course_descriptor, exam_modules):
|
||||
"""
|
||||
Calculates the score (percent) of the entrance exam using the provided modules
|
||||
"""
|
||||
exam_module_ids = [exam_module.location for exam_module in exam_modules]
|
||||
student_modules = StudentModule.objects.filter(
|
||||
student=user,
|
||||
course_id=course_descriptor.id,
|
||||
module_state_key__in=exam_module_ids,
|
||||
)
|
||||
exam_pct = 0
|
||||
if student_modules:
|
||||
module_pcts = []
|
||||
ignore_categories = ['course', 'chapter', 'sequential', 'vertical']
|
||||
for module in exam_modules:
|
||||
if module.graded and module.category not in ignore_categories:
|
||||
module_pct = 0
|
||||
try:
|
||||
student_module = student_modules.get(module_state_key=module.location)
|
||||
if student_module.max_grade:
|
||||
module_pct = student_module.grade / student_module.max_grade
|
||||
module_pcts.append(module_pct)
|
||||
except StudentModule.DoesNotExist:
|
||||
pass
|
||||
if module_pcts:
|
||||
exam_pct = sum(module_pcts) / float(len(module_pcts))
|
||||
return exam_pct
|
||||
|
||||
|
||||
def milestones_achieved_by_user(user, namespace):
|
||||
"""
|
||||
It would fetch list of milestones completed by user
|
||||
|
||||
@@ -1037,7 +1037,7 @@ class EntranceExamTest(UniqueCourseTest):
|
||||
self.course_info['run'], self.course_info['display_name']
|
||||
).install()
|
||||
|
||||
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.settings_page = SettingsPage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
@@ -1050,19 +1050,19 @@ class EntranceExamTest(UniqueCourseTest):
|
||||
|
||||
def test_entrance_exam_section(self):
|
||||
"""
|
||||
Scenario: Any course that is enabled for an entrance exam, should have entrance exam section at course info
|
||||
Scenario: Any course that is enabled for an entrance exam, should have entrance exam chapter at courseware
|
||||
page.
|
||||
Given that I am on the course info page
|
||||
When I view the course info that has an entrance exam
|
||||
Then there should be an "Entrance Exam" section.'
|
||||
Given that I am on the courseware page
|
||||
When I view the courseware that has an entrance exam
|
||||
Then there should be an "Entrance Exam" chapter.'
|
||||
"""
|
||||
|
||||
# visit course info page and make sure there is not entrance exam section.
|
||||
self.course_info_page.visit()
|
||||
self.course_info_page.wait_for_page()
|
||||
entrance_exam_link_selector = 'div#accordion nav div h3 a'
|
||||
# visit courseware page and make sure there is not entrance exam chapter.
|
||||
self.courseware_page.visit()
|
||||
self.courseware_page.wait_for_page()
|
||||
self.assertFalse(element_has_text(
|
||||
page=self.course_info_page,
|
||||
css_selector='div ol li a',
|
||||
page=self.courseware_page,
|
||||
css_selector=entrance_exam_link_selector,
|
||||
text='Entrance Exam'
|
||||
))
|
||||
|
||||
@@ -1082,10 +1082,10 @@ class EntranceExamTest(UniqueCourseTest):
|
||||
AutoAuthPage(self.browser, course_id=self.course_id, staff=False).visit()
|
||||
|
||||
# visit course info page and make sure there is an "Entrance Exam" section.
|
||||
self.course_info_page.visit()
|
||||
self.course_info_page.wait_for_page()
|
||||
self.courseware_page.visit()
|
||||
self.courseware_page.wait_for_page()
|
||||
self.assertTrue(element_has_text(
|
||||
page=self.course_info_page,
|
||||
css_selector='div ol li a',
|
||||
page=self.courseware_page,
|
||||
css_selector=entrance_exam_link_selector,
|
||||
text='Entrance Exam'
|
||||
))
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.contrib.auth.models import AnonymousUser
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
|
||||
from xblock.core import XBlock
|
||||
|
||||
from xmodule.course_module import (
|
||||
@@ -25,12 +26,13 @@ from xmodule.partitions.partitions import NoSuchUserPartitionError, NoSuchUserPa
|
||||
from external_auth.models import ExternalAuthMap
|
||||
from courseware.masquerade import get_masquerade_role, is_masquerading_as_student
|
||||
from student import auth
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAllowed
|
||||
from student.roles import (
|
||||
GlobalStaff, CourseStaffRole, CourseInstructorRole,
|
||||
OrgStaffRole, OrgInstructorRole, CourseBetaTesterRole
|
||||
)
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAllowed
|
||||
from util.milestones_helpers import get_pre_requisite_courses_not_completed
|
||||
|
||||
import dogstats_wrapper as dog_stats_api
|
||||
|
||||
DEBUG_ACCESS = False
|
||||
|
||||
@@ -23,10 +23,8 @@ from courseware.model_data import FieldDataCache
|
||||
from courseware.module_render import get_module
|
||||
from student.models import CourseEnrollment
|
||||
import branding
|
||||
from util.milestones_helpers import get_required_content, calculate_entrance_exam_score
|
||||
from util.module_utils import yield_dynamic_descriptor_descendents
|
||||
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from .module_render import get_module_for_descriptor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -449,47 +447,3 @@ def get_problems_in_section(section):
|
||||
problem_descriptors[unicode(component.location)] = component
|
||||
|
||||
return problem_descriptors
|
||||
|
||||
|
||||
def get_entrance_exam_score(request, course):
|
||||
"""
|
||||
Get entrance exam score
|
||||
"""
|
||||
exam_key = UsageKey.from_string(course.entrance_exam_id)
|
||||
exam_descriptor = modulestore().get_item(exam_key)
|
||||
|
||||
def inner_get_module(descriptor):
|
||||
"""
|
||||
Delegate to get_module_for_descriptor.
|
||||
"""
|
||||
field_data_cache = FieldDataCache([descriptor], course.id, request.user)
|
||||
return get_module_for_descriptor(request.user, request, descriptor, field_data_cache, course.id)
|
||||
|
||||
exam_module_generators = yield_dynamic_descriptor_descendents(
|
||||
exam_descriptor,
|
||||
inner_get_module
|
||||
)
|
||||
exam_modules = [module for module in exam_module_generators]
|
||||
return calculate_entrance_exam_score(request.user, course, exam_modules)
|
||||
|
||||
|
||||
def get_entrance_exam_content_info(request, course):
|
||||
"""
|
||||
Get the entrance exam content information e.g. chapter, exam passing state.
|
||||
return exam chapter and its passing state.
|
||||
"""
|
||||
required_content = get_required_content(course, request.user)
|
||||
exam_chapter = None
|
||||
is_exam_passed = True
|
||||
# Iterating the list of required content of this course.
|
||||
for content in required_content:
|
||||
# database lookup to required content pointer
|
||||
usage_key = course.id.make_usage_key_from_deprecated_string(content)
|
||||
module_item = modulestore().get_item(usage_key)
|
||||
if not module_item.hide_from_toc and module_item.is_entrance_exam:
|
||||
# Here we are looking for entrance exam module/chapter in required_content.
|
||||
# If module_item is an entrance exam chapter then set and return its info e.g. exam chapter, exam state.
|
||||
exam_chapter = module_item
|
||||
is_exam_passed = False
|
||||
break
|
||||
return exam_chapter, is_exam_passed
|
||||
|
||||
171
lms/djangoapps/courseware/entrance_exams.py
Normal file
171
lms/djangoapps/courseware/entrance_exams.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
This file contains all entrance exam related utils/logic.
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.model_data import FieldDataCache
|
||||
from courseware.models import StudentModule
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from student.models import EntranceExamConfiguration
|
||||
from util.milestones_helpers import get_required_content
|
||||
from util.module_utils import yield_dynamic_descriptor_descendents
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
def feature_is_enabled():
|
||||
"""
|
||||
Checks to see if the Entrance Exams feature is enabled
|
||||
Use this operation instead of checking the feature flag all over the place
|
||||
"""
|
||||
return settings.FEATURES.get('ENTRANCE_EXAMS', False)
|
||||
|
||||
|
||||
def course_has_entrance_exam(course):
|
||||
"""
|
||||
Checks to see if a course is properly configured for an entrance exam
|
||||
"""
|
||||
if not feature_is_enabled():
|
||||
return False
|
||||
if not course.entrance_exam_enabled:
|
||||
return False
|
||||
if not course.entrance_exam_id:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def user_can_skip_entrance_exam(request, user, course):
|
||||
"""
|
||||
Checks all of the various override conditions for a user to skip an entrance exam
|
||||
Begin by short-circuiting if the course does not have an entrance exam
|
||||
"""
|
||||
if not course_has_entrance_exam(course):
|
||||
return True
|
||||
if not user.is_authenticated():
|
||||
return False
|
||||
if has_access(user, 'staff', course):
|
||||
return True
|
||||
if EntranceExamConfiguration.user_can_skip_entrance_exam(user, course.id):
|
||||
return True
|
||||
if not get_entrance_exam_content(request, course):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def user_has_passed_entrance_exam(request, course):
|
||||
"""
|
||||
Checks to see if the user has attained a sufficient score to pass the exam
|
||||
Begin by short-circuiting if the course does not have an entrance exam
|
||||
"""
|
||||
if not course_has_entrance_exam(course):
|
||||
return True
|
||||
if not request.user.is_authenticated():
|
||||
return False
|
||||
entrance_exam_score = get_entrance_exam_score(request, course)
|
||||
if entrance_exam_score >= course.entrance_exam_minimum_score_pct:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def user_must_complete_entrance_exam(request, user, course):
|
||||
"""
|
||||
Some courses can be gated on an Entrance Exam, which is a specially-configured chapter module which
|
||||
presents users with a problem set which they must complete. This particular workflow determines
|
||||
whether or not the user is allowed to clear the Entrance Exam gate and access the rest of the course.
|
||||
"""
|
||||
# First, let's see if the user is allowed to skip
|
||||
if user_can_skip_entrance_exam(request, user, course):
|
||||
return False
|
||||
# If they can't actually skip the exam, we'll need to see if they've already passed it
|
||||
if user_has_passed_entrance_exam(request, course):
|
||||
return False
|
||||
# Can't skip, haven't passed, must take the exam
|
||||
return True
|
||||
|
||||
|
||||
def _calculate_entrance_exam_score(user, course_descriptor, exam_modules):
|
||||
"""
|
||||
Calculates the score (percent) of the entrance exam using the provided modules
|
||||
"""
|
||||
# All of the exam module ids
|
||||
exam_module_ids = [exam_module.location for exam_module in exam_modules]
|
||||
|
||||
# All of the corresponding student module records
|
||||
student_modules = StudentModule.objects.filter(
|
||||
student=user,
|
||||
course_id=course_descriptor.id,
|
||||
module_state_key__in=exam_module_ids,
|
||||
)
|
||||
student_module_dict = {}
|
||||
for student_module in student_modules:
|
||||
student_module_dict[unicode(student_module.module_state_key)] = {
|
||||
'grade': student_module.grade,
|
||||
'max_grade': student_module.max_grade
|
||||
}
|
||||
exam_percentage = 0
|
||||
module_percentages = []
|
||||
ignore_categories = ['course', 'chapter', 'sequential', 'vertical']
|
||||
|
||||
for module in exam_modules:
|
||||
if module.graded and module.category not in ignore_categories:
|
||||
module_percentage = 0
|
||||
module_location = unicode(module.location)
|
||||
if module_location in student_module_dict and student_module_dict[module_location]['max_grade']:
|
||||
student_module = student_module_dict[module_location]
|
||||
module_percentage = student_module['grade'] / student_module['max_grade']
|
||||
|
||||
module_percentages.append(module_percentage)
|
||||
if module_percentages:
|
||||
exam_percentage = sum(module_percentages) / float(len(module_percentages))
|
||||
return exam_percentage
|
||||
|
||||
|
||||
def get_entrance_exam_score(request, course):
|
||||
"""
|
||||
Gather the set of modules which comprise the entrance exam
|
||||
Note that 'request' may not actually be a genuine request, due to the
|
||||
circular nature of module_render calling entrance_exams and get_module_for_descriptor
|
||||
being used here. In some use cases, the caller is actually mocking a request, although
|
||||
in these scenarios the 'user' child object can be trusted and used as expected.
|
||||
It's a much larger refactoring job to break this legacy mess apart, unfortunately.
|
||||
"""
|
||||
exam_key = UsageKey.from_string(course.entrance_exam_id)
|
||||
exam_descriptor = modulestore().get_item(exam_key)
|
||||
|
||||
def inner_get_module(descriptor):
|
||||
"""
|
||||
Delegate to get_module_for_descriptor (imported here to avoid circular reference)
|
||||
"""
|
||||
from courseware.module_render import get_module_for_descriptor
|
||||
field_data_cache = FieldDataCache([descriptor], course.id, request.user)
|
||||
return get_module_for_descriptor(
|
||||
request.user,
|
||||
request,
|
||||
descriptor,
|
||||
field_data_cache,
|
||||
course.id
|
||||
)
|
||||
|
||||
exam_module_generators = yield_dynamic_descriptor_descendents(
|
||||
exam_descriptor,
|
||||
inner_get_module
|
||||
)
|
||||
exam_modules = [module for module in exam_module_generators]
|
||||
return _calculate_entrance_exam_score(request.user, course, exam_modules)
|
||||
|
||||
|
||||
def get_entrance_exam_content(request, course):
|
||||
"""
|
||||
Get the entrance exam content information (ie, chapter module)
|
||||
"""
|
||||
required_content = get_required_content(course, request.user)
|
||||
|
||||
exam_module = None
|
||||
for content in required_content:
|
||||
usage_key = course.id.make_usage_key_from_deprecated_string(content)
|
||||
module_item = modulestore().get_item(usage_key)
|
||||
if not module_item.hide_from_toc and module_item.is_entrance_exam:
|
||||
exam_module = module_item
|
||||
break
|
||||
return exam_module
|
||||
@@ -23,12 +23,17 @@ from django.core.context_processors import csrf
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.test.client import RequestFactory
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from courseware.access import has_access, get_user_role
|
||||
from courseware.masquerade import setup_masquerade
|
||||
from courseware.model_data import FieldDataCache, DjangoKeyValueStore
|
||||
from courseware.entrance_exams import (
|
||||
get_entrance_exam_score,
|
||||
user_must_complete_entrance_exam
|
||||
)
|
||||
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
||||
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes
|
||||
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
|
||||
@@ -132,11 +137,17 @@ def toc_for_course(request, course, active_chapter, active_section, field_data_c
|
||||
if course_module is None:
|
||||
return None
|
||||
|
||||
# Check to see if the course is gated on milestone-required content (such as an Entrance Exam)
|
||||
toc_chapters = list()
|
||||
chapters = course_module.get_display_items()
|
||||
|
||||
# See if the course is gated by one or more content milestones
|
||||
required_content = milestones_helpers.get_required_content(course, request.user)
|
||||
|
||||
chapters = list()
|
||||
for chapter in course_module.get_display_items():
|
||||
# The user may not actually have to complete the entrance exam, if one is required
|
||||
if not user_must_complete_entrance_exam(request, request.user, course):
|
||||
required_content = [content for content in required_content if not content == course.entrance_exam_id]
|
||||
|
||||
for chapter in chapters:
|
||||
# Only show required content, if there is required content
|
||||
# chapter.hide_from_toc is read-only (boo)
|
||||
local_hide_from_toc = False
|
||||
@@ -162,11 +173,13 @@ def toc_for_course(request, course, active_chapter, active_section, field_data_c
|
||||
'active': active,
|
||||
'graded': section.graded,
|
||||
})
|
||||
chapters.append({'display_name': chapter.display_name_with_default,
|
||||
'url_name': chapter.url_name,
|
||||
'sections': sections,
|
||||
'active': chapter.url_name == active_chapter})
|
||||
return chapters
|
||||
toc_chapters.append({
|
||||
'display_name': chapter.display_name_with_default,
|
||||
'url_name': chapter.url_name,
|
||||
'sections': sections,
|
||||
'active': chapter.url_name == active_chapter
|
||||
})
|
||||
return toc_chapters
|
||||
|
||||
|
||||
def get_module(user, request, usage_key, field_data_cache,
|
||||
@@ -361,20 +374,6 @@ def get_module_system_for_user(user, field_data_cache,
|
||||
request_token=request_token,
|
||||
)
|
||||
|
||||
def _calculate_entrance_exam_score(user, course_descriptor):
|
||||
"""
|
||||
Internal helper to calculate a user's score for a course's entrance exam
|
||||
"""
|
||||
exam_key = UsageKey.from_string(course_descriptor.entrance_exam_id)
|
||||
exam_descriptor = modulestore().get_item(exam_key)
|
||||
exam_module_generators = yield_dynamic_descriptor_descendents(
|
||||
exam_descriptor,
|
||||
inner_get_module
|
||||
)
|
||||
exam_modules = [module for module in exam_module_generators]
|
||||
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):
|
||||
"""
|
||||
Internal helper to handle milestone fulfillments for the specified content module
|
||||
@@ -388,7 +387,10 @@ def get_module_system_for_user(user, field_data_cache,
|
||||
entrance_exam_enabled = getattr(course, 'entrance_exam_enabled', False)
|
||||
in_entrance_exam = getattr(content, 'in_entrance_exam', False)
|
||||
if entrance_exam_enabled and in_entrance_exam:
|
||||
exam_pct = _calculate_entrance_exam_score(user, course)
|
||||
# We don't have access to the true request object in this context, but we can use a mock
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
exam_pct = get_entrance_exam_score(request, course)
|
||||
if exam_pct >= course.entrance_exam_minimum_score_pct:
|
||||
exam_key = UsageKey.from_string(course.entrance_exam_id)
|
||||
relationship_types = milestones_helpers.get_milestone_relationship_types()
|
||||
@@ -398,7 +400,7 @@ def get_module_system_for_user(user, field_data_cache,
|
||||
relationship=relationship_types['FULFILLS']
|
||||
)
|
||||
# Add each milestone to the user's set...
|
||||
user = {'id': user.id}
|
||||
user = {'id': request.user.id}
|
||||
for milestone in content_milestones:
|
||||
milestones_helpers.add_user_milestone(user, milestone)
|
||||
|
||||
@@ -437,15 +439,13 @@ def get_module_system_for_user(user, field_data_cache,
|
||||
|
||||
dog_stats_api.increment("lms.courseware.question_answered", tags=tags)
|
||||
|
||||
# If we're using the awesome edx-milestones app, we need to cycle
|
||||
# through the fulfillment scenarios to see if any are now applicable
|
||||
# Cycle through the milestone fulfillment scenarios to see if any are now applicable
|
||||
# thanks to the updated grading information that was just submitted
|
||||
if settings.FEATURES.get('MILESTONES_APP', False):
|
||||
_fulfill_content_milestones(
|
||||
user,
|
||||
course_id,
|
||||
descriptor.location,
|
||||
)
|
||||
_fulfill_content_milestones(
|
||||
user,
|
||||
course_id,
|
||||
descriptor.location,
|
||||
)
|
||||
|
||||
def publish(block, event_type, event):
|
||||
"""A function that allows XModules to publish events."""
|
||||
|
||||
@@ -3,9 +3,11 @@ This module is essentially a broker to xmodule/tabs.py -- it was originally intr
|
||||
perform some LMS-specific tab display gymnastics for the Entrance Exams feature
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.entrance_exams import user_must_complete_entrance_exam
|
||||
from student.models import CourseEnrollment, EntranceExamConfiguration
|
||||
from xmodule.tabs import CourseTabList
|
||||
|
||||
@@ -25,29 +27,15 @@ def get_course_tab_list(course, user):
|
||||
user_is_enrolled
|
||||
)
|
||||
|
||||
# Entrance Exams Feature
|
||||
# If the course has an entrance exam, we'll need to see if the user has not passed it
|
||||
# If so, we'll need to hide away all of the tabs except for Courseware and Instructor
|
||||
entrance_exam_mode = False
|
||||
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
|
||||
if getattr(course, 'entrance_exam_enabled', False):
|
||||
course_milestones_paths = milestones_helpers.get_course_milestones_fulfillment_paths(
|
||||
unicode(course.id),
|
||||
milestones_helpers.serialize_user(user)
|
||||
)
|
||||
for __, value in course_milestones_paths.iteritems():
|
||||
if len(value.get('content', [])):
|
||||
for content in value['content']:
|
||||
if content == course.entrance_exam_id \
|
||||
and not EntranceExamConfiguration.user_can_skip_entrance_exam(user, course.id):
|
||||
entrance_exam_mode = True
|
||||
break
|
||||
|
||||
# Now that we've loaded the tabs for this course, perform the Entrance Exam mode work
|
||||
# Majority case is no entrance exam defined
|
||||
# Now that we've loaded the tabs for this course, perform the Entrance Exam work
|
||||
# If the user has to take an entrance exam, we'll need to hide away all of the tabs
|
||||
# except for the Courseware and Instructor tabs (latter is only viewed if applicable)
|
||||
# We don't have access to the true request object in this context, but we can use a mock
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
course_tab_list = []
|
||||
for tab in xmodule_tab_list:
|
||||
if entrance_exam_mode:
|
||||
if user_must_complete_entrance_exam(request, user, course):
|
||||
# Hide all of the tabs except for 'Courseware' and 'Instructor'
|
||||
# Rename 'Courseware' tab to 'Entrance Exam'
|
||||
if tab.type not in ['courseware', 'instructor']:
|
||||
|
||||
@@ -7,18 +7,34 @@ from django.core.urlresolvers import reverse
|
||||
|
||||
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 courseware.tests.factories import UserFactory, InstructorFactory, StaffFactory
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from courseware.entrance_exams import (
|
||||
course_has_entrance_exam,
|
||||
get_entrance_exam_content,
|
||||
get_entrance_exam_score,
|
||||
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 util import milestones_helpers
|
||||
from util.milestones_helpers import (
|
||||
add_milestone,
|
||||
add_course_milestone,
|
||||
get_namespace_choices,
|
||||
generate_milestone_namespace,
|
||||
add_course_content_milestone,
|
||||
get_milestone_relationship_types,
|
||||
seed_milestone_relationship_types,
|
||||
)
|
||||
from student.models import CourseEnrollment
|
||||
from mock import patch
|
||||
from student.tests.factories import CourseEnrollmentFactory, AnonymousUserFactory
|
||||
from mock import patch, Mock
|
||||
import mock
|
||||
|
||||
|
||||
class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
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
|
||||
@@ -110,8 +126,8 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
display_name="Exam Problem - Problem 3"
|
||||
)
|
||||
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
|
||||
namespace_choices = milestones_helpers.get_namespace_choices()
|
||||
milestone_namespace = milestones_helpers.generate_milestone_namespace(
|
||||
namespace_choices = get_namespace_choices()
|
||||
milestone_namespace = generate_milestone_namespace(
|
||||
namespace_choices.get('ENTRANCE_EXAM'),
|
||||
self.course.id
|
||||
)
|
||||
@@ -120,20 +136,21 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
'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(
|
||||
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
|
||||
)
|
||||
milestones_helpers.add_course_content_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
|
||||
@@ -282,14 +299,14 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
self.assertIn('Exam Problem - Problem 1', resp.content)
|
||||
self.assertIn('Exam Problem - Problem 2', resp.content)
|
||||
|
||||
def test_entrance_exam_content_info(self):
|
||||
def test_get_entrance_exam_content(self):
|
||||
"""
|
||||
test entrance exam content info method
|
||||
test get entrance exam content method
|
||||
"""
|
||||
exam_chapter, is_exam_passed = get_entrance_exam_content_info(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(is_exam_passed, False)
|
||||
self.assertFalse(user_has_passed_entrance_exam(self.request, self.course))
|
||||
|
||||
# Pass the entrance exam
|
||||
# pylint: disable=maybe-no-member,no-member
|
||||
@@ -309,9 +326,18 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
)._xmodule
|
||||
module.system.publish(self.problem_1, 'grade', grade_dict)
|
||||
|
||||
exam_chapter, is_exam_passed = get_entrance_exam_content_info(self.request, self.course)
|
||||
# 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.assertEqual(is_exam_passed, True)
|
||||
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):
|
||||
@@ -323,7 +349,7 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
|
||||
# Pass the entrance exam
|
||||
# pylint: disable=maybe-no-member,no-member
|
||||
grade_dict = {'value': 1, 'max_value': 2, 'user_id': self.request.user.id}
|
||||
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,
|
||||
@@ -339,9 +365,18 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
)._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)
|
||||
# 50 percent exam score should be achieved.
|
||||
self.assertEqual(exam_score * 100, 50)
|
||||
self.assertGreater(exam_score * 100, 50)
|
||||
|
||||
def test_entrance_exam_requirement_message(self):
|
||||
"""
|
||||
@@ -364,6 +399,12 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
"""
|
||||
Unit Test: entrance exam message should not be present outside the context of entrance exam subsection.
|
||||
"""
|
||||
# Login as staff to avoid redirect to entrance exam
|
||||
self.client.logout()
|
||||
staff_user = StaffFactory(course_key=self.course.id)
|
||||
self.client.login(username=staff_user.username, password='test')
|
||||
CourseEnrollment.enroll(staff_user, self.course.id)
|
||||
|
||||
url = reverse(
|
||||
'courseware_section',
|
||||
kwargs={
|
||||
@@ -409,13 +450,22 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
)._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)
|
||||
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})
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True, 'MILESTONES_APP': True})
|
||||
def test_entrance_exam_gating(self):
|
||||
"""
|
||||
Unit Test: test_entrance_exam_gating
|
||||
@@ -486,7 +536,7 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
self.assertIn(toc_section, unlocked_toc)
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
|
||||
def test_skip_entrance_exame_gating(self):
|
||||
def test_skip_entrance_exam_gating(self):
|
||||
"""
|
||||
Tests gating is disabled if skip entrance exam is set for a user.
|
||||
"""
|
||||
@@ -519,3 +569,137 @@ class EntranceExamTestCases(ModuleStoreTestCase):
|
||||
)
|
||||
for toc_section in self.expected_unlocked_toc:
|
||||
self.assertIn(toc_section, unlocked_toc)
|
||||
|
||||
def test_entrance_exam_gating_for_staff(self):
|
||||
"""
|
||||
Tests gating is disabled if user is member of staff.
|
||||
"""
|
||||
|
||||
# Login as member of staff
|
||||
self.client.logout()
|
||||
staff_user = StaffFactory(course_key=self.course.id)
|
||||
staff_user.is_staff = True
|
||||
self.client.login(username=staff_user.username, password='test')
|
||||
|
||||
# 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
|
||||
)
|
||||
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):
|
||||
"""
|
||||
Test courseware access page without passing entrance exam
|
||||
"""
|
||||
url = reverse(
|
||||
'courseware_chapter',
|
||||
kwargs={'course_id': unicode(self.course.id), 'chapter': self.chapter.url_name}
|
||||
)
|
||||
response = self.client.get(url)
|
||||
redirect_url = reverse('courseware', args=[unicode(self.course.id)])
|
||||
self.assertRedirects(response, redirect_url, status_code=302, target_status_code=302)
|
||||
response = self.client.get(redirect_url)
|
||||
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):
|
||||
"""
|
||||
Test courseware access page without passing entrance exam
|
||||
"""
|
||||
url = reverse('info', args=[unicode(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
redirect_url = reverse('courseware', args=[unicode(self.course.id)])
|
||||
self.assertRedirects(response, redirect_url, status_code=302, target_status_code=302)
|
||||
response = self.client.get(redirect_url)
|
||||
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):
|
||||
"""
|
||||
Test courseware access page after passing entrance exam
|
||||
"""
|
||||
# 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):
|
||||
"""
|
||||
Test courseware access page without passing entrance exam but with staff user
|
||||
"""
|
||||
self.logout()
|
||||
staff_user = StaffFactory.create(course_key=self.course.id)
|
||||
self.login(staff_user.email, 'test')
|
||||
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
|
||||
"""
|
||||
self.logout()
|
||||
staff_user = StaffFactory.create(course_key=self.course.id)
|
||||
self.login(staff_user.email, 'test')
|
||||
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': False})
|
||||
def test_courseware_page_access_when_entrance_exams_disabled(self):
|
||||
"""
|
||||
Test courseware page access when ENTRANCE_EXAMS feature is disabled
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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={
|
||||
'entrance_exam_enabled': True,
|
||||
}
|
||||
)
|
||||
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(
|
||||
)
|
||||
self.assertTrue(user_has_passed_entrance_exam(self.request, course))
|
||||
|
||||
def _assert_chapter_loaded(self, course, chapter):
|
||||
"""
|
||||
Asserts courseware chapter load successfully.
|
||||
"""
|
||||
url = reverse(
|
||||
'courseware_chapter',
|
||||
kwargs={'course_id': unicode(course.id), 'chapter': chapter.url_name}
|
||||
)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -10,7 +10,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from courseware.courses import get_course_by_id
|
||||
from courseware.tests.helpers import get_request_for_user, LoginEnrollmentTestCase
|
||||
from courseware.tests.factories import InstructorFactory
|
||||
from courseware.tests.factories import InstructorFactory, StaffFactory
|
||||
from xmodule import tabs
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
TEST_DATA_MIXED_TOY_MODULESTORE, TEST_DATA_MIXED_CLOSED_MODULESTORE
|
||||
@@ -146,15 +146,18 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
Unit Test: test_get_course_tabs_list_entrance_exam_enabled
|
||||
"""
|
||||
entrance_exam = ItemFactory.create(
|
||||
category="chapter", parent_location=self.course.location,
|
||||
data="Exam Data", display_name="Entrance Exam"
|
||||
category="chapter",
|
||||
parent_location=self.course.location,
|
||||
data="Exam Data",
|
||||
display_name="Entrance Exam",
|
||||
is_entrance_exam=True
|
||||
)
|
||||
entrance_exam.is_entrance_exam = True
|
||||
milestone = {
|
||||
'name': 'Test Milestone',
|
||||
'namespace': '{}.entrance_exams'.format(unicode(self.course.id)),
|
||||
'description': 'Testing Courseware Tabs'
|
||||
}
|
||||
self.user.is_staff = False
|
||||
self.course.entrance_exam_enabled = True
|
||||
self.course.entrance_exam_id = unicode(entrance_exam.location)
|
||||
milestone = milestones_helpers.add_milestone(milestone)
|
||||
@@ -170,10 +173,9 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
milestone
|
||||
)
|
||||
course_tab_list = get_course_tab_list(self.course, self.user)
|
||||
self.assertEqual(len(course_tab_list), 2)
|
||||
self.assertEqual(len(course_tab_list), 1)
|
||||
self.assertEqual(course_tab_list[0]['tab_id'], 'courseware')
|
||||
self.assertEqual(course_tab_list[0]['name'], 'Entrance Exam')
|
||||
self.assertEqual(course_tab_list[1]['tab_id'], 'instructor')
|
||||
|
||||
def test_get_course_tabs_list_skipped_entrance_exam(self):
|
||||
"""
|
||||
@@ -198,6 +200,19 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
course_tab_list = get_course_tab_list(self.course, self.user)
|
||||
self.assertEqual(len(course_tab_list), 5)
|
||||
|
||||
def test_course_tabs_list_for_staff_members(self):
|
||||
"""
|
||||
Tests tab list is not limited if user is member of staff
|
||||
and has not passed entrance exam.
|
||||
"""
|
||||
# Login as member of staff
|
||||
self.client.logout()
|
||||
staff_user = StaffFactory(course_key=self.course.id)
|
||||
self.client.login(username=staff_user.username, password='test')
|
||||
|
||||
course_tab_list = get_course_tab_list(self.course, staff_user)
|
||||
self.assertEqual(len(course_tab_list), 5)
|
||||
|
||||
|
||||
class TextBookTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
@@ -32,12 +32,22 @@ from markupsafe import escape
|
||||
|
||||
from courseware import grades
|
||||
from courseware.access import has_access, _adjust_start_date_for_beta_testers
|
||||
from courseware.courses import get_courses, get_course, get_studio_url, get_course_with_access, sort_by_announcement,\
|
||||
get_entrance_exam_content_info
|
||||
from courseware.courses import sort_by_start_date, get_entrance_exam_score
|
||||
from courseware.courses import (
|
||||
get_courses, get_course,
|
||||
get_studio_url, get_course_with_access,
|
||||
sort_by_announcement,
|
||||
sort_by_start_date,
|
||||
)
|
||||
from courseware.masquerade import setup_masquerade
|
||||
from courseware.model_data import FieldDataCache
|
||||
from .module_render import toc_for_course, get_module_for_descriptor, get_module
|
||||
from .entrance_exams import (
|
||||
course_has_entrance_exam,
|
||||
get_entrance_exam_content,
|
||||
get_entrance_exam_score,
|
||||
user_must_complete_entrance_exam,
|
||||
user_has_passed_entrance_exam
|
||||
)
|
||||
from courseware.models import StudentModule, StudentModuleHistory
|
||||
from course_modes.models import CourseMode
|
||||
|
||||
@@ -366,6 +376,15 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
user.id, unicode(course.id))
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
# Entrance Exam Check
|
||||
# If the course has an entrance exam and the requested chapter is NOT the entrance exam, and
|
||||
# the user hasn't yet met the criteria to bypass the entrance exam, redirect them to the exam.
|
||||
if chapter and course_has_entrance_exam(course):
|
||||
chapter_descriptor = course.get_child_by(lambda m: m.location.name == chapter)
|
||||
if chapter_descriptor and not getattr(chapter_descriptor, 'is_entrance_exam', False) \
|
||||
and user_must_complete_entrance_exam(request, user, course):
|
||||
log.info(u'User %d tried to view course %s without passing entrance exam', user.id, unicode(course.id))
|
||||
return redirect(reverse('courseware', args=[unicode(course.id)]))
|
||||
# check to see if there is a required survey that must be taken before
|
||||
# the user can access the course.
|
||||
if survey.utils.must_answer_survey(course, user):
|
||||
@@ -412,9 +431,9 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
return render_to_response('courseware/courseware.html', context)
|
||||
elif chapter is None:
|
||||
# Check first to see if we should instead redirect the user to an Entrance Exam
|
||||
if settings.FEATURES.get('ENTRANCE_EXAMS', False) and course.entrance_exam_enabled:
|
||||
exam_chapter, __ = get_entrance_exam_content_info(request, course)
|
||||
if exam_chapter is not None:
|
||||
if course_has_entrance_exam(course):
|
||||
exam_chapter = get_entrance_exam_content(request, course)
|
||||
if exam_chapter:
|
||||
exam_section = None
|
||||
if exam_chapter.get_children():
|
||||
exam_section = exam_chapter.get_children()[0]
|
||||
@@ -454,13 +473,12 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
|
||||
raise Http404
|
||||
|
||||
if settings.FEATURES.get('ENTRANCE_EXAMS', False) and course.entrance_exam_enabled:
|
||||
if course_has_entrance_exam(course):
|
||||
# Message should not appear outside the context of entrance exam subsection.
|
||||
# if section is none then we don't need to show message on welcome back screen also.
|
||||
if getattr(chapter_module, 'is_entrance_exam', False) and section is not None:
|
||||
__, is_exam_passed = get_entrance_exam_content_info(request, course)
|
||||
context['entrance_exam_current_score'] = get_entrance_exam_score(request, course)
|
||||
context['entrance_exam_passed'] = is_exam_passed
|
||||
context['entrance_exam_passed'] = user_has_passed_entrance_exam(request, course)
|
||||
|
||||
if section is not None:
|
||||
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.location.name == section)
|
||||
@@ -670,6 +688,11 @@ def course_info(request, course_id):
|
||||
with modulestore().bulk_operations(course_key):
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
# If the user needs to take an entrance exam to access this course, then we'll need
|
||||
# to send them to that specific course module before allowing them into other areas
|
||||
if user_must_complete_entrance_exam(request, request.user, course):
|
||||
return redirect(reverse('courseware', args=[unicode(course.id)]))
|
||||
|
||||
# check to see if there is a required survey that must be taken before
|
||||
# the user can access the course.
|
||||
if request.user.is_authenticated() and survey.utils.must_answer_survey(course, request.user):
|
||||
|
||||
Reference in New Issue
Block a user