Commit 9e734442 by Diana Huang

Initial version of new transformer.

parent c58b3a14
......@@ -207,7 +207,7 @@ def remove_course_milestones(course_key, user, relationship):
milestones_api.remove_user_milestone({'id': user.id}, milestone)
def get_required_content(course, user):
def get_required_content(course_key, user):
"""
Queries milestones subsystem to see if the specified course is gated on one or more milestones,
and if those milestones can be fulfilled via completion of a particular course content module
......@@ -217,7 +217,7 @@ def get_required_content(course, user):
# Get all of the outstanding milestones for this course, for this user
try:
milestone_paths = get_course_milestones_fulfillment_paths(
unicode(course.id),
unicode(course_key),
serialize_user(user)
)
except InvalidMilestoneRelationshipTypeException:
......
......@@ -30,6 +30,7 @@ class CoursewarePage(CoursePage):
def is_browser_on_page(self):
return self.q(css='.course-content').present
# TODO: TNL-6546: Remove and find callers
@property
def chapter_count_in_navigation(self):
"""
......
......@@ -1233,7 +1233,7 @@ class EntranceExamTest(UniqueCourseTest):
self.course_info['run'], self.course_info['display_name']
).install()
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.settings_page = SettingsPage(
self.browser,
self.course_info['org'],
......@@ -1246,18 +1246,53 @@ class EntranceExamTest(UniqueCourseTest):
def test_entrance_exam_section(self):
"""
Scenario: Any course that is enabled for an entrance exam, should have
entrance exam section in the course outline.
Given that I visit the course outline
And entrance exams are not yet enabled
Then I should not see an "Entrance Exam" section
When I log in as staff
And enable entrance exams
And I visit the course outline again as student
Then there should be an "Entrance Exam" chapter.'
"""
# visit the course outline and make sure there is no "Entrance Exam" section.
self.course_home_page.visit()
self.assertFalse('Entrance Exam' in self.course_home_page.outline.sections.keys())
# Logout and login as a staff.
LogoutPage(self.browser).visit()
AutoAuthPage(self.browser, course_id=self.course_id, staff=True).visit()
# visit course settings page and set/enabled entrance exam for that course.
self.settings_page.visit()
self.settings_page.entrance_exam_field.click()
self.settings_page.save_changes()
# Logout and login as a student.
LogoutPage(self.browser).visit()
AutoAuthPage(self.browser, course_id=self.course_id, staff=False).visit()
# visit the course outline and make sure there is an "Entrance Exam" section.
self.course_home_page.visit()
self.assertTrue('Entrance Exam' in self.course_home_page.outline.sections.keys())
# TODO: TNL-6546: Remove test
def test_entrance_exam_section_2(self):
"""
Scenario: Any course that is enabled for an entrance exam, should have entrance exam chapter at course
page.
Given that I am on the course page
When I view the course that has an entrance exam
Then there should be an "Entrance Exam" chapter.'
"""
courseware_page = CoursewarePage(self.browser, self.course_id)
entrance_exam_link_selector = '.accordion .course-navigation .chapter .group-heading'
# visit course page and make sure there is not entrance exam chapter.
self.courseware_page.visit()
self.courseware_page.wait_for_page()
courseware_page.visit()
courseware_page.wait_for_page()
self.assertFalse(element_has_text(
page=self.courseware_page,
page=courseware_page,
css_selector=entrance_exam_link_selector,
text='Entrance Exam'
))
......@@ -1276,10 +1311,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.courseware_page.visit()
self.courseware_page.wait_for_page()
courseware_page.visit()
courseware_page.wait_for_page()
self.assertTrue(element_has_text(
page=self.courseware_page,
page=courseware_page,
css_selector=entrance_exam_link_selector,
text='Entrance Exam'
))
......
......@@ -6,6 +6,7 @@ from textwrap import dedent
from common.test.acceptance.tests.helpers import UniqueCourseTest
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage
from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.problem import ProblemPage
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
......@@ -92,6 +93,8 @@ class EntranceExamPassTest(EntranceExamTest):
When I pass entrance exam
Then I can see complete TOC of course
And I can see message indicating my pass status
When I switch to course home page
Then I see 2 sections
"""
self.courseware_page.visit()
problem_page = ProblemPage(self.browser)
......@@ -102,4 +105,29 @@ class EntranceExamPassTest(EntranceExamTest):
problem_page.click_submit()
self.courseware_page.wait_for_page()
self.assertTrue(self.courseware_page.has_passed_message())
self.assertEqual(self.courseware_page.chapter_count_in_navigation, 2)
course_home_page = CourseHomePage(self.browser, self.course_id)
course_home_page.visit()
self.assertEqual(course_home_page.outline.num_sections, 2)
# TODO: TNL-6546: Delete test using outline on courseware
def test_course_is_unblocked_as_soon_as_student_passes_entrance_exam_2(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_submit()
self.courseware_page.wait_for_page()
self.assertTrue(self.courseware_page.has_passed_message())
self.assertEqual(self.courseware_page.num_sections, 2)
......@@ -14,6 +14,7 @@ from common.test.acceptance.pages.studio.import_export import (
ImportCoursePage)
from common.test.acceptance.pages.studio.library import LibraryEditPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.staff_view import StaffCoursewarePage
......@@ -282,9 +283,13 @@ class TestEntranceExamCourseImport(ImportTestMixin, StudioCourseTest):
When I visit the import page
And I upload a course that has an entrance exam section named 'Entrance Exam'
And I visit the course outline page again
The section named 'Entrance Exam' should now be available.
And when I switch the view mode to student view and Visit CourseWare
Then I see one section in the sidebar that is 'Entrance Exam'
The section named 'Entrance Exam' should now be available
When I visit the LMS Course Home page
Then I should see a section named 'Section' or 'Entrance Exam'
When I switch the view mode to student view
Then I should only see a section named 'Entrance Exam'
When I visit the courseware page
Then a message regarding the 'Entrance Exam'
"""
self.landing_page.visit()
# Should not exist yet.
......@@ -300,10 +305,16 @@ class TestEntranceExamCourseImport(ImportTestMixin, StudioCourseTest):
self.landing_page.section("Section")
self.landing_page.view_live()
course_home = CourseHomePage(self.browser, self.course_id)
course_home.visit()
self.assertEqual(course_home.outline.num_sections, 2)
course_home.preview.set_staff_view_mode('Student')
self.assertEqual(course_home.outline.num_sections, 1)
courseware = CoursewarePage(self.browser, self.course_id)
courseware.wait_for_page()
StaffCoursewarePage(self.browser, self.course_id).set_staff_view_mode('Learner')
self.assertEqual(courseware.num_sections, 1)
courseware.visit()
StaffCoursewarePage(self.browser, self.course_id).set_staff_view_mode('Student')
self.assertIn(
"To access course materials, you must score", courseware.entrance_exam_message_selector.text[0]
)
......
......@@ -51,8 +51,12 @@ def get_blocks(
"""
# create ordered list of transformers, adding BlocksAPITransformer at end.
transformers = BlockStructureTransformers()
can_view_special_exam = False
if requested_fields is not None and 'special_exam' in requested_fields:
can_view_special_exam = True
if user is not None:
transformers += COURSE_BLOCK_ACCESS_TRANSFORMERS + [MilestonesTransformer(), HiddenContentTransformer()]
transformers += COURSE_BLOCK_ACCESS_TRANSFORMERS
transformers += [MilestonesTransformer(can_view_special_exam), HiddenContentTransformer()]
transformers += [
BlocksAPITransformer(
block_counts,
......
......@@ -146,7 +146,7 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase):
self._get_blocks(
course,
expected_mongo_queries=0,
expected_sql_queries=5 if with_storage_backing else 4,
expected_sql_queries=6 if with_storage_backing else 5,
)
@ddt.data(
......@@ -164,5 +164,5 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase):
self._get_blocks(
course,
expected_mongo_queries,
expected_sql_queries=13 if with_storage_backing else 5,
expected_sql_queries=14 if with_storage_backing else 6,
)
......@@ -6,6 +6,7 @@ from lms.djangoapps.course_blocks.transformers.visibility import VisibilityTrans
from .student_view import StudentViewTransformer
from .block_counts import BlockCountsTransformer
from .navigation import BlockNavigationTransformer
from .milestones import MilestonesTransformer
class SupportedFieldType(object):
......@@ -44,6 +45,8 @@ SUPPORTED_FIELDS = [
# 'student_view_multi_device'
SupportedFieldType(StudentViewTransformer.STUDENT_VIEW_MULTI_DEVICE, StudentViewTransformer),
SupportedFieldType('special_exam', MilestonesTransformer),
# set the block_field_name to None so the entire data for the transformer is serialized
SupportedFieldType(None, BlockCountsTransformer, BlockCountsTransformer.BLOCK_COUNTS),
......
......@@ -2,16 +2,22 @@
Milestones Transformer
"""
import logging
from django.conf import settings
from openedx.core.djangoapps.content.block_structure.transformer import (
BlockStructureTransformer,
FilteringTransformerMixin,
)
from edx_proctoring.exceptions import ProctoredExamNotFoundException
from edx_proctoring.api import get_attempt_status_summary
from student.models import EntranceExamConfiguration
from util import milestones_helpers
log = logging.getLogger(__name__)
class MilestonesTransformer(FilteringTransformerMixin, BlockStructureTransformer):
class MilestonesTransformer(BlockStructureTransformer):
"""
Excludes all special exams (timed, proctored, practice proctored) from the student view.
Excludes all blocks with unfulfilled milestones from the student view.
......@@ -23,6 +29,9 @@ class MilestonesTransformer(FilteringTransformerMixin, BlockStructureTransformer
def name(cls):
return "milestones"
def __init__(self, can_view_special_exams=True):
self.can_view_special_exams = can_view_special_exams
@classmethod
def collect(cls, block_structure):
"""
......@@ -35,22 +44,79 @@ class MilestonesTransformer(FilteringTransformerMixin, BlockStructureTransformer
block_structure.request_xblock_fields('is_proctored_enabled')
block_structure.request_xblock_fields('is_practice_exam')
block_structure.request_xblock_fields('is_timed_exam')
block_structure.request_xblock_fields('entrance_exam_id')
def transform(self, usage_info, block_structure):
"""
Modify block structure according to the behavior of milestones and special exams.
"""
def add_special_exam_info(block_key):
"""
Adds special exam information to course blocks.
"""
if self.is_special_exam(block_key, block_structure):
#
# call into edx_proctoring subsystem
# to get relevant proctoring information regarding this
# level of the courseware
#
# This will return None, if (user, course_id, content_id)
# is not applicable
#
timed_exam_attempt_context = None
try:
timed_exam_attempt_context = get_attempt_status_summary(
usage_info.user.id,
unicode(block_key.course_key),
unicode(block_key)
)
except ProctoredExamNotFoundException as ex:
log.exception(ex)
if timed_exam_attempt_context:
# yes, user has proctoring context about
# this level of the courseware
# so add to the accordion data context
block_structure.set_transformer_block_field(
block_key,
self,
'special_exam',
timed_exam_attempt_context,
)
def transform_block_filters(self, usage_info, block_structure):
if usage_info.has_staff_access:
return [block_structure.create_universal_filter()]
root_key = block_structure.root_block_usage_key
course_key = root_key.course_key
user_can_skip = EntranceExamConfiguration.user_can_skip_entrance_exam(usage_info.user, course_key)
exam_id = block_structure.get_xblock_field(root_key, 'entrance_exam_id')
required_content = milestones_helpers.get_required_content(course_key, usage_info.user)
if user_can_skip:
required_content = [content for content in required_content if not content == exam_id]
def user_gated_from_block(block_key):
"""
Checks whether the user is gated from accessing this block, first via special exams,
then via a general milestones check.
"""
return (
settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and
self.is_special_exam(block_key, block_structure)
) or self.has_pending_milestones_for_user(block_key, usage_info)
if usage_info.has_staff_access:
return False
elif self.has_pending_milestones_for_user(block_key, usage_info):
return True
elif required_content:
if block_key.block_type == 'chapter' and unicode(block_key) not in required_content:
return True
elif (settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and
(self.is_special_exam(block_key, block_structure) and
not self.can_view_special_exams)):
return True
return False
return [block_structure.create_removal_filter(user_gated_from_block)]
for block_key in block_structure.topological_traversal():
if user_gated_from_block(block_key):
block_structure.remove_block(block_key, False)
else:
add_special_exam_info(block_key)
@staticmethod
def is_special_exam(block_key, block_structure):
......
......@@ -9,6 +9,7 @@ from gating import api as lms_gating_api
from lms.djangoapps.course_blocks.transformers.tests.helpers import CourseStructureTestCase
from milestones.tests.utils import MilestonesTestCaseMixin
from openedx.core.lib.gating import api as gating_api
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
from student.tests.factories import CourseEnrollmentFactory
from ..milestones import MilestonesTransformer
......@@ -38,6 +39,8 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
# Enroll user in course.
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
self.transformers = BlockStructureTransformers([self.TRANSFORMER_CLASS_TO_TEST(False)])
def setup_gated_section(self, gated_block, gating_block):
"""
Test helper to create a gating requirement.
......@@ -157,7 +160,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
self.course.enable_subsection_gating = True
self.setup_gated_section(self.blocks[gated_block_ref], self.blocks[gating_block_ref])
with self.assertNumQueries(6):
with self.assertNumQueries(8):
self.get_blocks_and_check_against_expected(self.user, expected_blocks_before_completion)
# clear the request cache to simulate a new request
......@@ -171,7 +174,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
self.user,
)
with self.assertNumQueries(6):
with self.assertNumQueries(8):
self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS_EXCEPT_SPECIAL)
def test_staff_access(self):
......@@ -183,6 +186,30 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
self.setup_gated_section(self.blocks['H'], self.blocks['A'])
self.get_blocks_and_check_against_expected(self.staff, expected_blocks)
def test_can_view_special(self):
"""
When the block structure transformers are set to allow users to view special exams,
ensure that we can see the special exams and not any of the otherwise gated blocks.
"""
self.transformers = BlockStructureTransformers([self.TRANSFORMER_CLASS_TO_TEST(True)])
self.course.enable_subsection_gating = True
self.setup_gated_section(self.blocks['H'], self.blocks['A'])
expected_blocks = (
'course', 'A', 'B', 'C', 'ProctoredExam', 'D', 'E', 'PracticeExam', 'F', 'G', 'TimedExam', 'J', 'K'
)
self.get_blocks_and_check_against_expected(self.user, expected_blocks)
# clear the request cache to simulate a new request
self.clear_caches()
# this call triggers reevaluation of prerequisites fulfilled by the gating block.
with patch('gating.api._get_subsection_percentage', Mock(return_value=100)):
lms_gating_api.evaluate_prerequisite(
self.course,
Mock(location=self.blocks['A'].location),
self.user,
)
self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS)
def get_blocks_and_check_against_expected(self, user, expected_blocks):
"""
Calls the course API as the specified user and checks the
......
......@@ -55,7 +55,7 @@ def get_entrance_exam_content(user, course):
"""
Get the entrance exam content information (ie, chapter module)
"""
required_content = get_required_content(course, user)
required_content = get_required_content(course.id, user)
exam_module = None
for content in required_content:
......
......@@ -162,7 +162,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
# Check for content which needs to be completed
# before the rest of the content is made available
required_content = milestones_helpers.get_required_content(course, user)
required_content = milestones_helpers.get_required_content(course.id, user)
# The user may not actually have to complete the entrance exam, if one is required
if user_can_skip_entrance_exam(user, course):
......
......@@ -27,6 +27,7 @@ from django.utils.translation import ugettext as _
</div>
<ol class="outline-item focusable" role="group" tabindex="0">
% for subsection in section.get('children') or []:
${ subsection.get('special_exam', '') }
<li
class="subsection ${ 'current' if subsection['current'] else '' }"
role="treeitem"
......
......@@ -47,7 +47,7 @@ class CourseOutlineFragmentView(EdxFragmentView):
course_usage_key,
user=request.user,
nav_depth=3,
requested_fields=['children', 'display_name', 'type'],
requested_fields=['children', 'display_name', 'type', 'due', 'graded', 'special_exam'],
block_types_filter=['course', 'chapter', 'sequential']
)
......
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