Commit 2d1ff75b by Zia Fazal

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
parent 87e3988c
...@@ -223,6 +223,27 @@ class CourseFixture(XBlockContainerFixture): ...@@ -223,6 +223,27 @@ class CourseFixture(XBlockContainerFixture):
self._configure_course() self._configure_course()
@property @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): def _course_location(self):
""" """
Return the locator string for the course. Return the locator string for the course.
......
...@@ -128,6 +128,33 @@ class CoursewarePage(CoursePage): ...@@ -128,6 +128,33 @@ class CoursewarePage(CoursePage):
# elf.wait_for_element_presence(".proctored-exam-code", "unique exam code") # elf.wait_for_element_presence(".proctored-exam-code", "unique exam code")
@property @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): def is_timer_bar_present(self):
""" """
Returns True if the timed/proctored exam timer bar is visible on the courseware. Returns True if the timed/proctored exam timer bar is visible on the courseware.
......
...@@ -84,6 +84,13 @@ class ProblemPage(PageObject): ...@@ -84,6 +84,13 @@ class ProblemPage(PageObject):
self.q(css='div.problem button.hint-button').click() self.q(css='div.problem button.hint-button').click()
self.wait_for_ajax() 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): def is_correct(self):
""" """
Is there a "correct" status showing? Is there a "correct" status showing?
......
# -*- 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 ...@@ -6,6 +6,7 @@ from django.conf import settings
from courseware.access import has_access from courseware.access import has_access
from courseware.model_data import FieldDataCache, ScoresClient from courseware.model_data import FieldDataCache, ScoresClient
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.locator import BlockUsageLocator
from student.models import EntranceExamConfiguration from student.models import EntranceExamConfiguration
from util.milestones_helpers import get_required_content from util.milestones_helpers import get_required_content
from util.module_utils import yield_dynamic_descriptor_descendants from util.module_utils import yield_dynamic_descriptor_descendants
...@@ -89,14 +90,25 @@ def _calculate_entrance_exam_score(user, course_descriptor, exam_modules): ...@@ -89,14 +90,25 @@ def _calculate_entrance_exam_score(user, course_descriptor, exam_modules):
""" """
student_module_dict = {} student_module_dict = {}
scores_client = ScoresClient(course_descriptor.id, user.id) 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) scores_client.fetch_scores(locations)
# Iterate over all of the exam modules to get score of user for each of them # Iterate over all of the exam modules to get score of user for each of them
for exam_module in exam_modules: for index, exam_module in enumerate(exam_modules):
exam_module_score = scores_client.get(exam_module.location) exam_module_score = scores_client.get(locations[index])
if exam_module_score: if exam_module_score:
student_module_dict[unicode(exam_module.location)] = { student_module_dict[unicode(locations[index])] = {
'grade': exam_module_score.correct, 'grade': exam_module_score.correct,
'max_grade': exam_module_score.total 'max_grade': exam_module_score.total
} }
...@@ -104,10 +116,10 @@ def _calculate_entrance_exam_score(user, course_descriptor, exam_modules): ...@@ -104,10 +116,10 @@ def _calculate_entrance_exam_score(user, course_descriptor, exam_modules):
module_percentages = [] module_percentages = []
ignore_categories = ['course', 'chapter', 'sequential', 'vertical'] 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: if module.graded and module.category not in ignore_categories:
module_percentage = 0 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']: if module_location in student_module_dict and student_module_dict[module_location]['max_grade']:
student_module = student_module_dict[module_location] student_module = student_module_dict[module_location]
module_percentage = student_module['grade'] / student_module['max_grade'] module_percentage = student_module['grade'] / student_module['max_grade']
......
...@@ -38,7 +38,8 @@ from courseware.model_data import DjangoKeyValueStore, FieldDataCache, set_score ...@@ -38,7 +38,8 @@ from courseware.model_data import DjangoKeyValueStore, FieldDataCache, set_score
from courseware.models import SCORE_CHANGED from courseware.models import SCORE_CHANGED
from courseware.entrance_exams import ( from courseware.entrance_exams import (
get_entrance_exam_score, 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 edxmako.shortcuts import render_to_string
from eventtracking import tracker from eventtracking import tracker
...@@ -1062,6 +1063,12 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course ...@@ -1062,6 +1063,12 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course
try: try:
with tracker.get_tracker().context(tracking_context_name, tracking_context): with tracker.get_tracker().context(tracking_context_name, tracking_context):
resp = instance.handle(handler, req, suffix) 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: except NoSuchHandlerError:
log.exception("XBlock %s attempted to access missing handler %r", instance, handler) log.exception("XBlock %s attempted to access missing handler %r", instance, handler)
...@@ -1178,3 +1185,22 @@ def _check_files_limits(files): ...@@ -1178,3 +1185,22 @@ def _check_files_limits(files):
return msg return msg
return None 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): ...@@ -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 # 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". # "Courseware" tab. The tab is then renamed as "Entrance Exam".
course_tab_list = [] course_tab_list = []
must_complete_ee = user_must_complete_entrance_exam(request, user, course)
for tab in xmodule_tab_list: 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' # Hide all of the tabs except for 'Courseware'
# Rename 'Courseware' tab to 'Entrance Exam' # Rename 'Courseware' tab to 'Entrance Exam'
if tab.type is not 'courseware': if tab.type is not 'courseware':
......
...@@ -4,10 +4,12 @@ Tests use cases related to LMS Entrance Exam behavior, such as gated content acc ...@@ -4,10 +4,12 @@ Tests use cases related to LMS Entrance Exam behavior, such as gated content acc
from mock import patch, Mock from mock import patch, Mock
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.client import RequestFactory
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from courseware.model_data import FieldDataCache 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.factories import UserFactory, InstructorFactory, StaffFactory
from courseware.tests.helpers import ( from courseware.tests.helpers import (
LoginEnrollmentTestCase, LoginEnrollmentTestCase,
...@@ -115,10 +117,16 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -115,10 +117,16 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
category='vertical', category='vertical',
display_name='Exam Vertical - Unit 1' 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( self.problem_1 = ItemFactory.create(
parent=subsection, parent=subsection,
category="problem", category="problem",
display_name="Exam Problem - Problem 1" display_name="Exam Problem - Problem 1",
data=problem_xml
) )
self.problem_2 = ItemFactory.create( self.problem_2 = ItemFactory.create(
parent=subsection, parent=subsection,
...@@ -511,6 +519,28 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -511,6 +519,28 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
) )
self.assertTrue(user_has_passed_entrance_exam(self.request, course)) 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): def _assert_chapter_loaded(self, course, chapter):
""" """
Asserts courseware chapter load successfully. Asserts courseware chapter load successfully.
......
...@@ -206,6 +206,16 @@ ${fragment.foot_html()} ...@@ -206,6 +206,16 @@ ${fragment.foot_html()}
current_score=int(entrance_exam_current_score * 100) current_score=int(entrance_exam_current_score * 100)
)} )}
</p> </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: % else:
<p class="sequential-status-message"> <p class="sequential-status-message">
${_('Your score is {current_score}%. You have passed the entrance exam.').format( ${_('Your score is {current_score}%. You have passed the entrance exam.').format(
......
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