From 2d1ff75ba0d4d135c257505653b73f213dfcd94f Mon Sep 17 00:00:00 2001
From: Zia Fazal
Date: Fri, 9 Oct 2015 20:00:30 +0500
Subject: [PATCH] =?UTF-8?q?added=20user=E2=80=99s=20pass=20status=20to=20p?=
=?UTF-8?q?roblem=5Fcheck=20response=20if=20problem=20is=20in=20Entrance?=
=?UTF-8?q?=20Exam.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
unit test to check presence of entrance_exam_passed
changed based on Matt's feedback
changes after Asad's feedback
---
common/test/acceptance/fixtures/course.py | 21 ++++
.../test/acceptance/pages/lms/courseware.py | 27 +++++
common/test/acceptance/pages/lms/problem.py | 7 ++
.../tests/lms/test_lms_entrance_exams.py | 105 ++++++++++++++++++
lms/djangoapps/courseware/entrance_exams.py | 24 +++-
lms/djangoapps/courseware/module_render.py | 28 ++++-
lms/djangoapps/courseware/tabs.py | 3 +-
.../courseware/tests/test_entrance_exam.py | 34 +++++-
lms/templates/courseware/courseware.html | 10 ++
9 files changed, 249 insertions(+), 10 deletions(-)
create mode 100644 common/test/acceptance/tests/lms/test_lms_entrance_exams.py
diff --git a/common/test/acceptance/fixtures/course.py b/common/test/acceptance/fixtures/course.py
index 90521be680..2ea327ea0b 100644
--- a/common/test/acceptance/fixtures/course.py
+++ b/common/test/acceptance/fixtures/course.py
@@ -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):
"""
diff --git a/common/test/acceptance/pages/lms/courseware.py b/common/test/acceptance/pages/lms/courseware.py
index a2395397e8..359eda9a4c 100644
--- a/common/test/acceptance/pages/lms/courseware.py
+++ b/common/test/acceptance/pages/lms/courseware.py
@@ -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):
"""
diff --git a/common/test/acceptance/pages/lms/problem.py b/common/test/acceptance/pages/lms/problem.py
index 7b5f979fe3..83476815aa 100644
--- a/common/test/acceptance/pages/lms/problem.py
+++ b/common/test/acceptance/pages/lms/problem.py
@@ -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?
diff --git a/common/test/acceptance/tests/lms/test_lms_entrance_exams.py b/common/test/acceptance/tests/lms/test_lms_entrance_exams.py
new file mode 100644
index 0000000000..b598e60267
--- /dev/null
+++ b/common/test/acceptance/tests/lms/test_lms_entrance_exams.py
@@ -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("""
+
+ What is height of eiffel tower without the antenna?.
+
+
+ 324 metersAntenna is 24 meters high
+ 300 meters
+ 224 meters
+ 400 meters
+
+
+
+ """)
+ 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)
diff --git a/lms/djangoapps/courseware/entrance_exams.py b/lms/djangoapps/courseware/entrance_exams.py
index 7a02196dc0..8c6f73c7e1 100644
--- a/lms/djangoapps/courseware/entrance_exams.py
+++ b/lms/djangoapps/courseware/entrance_exams.py
@@ -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']
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 43b9866782..4c169391e3 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -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
diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py
index 89cc13ca68..76a46d692c 100644
--- a/lms/djangoapps/courseware/tabs.py
+++ b/lms/djangoapps/courseware/tabs.py
@@ -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':
diff --git a/lms/djangoapps/courseware/tests/test_entrance_exam.py b/lms/djangoapps/courseware/tests/test_entrance_exam.py
index ae1fd8622a..7ee1f10b54 100644
--- a/lms/djangoapps/courseware/tests/test_entrance_exam.py
+++ b/lms/djangoapps/courseware/tests/test_entrance_exam.py
@@ -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.
diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html
index bb6f91885e..775c230546 100644
--- a/lms/templates/courseware/courseware.html
+++ b/lms/templates/courseware/courseware.html
@@ -206,6 +206,16 @@ ${fragment.foot_html()}
current_score=int(entrance_exam_current_score * 100)
)}
+
% else:
${_('Your score is {current_score}%. You have passed the entrance exam.').format(