added user’s pass status to problem_check response if problem is in
Entrance Exam. unit test to check presence of entrance_exam_passed changed based on Matt's feedback changes after Asad's feedback
This commit is contained in:
@@ -222,6 +222,27 @@ class CourseFixture(XBlockContainerFixture):
|
||||
"""
|
||||
self._configure_course()
|
||||
|
||||
@property
|
||||
def course_outline(self):
|
||||
"""
|
||||
Retrieves course outline in JSON format.
|
||||
"""
|
||||
url = STUDIO_BASE_URL + '/course/' + self._course_key + "?format=json"
|
||||
response = self.session.get(url, headers=self.headers)
|
||||
|
||||
if not response.ok:
|
||||
raise FixtureError(
|
||||
"Could not retrieve course outline json. Status was {0}".format(
|
||||
response.status_code))
|
||||
|
||||
try:
|
||||
course_outline_json = response.json()
|
||||
except ValueError:
|
||||
raise FixtureError(
|
||||
"Could not decode course outline as JSON: '{0}'".format(response)
|
||||
)
|
||||
return course_outline_json
|
||||
|
||||
@property
|
||||
def _course_location(self):
|
||||
"""
|
||||
|
||||
@@ -127,6 +127,33 @@ class CoursewarePage(CoursePage):
|
||||
# Wait for the unique exam code to appear.
|
||||
# elf.wait_for_element_presence(".proctored-exam-code", "unique exam code")
|
||||
|
||||
@property
|
||||
def entrance_exam_message_selector(self):
|
||||
"""
|
||||
Return the entrance exam status message selector on the top of courseware page.
|
||||
"""
|
||||
return self.q(css='#content .container section.course-content .sequential-status-message')
|
||||
|
||||
def has_entrance_exam_message(self):
|
||||
"""
|
||||
Returns boolean indicating presence entrance exam status message container div.
|
||||
"""
|
||||
return self.entrance_exam_message_selector.is_present()
|
||||
|
||||
def has_passed_message(self):
|
||||
"""
|
||||
Returns boolean indicating presence of passed message.
|
||||
"""
|
||||
return self.entrance_exam_message_selector.is_present() \
|
||||
and "You have passed the entrance exam" in self.entrance_exam_message_selector.text[0]
|
||||
|
||||
@property
|
||||
def chapter_count_in_navigation(self):
|
||||
"""
|
||||
Returns count of chapters available on LHS navigation.
|
||||
"""
|
||||
return len(self.q(css='nav.course-navigation a.chapter'))
|
||||
|
||||
@property
|
||||
def is_timer_bar_present(self):
|
||||
"""
|
||||
|
||||
@@ -84,6 +84,13 @@ class ProblemPage(PageObject):
|
||||
self.q(css='div.problem button.hint-button').click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def click_choice(self, choice_value):
|
||||
"""
|
||||
Click the choice input(radio, checkbox or option) where value matches `choice_value` in choice group.
|
||||
"""
|
||||
self.q(css='div.problem .choicegroup input[value="' + choice_value + '"]').click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def is_correct(self):
|
||||
"""
|
||||
Is there a "correct" status showing?
|
||||
|
||||
105
common/test/acceptance/tests/lms/test_lms_entrance_exams.py
Normal file
105
common/test/acceptance/tests/lms/test_lms_entrance_exams.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Bok choy acceptance tests for Entrance exams in the LMS
|
||||
"""
|
||||
from textwrap import dedent
|
||||
|
||||
from ..helpers import UniqueCourseTest
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.courseware import CoursewarePage
|
||||
from ...pages.lms.problem import ProblemPage
|
||||
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
|
||||
|
||||
class EntranceExamTest(UniqueCourseTest):
|
||||
"""
|
||||
Base class for tests of Entrance Exams in the LMS.
|
||||
"""
|
||||
USERNAME = "joe_student"
|
||||
EMAIL = "joe@example.com"
|
||||
|
||||
def setUp(self):
|
||||
super(EntranceExamTest, self).setUp()
|
||||
|
||||
self.xqueue_grade_response = None
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
|
||||
# Install a course with a hierarchy and problems
|
||||
course_fixture = CourseFixture(
|
||||
self.course_info['org'], self.course_info['number'],
|
||||
self.course_info['run'], self.course_info['display_name'],
|
||||
settings={
|
||||
'entrance_exam_enabled': 'true',
|
||||
'entrance_exam_minimum_score_pct': '50'
|
||||
}
|
||||
)
|
||||
|
||||
problem = self.get_problem()
|
||||
course_fixture.add_children(
|
||||
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
||||
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(problem)
|
||||
)
|
||||
).install()
|
||||
|
||||
entrance_exam_subsection = None
|
||||
outline = course_fixture.course_outline
|
||||
for child in outline['child_info']['children']:
|
||||
if child.get('display_name') == "Entrance Exam":
|
||||
entrance_exam_subsection = child['child_info']['children'][0]
|
||||
|
||||
if entrance_exam_subsection:
|
||||
course_fixture.create_xblock(entrance_exam_subsection['id'], problem)
|
||||
|
||||
# Auto-auth register for the course.
|
||||
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
|
||||
course_id=self.course_id, staff=False).visit()
|
||||
|
||||
def get_problem(self):
|
||||
""" Subclasses should override this to complete the fixture """
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class EntranceExamPassTest(EntranceExamTest):
|
||||
"""
|
||||
Tests the scenario when a student passes entrance exam.
|
||||
"""
|
||||
|
||||
def get_problem(self):
|
||||
"""
|
||||
Create a multiple choice problem
|
||||
"""
|
||||
xml = dedent("""
|
||||
<problem>
|
||||
<p>What is height of eiffel tower without the antenna?.</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup label="What is height of eiffel tower without the antenna?" type="MultipleChoice">
|
||||
<choice correct="false">324 meters<choicehint>Antenna is 24 meters high</choicehint></choice>
|
||||
<choice correct="true">300 meters</choice>
|
||||
<choice correct="false">224 meters</choice>
|
||||
<choice correct="false">400 meters</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
return XBlockFixtureDesc('problem', 'HEIGHT OF EIFFEL TOWER', data=xml)
|
||||
|
||||
def test_course_is_unblocked_as_soon_as_student_passes_entrance_exam(self):
|
||||
"""
|
||||
Scenario: Ensure that entrance exam status message is updated and courseware is unblocked as soon as
|
||||
student passes entrance exam.
|
||||
Given I have a course with entrance exam as pre-requisite
|
||||
When I pass entrance exam
|
||||
Then I can see complete TOC of course
|
||||
And I can see message indicating my pass status
|
||||
"""
|
||||
self.courseware_page.visit()
|
||||
problem_page = ProblemPage(self.browser)
|
||||
self.assertEqual(problem_page.wait_for_page().problem_name, 'HEIGHT OF EIFFEL TOWER')
|
||||
self.assertTrue(self.courseware_page.has_entrance_exam_message())
|
||||
self.assertFalse(self.courseware_page.has_passed_message())
|
||||
problem_page.click_choice('choice_1')
|
||||
problem_page.click_check()
|
||||
self.courseware_page.wait_for_page()
|
||||
self.assertTrue(self.courseware_page.has_passed_message())
|
||||
self.assertEqual(self.courseware_page.chapter_count_in_navigation, 2)
|
||||
@@ -6,6 +6,7 @@ from django.conf import settings
|
||||
from courseware.access import has_access
|
||||
from courseware.model_data import FieldDataCache, ScoresClient
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from opaque_keys.edx.locator import BlockUsageLocator
|
||||
from student.models import EntranceExamConfiguration
|
||||
from util.milestones_helpers import get_required_content
|
||||
from util.module_utils import yield_dynamic_descriptor_descendants
|
||||
@@ -89,14 +90,25 @@ def _calculate_entrance_exam_score(user, course_descriptor, exam_modules):
|
||||
"""
|
||||
student_module_dict = {}
|
||||
scores_client = ScoresClient(course_descriptor.id, user.id)
|
||||
locations = [exam_module.location for exam_module in exam_modules]
|
||||
# removing branch and version from exam modules locator
|
||||
# otherwise student module would not return scores since module usage keys would not match
|
||||
locations = [
|
||||
BlockUsageLocator(
|
||||
course_key=course_descriptor.id,
|
||||
block_type=exam_module.location.block_type,
|
||||
block_id=exam_module.location.block_id
|
||||
)
|
||||
if isinstance(exam_module.location, BlockUsageLocator) and exam_module.location.version
|
||||
else exam_module.location
|
||||
for exam_module in exam_modules
|
||||
]
|
||||
scores_client.fetch_scores(locations)
|
||||
|
||||
# Iterate over all of the exam modules to get score of user for each of them
|
||||
for exam_module in exam_modules:
|
||||
exam_module_score = scores_client.get(exam_module.location)
|
||||
for index, exam_module in enumerate(exam_modules):
|
||||
exam_module_score = scores_client.get(locations[index])
|
||||
if exam_module_score:
|
||||
student_module_dict[unicode(exam_module.location)] = {
|
||||
student_module_dict[unicode(locations[index])] = {
|
||||
'grade': exam_module_score.correct,
|
||||
'max_grade': exam_module_score.total
|
||||
}
|
||||
@@ -104,10 +116,10 @@ def _calculate_entrance_exam_score(user, course_descriptor, exam_modules):
|
||||
module_percentages = []
|
||||
ignore_categories = ['course', 'chapter', 'sequential', 'vertical']
|
||||
|
||||
for module in exam_modules:
|
||||
for index, module in enumerate(exam_modules):
|
||||
if module.graded and module.category not in ignore_categories:
|
||||
module_percentage = 0
|
||||
module_location = unicode(module.location)
|
||||
module_location = unicode(locations[index])
|
||||
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']
|
||||
|
||||
@@ -38,7 +38,8 @@ from courseware.model_data import DjangoKeyValueStore, FieldDataCache, set_score
|
||||
from courseware.models import SCORE_CHANGED
|
||||
from courseware.entrance_exams import (
|
||||
get_entrance_exam_score,
|
||||
user_must_complete_entrance_exam
|
||||
user_must_complete_entrance_exam,
|
||||
user_has_passed_entrance_exam
|
||||
)
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from eventtracking import tracker
|
||||
@@ -1062,6 +1063,12 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course
|
||||
try:
|
||||
with tracker.get_tracker().context(tracking_context_name, tracking_context):
|
||||
resp = instance.handle(handler, req, suffix)
|
||||
if suffix == 'problem_check' \
|
||||
and course \
|
||||
and getattr(course, 'entrance_exam_enabled', False) \
|
||||
and getattr(instance, 'in_entrance_exam', False):
|
||||
ee_data = {'entrance_exam_passed': user_has_passed_entrance_exam(request, course)}
|
||||
resp = append_data_to_webob_response(resp, ee_data)
|
||||
|
||||
except NoSuchHandlerError:
|
||||
log.exception("XBlock %s attempted to access missing handler %r", instance, handler)
|
||||
@@ -1178,3 +1185,22 @@ def _check_files_limits(files):
|
||||
return msg
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def append_data_to_webob_response(response, data):
|
||||
"""
|
||||
Appends data to a JSON webob response.
|
||||
|
||||
Arguments:
|
||||
response (webob response object): the webob response object that needs to be modified
|
||||
data (dict): dictionary containing data that needs to be appended to response body
|
||||
|
||||
Returns:
|
||||
(webob response object): webob response with updated body.
|
||||
|
||||
"""
|
||||
if getattr(response, 'content_type', None) == 'application/json':
|
||||
response_data = json.loads(response.body)
|
||||
response_data.update(data)
|
||||
response.body = json.dumps(response_data)
|
||||
return response
|
||||
|
||||
@@ -294,8 +294,9 @@ def get_course_tab_list(request, course):
|
||||
# If the user has to take an entrance exam, we'll need to hide away all but the
|
||||
# "Courseware" tab. The tab is then renamed as "Entrance Exam".
|
||||
course_tab_list = []
|
||||
must_complete_ee = user_must_complete_entrance_exam(request, user, course)
|
||||
for tab in xmodule_tab_list:
|
||||
if user_must_complete_entrance_exam(request, user, course):
|
||||
if must_complete_ee:
|
||||
# Hide all of the tabs except for 'Courseware'
|
||||
# Rename 'Courseware' tab to 'Entrance Exam'
|
||||
if tab.type is not 'courseware':
|
||||
|
||||
@@ -4,10 +4,12 @@ Tests use cases related to LMS Entrance Exam behavior, such as gated content acc
|
||||
from mock import patch, Mock
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.client import RequestFactory
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from courseware.model_data import FieldDataCache
|
||||
from courseware.module_render import toc_for_course, get_module
|
||||
from courseware.module_render import toc_for_course, get_module, handle_xblock_callback
|
||||
from courseware.tests.factories import UserFactory, InstructorFactory, StaffFactory
|
||||
from courseware.tests.helpers import (
|
||||
LoginEnrollmentTestCase,
|
||||
@@ -115,10 +117,16 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
category='vertical',
|
||||
display_name='Exam Vertical - Unit 1'
|
||||
)
|
||||
problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
|
||||
question_text='The correct answer is Choice 3',
|
||||
choices=[False, False, True, False],
|
||||
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
|
||||
)
|
||||
self.problem_1 = ItemFactory.create(
|
||||
parent=subsection,
|
||||
category="problem",
|
||||
display_name="Exam Problem - Problem 1"
|
||||
display_name="Exam Problem - Problem 1",
|
||||
data=problem_xml
|
||||
)
|
||||
self.problem_2 = ItemFactory.create(
|
||||
parent=subsection,
|
||||
@@ -511,6 +519,28 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
)
|
||||
self.assertTrue(user_has_passed_entrance_exam(self.request, course))
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_MASQUERADE': False})
|
||||
def test_entrance_exam_xblock_response(self):
|
||||
"""
|
||||
Tests entrance exam xblock has `entrance_exam_passed` key in json response.
|
||||
"""
|
||||
request_factory = RequestFactory()
|
||||
data = {'input_{}_2_1'.format(unicode(self.problem_1.location.html_id())): 'choice_2'}
|
||||
request = request_factory.post(
|
||||
'problem_check',
|
||||
data=data
|
||||
)
|
||||
request.user = self.user
|
||||
response = handle_xblock_callback(
|
||||
request,
|
||||
unicode(self.course.id),
|
||||
unicode(self.problem_1.location),
|
||||
'xmodule_handler',
|
||||
'problem_check',
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('entrance_exam_passed', response.content)
|
||||
|
||||
def _assert_chapter_loaded(self, course, chapter):
|
||||
"""
|
||||
Asserts courseware chapter load successfully.
|
||||
|
||||
@@ -206,6 +206,16 @@ ${fragment.foot_html()}
|
||||
current_score=int(entrance_exam_current_score * 100)
|
||||
)}
|
||||
</p>
|
||||
<script type="text/javascript">
|
||||
$(document).ajaxSuccess(function(event, xhr, settings) {
|
||||
if (settings.url.indexOf("xmodule_handler/problem_check") > -1) {
|
||||
var data = JSON.parse(xhr.responseText);
|
||||
if (data.entrance_exam_passed){
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
% else:
|
||||
<p class="sequential-status-message">
|
||||
${_('Your score is {current_score}%. You have passed the entrance exam.').format(
|
||||
|
||||
Reference in New Issue
Block a user