Commit 5346ee25 by Kevin Kim Committed by GitHub

Merge pull request #12974 from edx/kkim/gate_banner

Gated Subsection Staff Banner
parents 99eb795a a39d6c2e
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
""" """
Utility library for working with the edx-milestones app Utility library for working with the edx-milestones app
""" """
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -12,6 +11,7 @@ from opaque_keys.edx.keys import CourseKey ...@@ -12,6 +11,7 @@ from opaque_keys.edx.keys import CourseKey
from milestones import api as milestones_api from milestones import api as milestones_api
from milestones.exceptions import InvalidMilestoneRelationshipTypeException from milestones.exceptions import InvalidMilestoneRelationshipTypeException
from milestones.models import MilestoneRelationshipType from milestones.models import MilestoneRelationshipType
from milestones.services import MilestonesService
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import request_cache import request_cache
...@@ -418,3 +418,16 @@ def remove_user_milestone(user, milestone): ...@@ -418,3 +418,16 @@ def remove_user_milestone(user, milestone):
if not settings.FEATURES.get('MILESTONES_APP'): if not settings.FEATURES.get('MILESTONES_APP'):
return None return None
return milestones_api.remove_user_milestone(user, milestone) return milestones_api.remove_user_milestone(user, milestone)
def get_service():
"""
Returns MilestonesService instance if feature flag enabled;
else returns None.
Note: MilestonesService only has access to the functions
explicitly requested in the MilestonesServices class
"""
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
return MilestonesService()
...@@ -110,6 +110,11 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase): ...@@ -110,6 +110,11 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase):
response = milestones_helpers.add_user_milestone(self.user, self.milestone) response = milestones_helpers.add_user_milestone(self.user, self.milestone)
self.assertIsNone(response) self.assertIsNone(response)
def test_get_service_returns_none_when_app_disabled(self):
"""MilestonesService is None when app disabled"""
response = milestones_helpers.get_service()
self.assertIsNone(response)
@patch.dict('django.conf.settings.FEATURES', {'MILESTONES_APP': True}) @patch.dict('django.conf.settings.FEATURES', {'MILESTONES_APP': True})
def test_any_unfulfilled_milestones(self): def test_any_unfulfilled_milestones(self):
""" Tests any_unfulfilled_milestones for invalid arguments """ """ Tests any_unfulfilled_milestones for invalid arguments """
......
...@@ -137,6 +137,7 @@ class ProctoringFields(object): ...@@ -137,6 +137,7 @@ class ProctoringFields(object):
@XBlock.wants('proctoring') @XBlock.wants('proctoring')
@XBlock.wants('milestones')
@XBlock.wants('credit') @XBlock.wants('credit')
@XBlock.needs("user") @XBlock.needs("user")
@XBlock.needs("bookmarks") @XBlock.needs("bookmarks")
...@@ -209,6 +210,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -209,6 +210,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
banner_text, special_html = special_html_view banner_text, special_html = special_html_view
if special_html and not masquerading_as_specific_student: if special_html and not masquerading_as_specific_student:
return Fragment(special_html) return Fragment(special_html)
else:
banner_text = self._gated_content_staff_banner()
return self._student_view(context, banner_text) return self._student_view(context, banner_text)
def _special_exam_student_view(self): def _special_exam_student_view(self):
...@@ -249,6 +252,20 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -249,6 +252,20 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
return banner_text, hidden_content_html return banner_text, hidden_content_html
def _gated_content_staff_banner(self):
"""
Checks whether the content is gated for learners. If so,
returns a banner_text depending on whether user is staff.
"""
milestones_service = self.runtime.service(self, 'milestones')
if milestones_service:
content_milestones = milestones_service.get_course_content_milestones(
self.course_id, self.location, 'requires'
)
banner_text = _('This subsection is unlocked for learners when they meet the prerequisite requirements.')
if content_milestones and self.runtime.user_is_staff:
return banner_text
def _can_user_view_content(self): def _can_user_view_content(self):
""" """
Returns whether the runtime user can view the content Returns whether the runtime user can view the content
......
...@@ -229,6 +229,12 @@ class CoursewarePage(CoursePage): ...@@ -229,6 +229,12 @@ class CoursewarePage(CoursePage):
return self.entrance_exam_message_selector.is_present() \ return self.entrance_exam_message_selector.is_present() \
and "You have passed the entrance exam" in self.entrance_exam_message_selector.text[0] and "You have passed the entrance exam" in self.entrance_exam_message_selector.text[0]
def has_banner(self):
"""
Returns boolean indicating presence of banner
"""
return self.q(css='.pattern-library-shim').is_present()
@property @property
def is_timer_bar_present(self): def is_timer_bar_present(self):
""" """
......
...@@ -9,6 +9,7 @@ from ...pages.studio.auto_auth import AutoAuthPage ...@@ -9,6 +9,7 @@ from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.overview import CourseOutlinePage from ...pages.studio.overview import CourseOutlinePage
from ...pages.lms.courseware import CoursewarePage from ...pages.lms.courseware import CoursewarePage
from ...pages.lms.problem import ProblemPage from ...pages.lms.problem import ProblemPage
from ...pages.lms.staff_view import StaffPage
from ...pages.common.logout import LogoutPage from ...pages.common.logout import LogoutPage
from ...fixtures.course import CourseFixture, XBlockFixtureDesc from ...fixtures.course import CourseFixture, XBlockFixtureDesc
...@@ -17,8 +18,11 @@ class GatingTest(UniqueCourseTest): ...@@ -17,8 +18,11 @@ class GatingTest(UniqueCourseTest):
""" """
Test gating feature in LMS. Test gating feature in LMS.
""" """
USERNAME = "STUDENT_TESTER" STAFF_USERNAME = "STAFF_TESTER"
EMAIL = "student101@example.com" STAFF_EMAIL = "staff101@example.com"
STUDENT_USERNAME = "STUDENT_TESTER"
STUDENT_EMAIL = "student101@example.com"
def setUp(self): def setUp(self):
super(GatingTest, self).setUp() super(GatingTest, self).setUp()
...@@ -82,7 +86,7 @@ class GatingTest(UniqueCourseTest): ...@@ -82,7 +86,7 @@ class GatingTest(UniqueCourseTest):
Make the first subsection a prerequisite Make the first subsection a prerequisite
""" """
# Login as staff # Login as staff
self._auto_auth("STAFF_TESTER", "staff101@example.com", True) self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
# Make the first subsection a prerequisite # Make the first subsection a prerequisite
self.course_outline.visit() self.course_outline.visit()
...@@ -95,7 +99,7 @@ class GatingTest(UniqueCourseTest): ...@@ -95,7 +99,7 @@ class GatingTest(UniqueCourseTest):
Gate the second subsection on the first subsection Gate the second subsection on the first subsection
""" """
# Login as staff # Login as staff
self._auto_auth("STAFF_TESTER", "staff101@example.com", True) self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
# Gate the second subsection based on the score achieved in the first subsection # Gate the second subsection based on the score achieved in the first subsection
self.course_outline.visit() self.course_outline.visit()
...@@ -103,6 +107,15 @@ class GatingTest(UniqueCourseTest): ...@@ -103,6 +107,15 @@ class GatingTest(UniqueCourseTest):
self.course_outline.select_advanced_tab(desired_item='gated_content') self.course_outline.select_advanced_tab(desired_item='gated_content')
self.course_outline.add_prerequisite_to_subsection("80") self.course_outline.add_prerequisite_to_subsection("80")
def _fulfill_prerequisite(self):
"""
Fulfill the prerequisite needed to see gated content
"""
problem_page = ProblemPage(self.browser)
self.assertEqual(problem_page.wait_for_page().problem_name, 'HEIGHT OF EIFFEL TOWER')
problem_page.click_choice('choice_1')
problem_page.click_check()
def test_subsection_gating_in_studio(self): def test_subsection_gating_in_studio(self):
""" """
Given that I am a staff member Given that I am a staff member
...@@ -132,7 +145,7 @@ class GatingTest(UniqueCourseTest): ...@@ -132,7 +145,7 @@ class GatingTest(UniqueCourseTest):
self.assertTrue(self.course_outline.gating_prerequisites_dropdown_is_visible()) self.assertTrue(self.course_outline.gating_prerequisites_dropdown_is_visible())
self.assertTrue(self.course_outline.gating_prerequisite_min_score_is_visible()) self.assertTrue(self.course_outline.gating_prerequisite_min_score_is_visible())
def test_gated_subsection_in_lms(self): def test_gated_subsection_in_lms_for_student(self):
""" """
Given that I am a student Given that I am a student
When I visit the LMS Courseware When I visit the LMS Courseware
...@@ -143,15 +156,58 @@ class GatingTest(UniqueCourseTest): ...@@ -143,15 +156,58 @@ class GatingTest(UniqueCourseTest):
self._setup_prereq() self._setup_prereq()
self._setup_gated_subsection() self._setup_gated_subsection()
self._auto_auth(self.USERNAME, self.EMAIL, False) self._auto_auth(self.STUDENT_USERNAME, self.STUDENT_EMAIL, False)
self.courseware_page.visit() self.courseware_page.visit()
self.assertEqual(self.courseware_page.num_subsections, 1) self.assertEqual(self.courseware_page.num_subsections, 1)
# Fulfill prerequisite and verify that gated subsection is shown # Fulfill prerequisite and verify that gated subsection is shown
problem_page = ProblemPage(self.browser) self._fulfill_prerequisite()
self.assertEqual(problem_page.wait_for_page().problem_name, 'HEIGHT OF EIFFEL TOWER') self.courseware_page.visit()
problem_page.click_choice('choice_1') self.assertEqual(self.courseware_page.num_subsections, 2)
problem_page.click_check()
def test_gated_subsection_in_lms_for_staff(self):
"""
Given that I am a staff member
When I visit the LMS Courseware
Then I can see all gated subsections
Displayed along with notification banners
Then if I masquerade as a student
Then I cannot see a gated subsection
When I fufill the gating prerequisite
Then I can see the gated subsection (without a banner)
"""
self._setup_prereq()
self._setup_gated_subsection()
# Fulfill prerequisites for specific student
self._auto_auth(self.STUDENT_USERNAME, self.STUDENT_EMAIL, False)
self.courseware_page.visit()
self._fulfill_prerequisite()
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self.courseware_page.visit() self.courseware_page.visit()
staff_page = StaffPage(self.browser, self.course_id)
self.assertEqual(staff_page.staff_view_mode, 'Staff')
self.assertEqual(self.courseware_page.num_subsections, 2)
# Click on gated section and check for banner
self.courseware_page.q(css='.chapter-content-container a').nth(1).click()
self.courseware_page.wait_for_page()
self.assertTrue(self.courseware_page.has_banner())
self.courseware_page.q(css='.chapter-content-container a').nth(0).click()
self.courseware_page.wait_for_page()
staff_page.set_staff_view_mode('Student')
self.assertEqual(self.courseware_page.num_subsections, 1)
self.assertFalse(self.courseware_page.has_banner())
staff_page.set_staff_view_mode_specific_student(self.STUDENT_USERNAME)
self.assertEqual(self.courseware_page.num_subsections, 2) self.assertEqual(self.courseware_page.num_subsections, 2)
self.courseware_page.q(css='.chapter-content-container a').nth(1).click()
self.courseware_page.wait_for_page()
self.assertFalse(self.courseware_page.has_banner())
...@@ -755,6 +755,7 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to ...@@ -755,6 +755,7 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to
'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff), 'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff),
"reverification": ReverificationService(), "reverification": ReverificationService(),
'proctoring': ProctoringService(), 'proctoring': ProctoringService(),
'milestones': milestones_helpers.get_service(),
'credit': CreditService(), 'credit': CreditService(),
'bookmarks': BookmarksService(user=user), 'bookmarks': BookmarksService(user=user),
}, },
......
...@@ -4,6 +4,7 @@ API for the gating djangoapp ...@@ -4,6 +4,7 @@ API for the gating djangoapp
import logging import logging
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from lms.djangoapps.courseware.access import _has_access_to_course
from milestones import api as milestones_api from milestones import api as milestones_api
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -285,12 +286,15 @@ def get_gated_content(course, user): ...@@ -285,12 +286,15 @@ def get_gated_content(course, user):
Returns: Returns:
list: The list of gated content usage keys for the given course list: The list of gated content usage keys for the given course
""" """
# Get the unfulfilled gating milestones for this course, for this user if _has_access_to_course(user, 'staff', course.id):
return [ return []
m['content_id'] for m in find_gating_milestones( else:
course.id, # Get the unfulfilled gating milestones for this course, for this user
None, return [
'requires', m['content_id'] for m in find_gating_milestones(
{'id': user.id} course.id,
) None,
] 'requires',
{'id': user.id}
)
]
...@@ -10,6 +10,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DAT ...@@ -10,6 +10,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DAT
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from openedx.core.lib.gating import api as gating_api from openedx.core.lib.gating import api as gating_api
from openedx.core.lib.gating.exceptions import GatingValidationError from openedx.core.lib.gating.exceptions import GatingValidationError
from student.tests.factories import UserFactory
@attr('shard_2') @attr('shard_2')
...@@ -154,19 +155,23 @@ class TestGatingApi(ModuleStoreTestCase, MilestonesTestCaseMixin): ...@@ -154,19 +155,23 @@ class TestGatingApi(ModuleStoreTestCase, MilestonesTestCaseMixin):
self.assertIsNone(min_score) self.assertIsNone(min_score)
def test_get_gated_content(self): def test_get_gated_content(self):
""" Test test_get_gated_content """ """
Verify staff bypasses gated content and student gets list of unfulfilled prerequisites.
"""
mock_user = MagicMock() staff = UserFactory(is_staff=True)
mock_user.id.return_value = 1 student = UserFactory(is_staff=False)
self.assertEqual(gating_api.get_gated_content(self.course, mock_user), []) self.assertEqual(gating_api.get_gated_content(self.course, staff), [])
self.assertEqual(gating_api.get_gated_content(self.course, student), [])
gating_api.add_prerequisite(self.course.id, self.seq1.location) gating_api.add_prerequisite(self.course.id, self.seq1.location)
gating_api.set_required_content(self.course.id, self.seq2.location, self.seq1.location, 100) gating_api.set_required_content(self.course.id, self.seq2.location, self.seq1.location, 100)
milestone = milestones_api.get_course_content_milestones(self.course.id, self.seq2.location, 'requires')[0] milestone = milestones_api.get_course_content_milestones(self.course.id, self.seq2.location, 'requires')[0]
self.assertEqual(gating_api.get_gated_content(self.course, mock_user), [unicode(self.seq2.location)]) self.assertEqual(gating_api.get_gated_content(self.course, staff), [])
self.assertEqual(gating_api.get_gated_content(self.course, student), [unicode(self.seq2.location)])
milestones_api.add_user_milestone({'id': mock_user.id}, milestone) milestones_api.add_user_milestone({'id': student.id}, milestone) # pylint: disable=no-member
self.assertEqual(gating_api.get_gated_content(self.course, mock_user), []) self.assertEqual(gating_api.get_gated_content(self.course, student), [])
...@@ -84,7 +84,7 @@ git+https://github.com/pmitros/RecommenderXBlock.git@v1.1#egg=recommender-xblock ...@@ -84,7 +84,7 @@ git+https://github.com/pmitros/RecommenderXBlock.git@v1.1#egg=recommender-xblock
git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1 git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1
-e git+https://github.com/pmitros/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock -e git+https://github.com/pmitros/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock
-e git+https://github.com/pmitros/DoneXBlock.git@857bf365f19c904d7e48364428f6b93ff153fabd#egg=done-xblock -e git+https://github.com/pmitros/DoneXBlock.git@857bf365f19c904d7e48364428f6b93ff153fabd#egg=done-xblock
git+https://github.com/edx/edx-milestones.git@v0.1.8#egg=edx-milestones==0.1.8 git+https://github.com/edx/edx-milestones.git@v0.1.9#egg=edx-milestones==0.1.9
git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils==1.0.2 git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils==1.0.2
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
-e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5 -e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5
......
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