').text(
+ '
' +
+ gettext(
+ 'You need to make correct answers for the some problems in the before quiz.'
+ ) +
+ '
'
+ )
+ current_tab.addClass('xblock')
@content_container.html(current_tab.text()).attr("aria-labelledby", current_tab.attr("aria-labelledby"))
XBlock.initializeBlocks(@content_container, @requestToken)
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index 99dcd73f9e6e..0775b2eef026 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -11,7 +11,7 @@
from lxml import etree
from xblock.core import XBlock
-from xblock.fields import Integer, Scope, Boolean
+from xblock.fields import Integer, Scope, Boolean, Dict
from xblock.fragment import Fragment
from .exceptions import NotFoundError
@@ -81,6 +81,14 @@ class SequenceFields(object):
scope=Scope.settings
)
+ progress_restriction = Dict(
+ help=_("Settings for progress restriction"),
+ default={
+ "type": "No Restriction",
+ },
+ scope=Scope.settings
+ )
+
class ProctoringFields(object):
"""
diff --git a/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py b/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py
index 5e8d4ec20367..d5f1505cd02c 100644
--- a/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py
+++ b/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py
@@ -5,8 +5,7 @@
from datetime import timedelta, datetime
from unittest import TestCase
-from django.utils.timezone import UTC
-
+from pytz import timezone, utc
from xmodule.course_metadata_utils import (
clean_course_key,
url_name_for_course_location,
@@ -28,7 +27,7 @@
)
-_TODAY = datetime.now(UTC())
+_TODAY = datetime.now(utc)
_LAST_MONTH = _TODAY - timedelta(days=30)
_LAST_WEEK = _TODAY - timedelta(days=7)
_NEXT_WEEK = _TODAY + timedelta(days=7)
@@ -104,14 +103,18 @@ def mock_strftime_localized(date_time, format_string):
else:
raise ValueError("Invalid format string :" + format_string)
- def nop_gettext(text):
+ def noop_gettext(text):
"""Dummy implementation of gettext, so we don't need Django."""
return text
- test_datetime = datetime(1945, 02, 06, 04, 20, 00, tzinfo=UTC())
+ test_datetime = datetime(1945, 2, 6, 4, 20, 00, tzinfo=utc)
advertised_start_parsable = "2038-01-19 03:14:07"
advertised_start_bad_date = "215-01-01 10:10:10"
advertised_start_unparsable = "This coming fall"
+ time_zone_normal_parsable = "2016-03-27 00:59:00"
+ time_zone_normal_datetime = datetime(2016, 3, 27, 00, 59, 00, tzinfo=utc)
+ time_zone_daylight_parsable = "2016-03-27 01:00:00"
+ time_zone_daylight_datetime = datetime(2016, 3, 27, 1, 00, 00, tzinfo=utc)
FunctionTest = namedtuple('FunctionTest', 'function scenarios') # pylint: disable=invalid-name
TestScenario = namedtuple('TestScenario', 'arguments expected_return') # pylint: disable=invalid-name
@@ -161,48 +164,73 @@ def nop_gettext(text):
# Test parsable advertised start date.
# Expect start datetime to be parsed and formatted back into a string.
TestScenario(
- (DEFAULT_START_DATE, advertised_start_parsable, 'DATE_TIME', nop_gettext, mock_strftime_localized),
+ (DEFAULT_START_DATE, advertised_start_parsable, 'DATE_TIME',
+ utc, noop_gettext, mock_strftime_localized),
mock_strftime_localized(Date().from_json(advertised_start_parsable), 'DATE_TIME') + " UTC"
),
# Test un-parsable advertised start date.
# Expect date parsing to throw a ValueError, and the advertised
# start to be returned in Title Case.
TestScenario(
- (test_datetime, advertised_start_unparsable, 'DATE_TIME', nop_gettext, mock_strftime_localized),
+ (test_datetime, advertised_start_unparsable, 'DATE_TIME',
+ utc, noop_gettext, mock_strftime_localized),
advertised_start_unparsable.title()
),
# Test parsable advertised start date from before January 1, 1900.
# Expect mock_strftime_localized to throw a ValueError, and the
# advertised start to be returned in Title Case.
TestScenario(
- (test_datetime, advertised_start_bad_date, 'DATE_TIME', nop_gettext, mock_strftime_localized),
+ (test_datetime, advertised_start_bad_date, 'DATE_TIME',
+ utc, noop_gettext, mock_strftime_localized),
advertised_start_bad_date.title()
),
# Test without advertised start date, but with a set start datetime.
# Expect formatted datetime to be returned.
TestScenario(
- (test_datetime, None, 'SHORT_DATE', nop_gettext, mock_strftime_localized),
+ (test_datetime, None, 'SHORT_DATE', utc, noop_gettext, mock_strftime_localized),
mock_strftime_localized(test_datetime, 'SHORT_DATE')
),
# Test without advertised start date and with default start datetime.
# Expect TBD to be returned.
TestScenario(
- (DEFAULT_START_DATE, None, 'SHORT_DATE', nop_gettext, mock_strftime_localized),
+ (DEFAULT_START_DATE, None, 'SHORT_DATE', utc, noop_gettext, mock_strftime_localized),
'TBD'
+ ),
+ # Test correctly formatted start datetime is returned during normal daylight hours
+ TestScenario(
+ (DEFAULT_START_DATE, time_zone_normal_parsable, 'DATE_TIME',
+ timezone('Europe/Paris'), noop_gettext, mock_strftime_localized),
+ "DATE_TIME " + "2016-03-27 01:59:00 CET"
+ ),
+ # Test correctly formatted start datetime is returned during daylight savings hours
+ TestScenario(
+ (DEFAULT_START_DATE, time_zone_daylight_parsable, 'DATE_TIME',
+ timezone('Europe/Paris'), noop_gettext, mock_strftime_localized),
+ "DATE_TIME " + "2016-03-27 03:00:00 CEST"
)
]),
FunctionTest(course_end_datetime_text, [
# Test with a set end datetime.
# Expect formatted datetime to be returned.
TestScenario(
- (test_datetime, 'TIME', mock_strftime_localized),
+ (test_datetime, 'TIME', utc, mock_strftime_localized),
mock_strftime_localized(test_datetime, 'TIME') + " UTC"
),
# Test with default end datetime.
# Expect empty string to be returned.
TestScenario(
- (None, 'TIME', mock_strftime_localized),
+ (None, 'TIME', utc, mock_strftime_localized),
""
+ ),
+ # Test correctly formatted end datetime is returned during normal daylight hours
+ TestScenario(
+ (time_zone_normal_datetime, 'TIME', timezone('Europe/Paris'), mock_strftime_localized),
+ "TIME " + "2016-03-27 01:59:00 CET"
+ ),
+ # Test correctly formatted end datetime is returned during daylight savings hours
+ TestScenario(
+ (time_zone_daylight_datetime, 'TIME', timezone('Europe/Paris'), mock_strftime_localized),
+ "TIME " + "2016-03-27 03:00:00 CEST"
)
]),
FunctionTest(may_certify_for_course, [
diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py
index 0c4e419bb5cf..2efa043b9a4b 100644
--- a/common/lib/xmodule/xmodule/tests/test_course_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_course_module.py
@@ -1,25 +1,25 @@
+"""Tests the course modules and their functions"""
+import ddt
import unittest
from datetime import datetime, timedelta
+import itertools
from fs.memoryfs import MemoryFS
-
from mock import Mock, patch
-import itertools
-
+from pytz import timezone, utc
from xblock.runtime import KvsFieldData, DictKeyValueStore
import xmodule.course_module
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey
-from django.utils.timezone import UTC
ORG = 'test_org'
COURSE = 'test_course'
-NOW = datetime.strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00').replace(tzinfo=UTC())
+NOW = datetime.strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00').replace(tzinfo=utc)
-_TODAY = datetime.now(UTC())
+_TODAY = datetime.now(utc)
_LAST_WEEK = _TODAY - timedelta(days=7)
_NEXT_WEEK = _TODAY + timedelta(days=7)
@@ -28,7 +28,7 @@ class CourseFieldsTestCase(unittest.TestCase):
def test_default_start_date(self):
self.assertEqual(
xmodule.course_module.CourseFields.start.default,
- datetime(2030, 1, 1, tzinfo=UTC())
+ datetime(2030, 1, 1, tzinfo=utc)
)
@@ -142,6 +142,7 @@ def test_may_certify(self):
self.assertFalse(self.future_noshow_certs.may_certify())
+@ddt.ddt
class IsNewCourseTestCase(unittest.TestCase):
"""Make sure the property is_new works on courses"""
@@ -224,6 +225,20 @@ def test_start_date_time_text(self, gmtime_mock):
print "Checking start=%s advertised=%s" % (setting[0], setting[1])
self.assertEqual(course.start_datetime_text("DATE_TIME"), setting[4])
+ @ddt.data(("2015-11-01T08:59", 'Nov 01, 2015', u'Nov 01, 2015 at 01:59 PDT'),
+ ("2015-11-01T09:00", 'Nov 01, 2015', u'Nov 01, 2015 at 01:00 PST'))
+ @ddt.unpack
+ def test_start_date_time_zone(self, course_date, expected_short_date, expected_date_time):
+ """
+ Test that start datetime text correctly formats datetimes
+ for normal daylight hours and daylight savings hours
+ """
+ time_zone = timezone('America/Los_Angeles')
+
+ course = get_dummy_course(start=course_date, advertised_start=course_date)
+ self.assertEqual(course.start_datetime_text(time_zone=time_zone), expected_short_date)
+ self.assertEqual(course.start_datetime_text("DATE_TIME", time_zone), expected_date_time)
+
def test_start_date_is_default(self):
for s in self.start_advertised_settings:
d = get_dummy_course(start=s[0], advertised_start=s[1])
@@ -277,6 +292,20 @@ def test_end_date_time_text(self):
course = get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
self.assertEqual('Sep 04, 2014 at 12:00 UTC', course.end_datetime_text("DATE_TIME"))
+ @ddt.data(("2015-11-01T08:59", 'Nov 01, 2015', u'Nov 01, 2015 at 01:59 PDT'),
+ ("2015-11-01T09:00", 'Nov 01, 2015', u'Nov 01, 2015 at 01:00 PST'))
+ @ddt.unpack
+ def test_end_date_time_zone(self, course_date, expected_short_date, expected_date_time):
+ """
+ Test that end datetime text correctly formats datetimes
+ for normal daylight hours and daylight savings hours
+ """
+ time_zone = timezone('America/Los_Angeles')
+ course = get_dummy_course(course_date, end=course_date)
+
+ self.assertEqual(course.end_datetime_text(time_zone=time_zone), expected_short_date)
+ self.assertEqual(course.end_datetime_text("DATE_TIME", time_zone), expected_date_time)
+
class DiscussionTopicsTestCase(unittest.TestCase):
def test_default_discussion_topics(self):
diff --git a/common/test/acceptance/pages/lms/ga_instructor_dashboard.py b/common/test/acceptance/pages/lms/ga_instructor_dashboard.py
index f480f9168e74..59ab44b461b6 100644
--- a/common/test/acceptance/pages/lms/ga_instructor_dashboard.py
+++ b/common/test/acceptance/pages/lms/ga_instructor_dashboard.py
@@ -101,3 +101,9 @@ def add_role(self, role_name, member):
self.q(css='div[data-rolename="{}"] input.add-field'.format(role_name)).fill(member)
self.q(css='div[data-rolename="{}"] input.add'.format(role_name)).click()
self.wait_for_ajax()
+
+ def add_role_by_display_name(self, role_name, member):
+ self.select_role(role_name)
+ self.q(css='div[data-display-name="{}"] input.add-field'.format(role_name)).fill(member)
+ self.q(css='div[data-display-name="{}"] input.add'.format(role_name)).click()
+ self.wait_for_ajax()
diff --git a/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py
index 675e76e56c77..0cfa2f233505 100644
--- a/common/test/acceptance/pages/studio/overview.py
+++ b/common/test/acceptance/pages/studio/overview.py
@@ -527,6 +527,7 @@ def change_problem_release_date_in_studio(self):
"""
self.q(css=".subsection-header-actions .configure-button").first.click()
self.q(css="#start_date").fill("01/01/2030")
+ self.q(css="#start_time").click()
self.q(css=".action-save").first.click()
self.wait_for_ajax()
diff --git a/common/test/acceptance/tests/ga_helpers.py b/common/test/acceptance/tests/ga_helpers.py
index 8b2936cfdd51..8c04e75ca34f 100644
--- a/common/test/acceptance/tests/ga_helpers.py
+++ b/common/test/acceptance/tests/ga_helpers.py
@@ -32,6 +32,16 @@
'password': 'GaOldCourse1',
'email': 'gaoldcourseviewer@example.com',
}
+GA_GLOBAL_COURSE_CREATOR_USER_INFO = {
+ 'username': 'Ga_Global_Course_Creator',
+ 'password': 'GaGlobalCourseCreator1',
+ 'email': 'ga_global_course_creator@example.com',
+}
+GA_COURSE_SCORER_USER_INFO = {
+ 'username': 'Ga_Course_Scorer',
+ 'password': 'Ga_Course_Scorer1',
+ 'email': 'ga_course_scorer@example.com',
+}
@unittest.skipUnless(os.environ.get('ENABLE_BOKCHOY_GA'), "Test only valid in gacco")
diff --git a/common/test/acceptance/tests/ga_role_helpers.py b/common/test/acceptance/tests/ga_role_helpers.py
new file mode 100644
index 000000000000..e763a21c4007
--- /dev/null
+++ b/common/test/acceptance/tests/ga_role_helpers.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+"""
+Test helper functions and base classes.
+"""
+from .ga_helpers import GaccoTestMixin, SUPER_USER_INFO, GA_COURSE_SCORER_USER_INFO, GA_GLOBAL_COURSE_CREATOR_USER_INFO
+from ..pages.lms.auto_auth import AutoAuthPage
+from ..pages.lms.ga_instructor_dashboard import MembershipPageMemberListSection, InstructorDashboardPage
+
+
+class GaccoTestRoleMixin(GaccoTestMixin):
+ def add_course_role(self, course_id, role_name, member):
+ self.switch_to_user(SUPER_USER_INFO)
+ instructor_dashboard_page = InstructorDashboardPage(self.browser, course_id).visit()
+ instructor_dashboard_page.select_membership()
+ MembershipPageMemberListSection(self.browser).wait_for_page().add_role_by_display_name(role_name, member)
+ self.logout()
+
+ def auto_auth_with_ga_global_course_creator(self, course_id):
+ # Auto-auth register for the course
+ AutoAuthPage(
+ self.browser,
+ username=GA_GLOBAL_COURSE_CREATOR_USER_INFO['username'],
+ password=GA_GLOBAL_COURSE_CREATOR_USER_INFO['password'],
+ email=GA_GLOBAL_COURSE_CREATOR_USER_INFO['email'],
+ course_id=course_id
+ ).visit()
+ return GA_GLOBAL_COURSE_CREATOR_USER_INFO
+
+ def auto_auth_with_ga_course_scorer(self, course_id):
+ self.add_course_role(course_id, 'Course Scorer', GA_COURSE_SCORER_USER_INFO['email'])
+ AutoAuthPage(
+ self.browser,
+ username=GA_COURSE_SCORER_USER_INFO['username'],
+ password=GA_COURSE_SCORER_USER_INFO['password'],
+ email=GA_COURSE_SCORER_USER_INFO['email'],
+ course_id=course_id
+ ).visit()
+ return GA_COURSE_SCORER_USER_INFO
diff --git a/common/test/acceptance/tests/lms/test_ga_courseware.py b/common/test/acceptance/tests/lms/test_ga_courseware.py
new file mode 100644
index 000000000000..64416b554585
--- /dev/null
+++ b/common/test/acceptance/tests/lms/test_ga_courseware.py
@@ -0,0 +1,312 @@
+"""
+End-to-end tests for the LMS.
+"""
+import ddt
+
+from ..ga_helpers import GaccoTestMixin, SUPER_USER_INFO
+from ..helpers import UniqueCourseTest
+from ...fixtures.course import CourseFixture, XBlockFixtureDesc
+from ...pages.lms.auto_auth import AutoAuthPage
+from ...pages.lms.course_nav import CourseNavPage
+from ...pages.lms.courseware import CoursewarePage, CoursewareSequentialTabPage
+from ...pages.lms.ga_django_admin import DjangoAdminPage
+from ...pages.lms.problem import ProblemPage
+from ...pages.studio.ga_overview import CourseOutlinePage
+from textwrap import dedent
+
+
+@ddt.ddt
+class CoursewareProgressRestrictionTest(UniqueCourseTest, GaccoTestMixin):
+
+ def setUp(self):
+ super(CoursewareProgressRestrictionTest, self).setUp()
+
+ self.user_info = {
+ 'username': 'STUDENT_TESTER_2147',
+ 'password': 'STUDENT_PASS',
+ 'email': 'student2147@example.com'
+ }
+ self.user_info_staff = {
+ 'username': 'STAFF_TESTER_2147',
+ 'password': 'STAFF_PASS',
+ 'email': 'staff2147@example.com'
+ }
+
+ self.course_info = {
+ 'org': 'test_org_00003',
+ 'number': self._testMethodName,
+ 'run': 'test_run_00003',
+ 'display_name': 'Progress Restriction Course'
+ }
+
+ self.course_fixture = CourseFixture(
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run'],
+ self.course_info['display_name']
+ )
+
+ problem1_xml = dedent("""
+
+ Answer is A
+
+
+
+
+ """)
+ problem2_xml = dedent("""
+
+ Answer is B
+
+
+
+
+ """)
+ self.course_fixture.add_children(
+ XBlockFixtureDesc('chapter', '1').add_children(
+ XBlockFixtureDesc('sequential', '1.1').add_children(
+ XBlockFixtureDesc('vertical', '1.1.1'),
+ XBlockFixtureDesc('vertical', '1.1.2').add_children(
+ XBlockFixtureDesc('problem', 'Test Problem 1', data=problem1_xml),
+ XBlockFixtureDesc('problem', 'Test Problem 2', data=problem2_xml),
+ ),
+ XBlockFixtureDesc('vertical', '1.1.3').add_children(
+ XBlockFixtureDesc('html', 'Test HTML 1', data='Test HTML 1')
+ )
+ ),
+ XBlockFixtureDesc('sequential', '1.2').add_children(
+ XBlockFixtureDesc('vertical', '1.2.1').add_children(
+ XBlockFixtureDesc('problem', 'Test Problem 3')
+ ),
+ XBlockFixtureDesc('vertical', '1.2.2').add_children(
+ XBlockFixtureDesc('problem', 'Test Problem 4')
+ )
+ )
+ ),
+ XBlockFixtureDesc('chapter', '2').add_children(
+ XBlockFixtureDesc('sequential', '2.1').add_children(
+ XBlockFixtureDesc('vertical', '2.1.1').add_children(
+ XBlockFixtureDesc('html', 'Test HTML 2', data='Test HTML 2')
+ )
+ ),
+ )
+ )
+ self.course_fixture.install()
+
+ self.course_outline_page = CourseOutlinePage(
+ self.browser,
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run']
+ )
+
+ self.courseware_page = CoursewarePage(self.browser, self.course_id)
+
+ def _switch_to_user(self, user_info, course_id=None, staff=None):
+ self.logout()
+ AutoAuthPage(
+ self.browser,
+ username=user_info['username'],
+ password=user_info['password'],
+ email=user_info['email'],
+ course_id=course_id,
+ staff=staff
+ ).visit()
+ return user_info
+
+ def _set_progress_restriction(self):
+ self._switch_to_user(SUPER_USER_INFO)
+
+ DjangoAdminPage(self.browser).visit().click_add('ga_optional', 'courseoptionalconfiguration').input({
+ 'enabled': True,
+ 'key': 'progress-restriction-settings',
+ 'course_key': self.course_id,
+ }).save()
+
+ def _set_passing_mark(self, section_at, subsection_at, unit_at, passing_mark):
+ self._set_progress_restriction()
+ self._switch_to_user(self.user_info_staff, course_id=self.course_id, staff=True)
+
+ self.course_outline_page.visit()
+ self.course_outline_page.expand_all_subsections()
+ unit = self.course_outline_page.section_at(section_at).subsection_at(subsection_at).unit_at(unit_at)
+ modal = unit.edit()
+ fields = modal.find_css('li.field-progress-restriction-passing-mark span.progress-restriction-percentage input')
+ fields.fill(passing_mark)
+ modal.save()
+
+ def _get_sequential_tab_page(self, position):
+ subsection_url = self.courseware_page.get_active_subsection_url()
+ url_part_list = subsection_url.split('/')
+ course_id = url_part_list[4]
+ chapter_id = url_part_list[-3]
+ subsection_id = url_part_list[-2]
+
+ return CoursewareSequentialTabPage(
+ self.browser,
+ course_id=course_id,
+ chapter=chapter_id,
+ subsection=subsection_id,
+ position=position
+ )
+
+ def test_accessibility_vertical_block(self):
+ self._set_passing_mark(0, 0, 1, 100)
+
+ # go to courseware
+ self._switch_to_user(self.user_info, course_id=self.course_id, staff=False)
+
+ self.courseware_page.visit()
+
+ course_nav = CourseNavPage(self.browser)
+ course_nav.go_to_section('1', '1.1')
+
+ # go to next unit of the unit that is set passing mark
+ # expecting not to be able to access
+ tab3_page = self._get_sequential_tab_page(3).visit()
+ self.assertIn('You need to make correct answers for the some problems in the before quiz.',
+ tab3_page.get_selected_tab_content())
+
+ # send correct answer to 1/2
+ tab2_page = self._get_sequential_tab_page(2).visit()
+ problem_page = ProblemPage(self.browser)
+ problem_page.fill_answer('A', 0)
+ problem_page.click_check()
+
+ # go to next unit of the unit that is set passing mark
+ # expecting not to be able to access yet
+ tab3_page.visit()
+ self.assertIn('You need to make correct answers for the some problems in the before quiz.',
+ tab3_page.get_selected_tab_content())
+
+ # send correct answer to 2/2
+ tab2_page.visit()
+ problem_page.fill_answer('B', 1)
+ problem_page.click_check()
+
+ # go to next unit of the unit that is set passing mark
+ # expecting to be able to access
+ tab3_page.visit()
+ self.assertNotIn('You need to make correct answers for the some problems in the before quiz.',
+ tab3_page.get_selected_tab_content())
+ self.assertIn('Test HTML 1',
+ tab3_page.get_selected_tab_content())
+
+ # resend incorrect answer
+ tab2_page.visit()
+ problem_page.fill_answer('C', 1)
+ problem_page.click_check()
+
+ # go to next unit of the unit that is set passing mark
+ # expecting to be able to access
+ tab3_page.visit()
+ self.assertNotIn('You need to make correct answers for the some problems in the before quiz.',
+ tab3_page.get_selected_tab_content())
+ self.assertIn('Test HTML 1',
+ tab3_page.get_selected_tab_content())
+
+ def test_can_access_previous_vertical_block(self):
+ self._set_passing_mark(0, 0, 1, 100)
+
+ # go to courseware
+ self._switch_to_user(self.user_info, course_id=self.course_id, staff=False)
+
+ self.courseware_page.visit()
+
+ course_nav = CourseNavPage(self.browser)
+ course_nav.go_to_section('1', '1.1')
+
+ # go to previous unit of the unit that is set passing mark
+ # expecting to be able to access
+ tab_page = self._get_sequential_tab_page(0).visit()
+ self.assertNotIn('You need to make correct answers for the some problems in the before quiz.',
+ tab_page.get_selected_tab_content())
+
+ @ddt.data(
+ ('1', '1.2', 'Test Problem 3'),
+ ('2', '2.1', 'Test HTML 2')
+ )
+ @ddt.unpack
+ def test_accessibility_sequential_block(self,
+ access_chapter_name,
+ access_sequential_name,
+ unit_content):
+ self._set_passing_mark(0, 0, 1, 100)
+
+ # go to courseware
+ self._switch_to_user(self.user_info, course_id=self.course_id, staff=False)
+
+ self.courseware_page.visit()
+
+ course_nav = CourseNavPage(self.browser)
+
+ # go to the following subsections of the unit that is set passing mark
+ # expecting not to be able to access
+ course_nav.go_to_section(access_chapter_name, access_sequential_name)
+
+ tab_page = self._get_sequential_tab_page(1).visit()
+ self.assertIn('You need to make correct answers for the some problems in the before quiz.',
+ tab_page.get_selected_tab_content())
+
+ # send correct answer
+ course_nav.go_to_section('1', '1.1')
+ self._get_sequential_tab_page(2).visit()
+ problem_page = ProblemPage(self.browser)
+ problem_page.fill_answer('A', 0)
+ problem_page.fill_answer('B', 1)
+ problem_page.click_check()
+
+ # go to the following subsections of the unit that is set passing mark
+ # expecting to be able to access
+ course_nav.go_to_section(access_chapter_name, access_sequential_name)
+ tab_page = self._get_sequential_tab_page(1).visit()
+ self.assertNotIn('You need to make correct answers for the some problems in the before quiz.',
+ tab_page.get_selected_tab_content())
+ self.assertIn(unit_content, tab_page.get_selected_tab_content())
+
+ def test_display_restricted_on_sidebar(self):
+ self._set_passing_mark(0, 0, 1, 100)
+
+ # go to courseware
+ self._switch_to_user(self.user_info, course_id=self.course_id, staff=False)
+
+ self.courseware_page.visit()
+
+ chapters = self.courseware_page.q(css='.course-index .accordion .course-navigation .chapter')
+ self.assertEqual(len(chapters), 2)
+
+ classes_of_chapters = chapters.attrs('class')
+ self.assertNotIn('restricted-chapter', classes_of_chapters[0].split(' '))
+ self.assertIn('restricted-chapter', classes_of_chapters[1].split(' '))
+
+ sections = self.courseware_page.q(css='.course-index .accordion .course-navigation .chapter-content-container .chapter-menu .menu-item')
+ self.assertEqual(len(sections), 3)
+
+ classes_of_sections = sections.attrs('class')
+ self.assertNotIn('restricted-section', classes_of_sections[0].split(' '))
+ self.assertIn('restricted-section', classes_of_sections[1].split(' '))
+ self.assertIn('restricted-section', classes_of_sections[2].split(' '))
+
+ # send correct answer
+ course_nav = CourseNavPage(self.browser)
+ course_nav.go_to_section('1', '1.1')
+ self._get_sequential_tab_page(2).visit()
+ problem_page = ProblemPage(self.browser)
+ problem_page.fill_answer('A', 0)
+ problem_page.fill_answer('B', 1)
+ problem_page.click_check()
+
+ chapters = self.courseware_page.q(css='.course-index .accordion .course-navigation .chapter')
+ self.assertEqual(len(chapters), 2)
+
+ classes_of_chapters = chapters.attrs('class')
+ self.assertNotIn('restricted-chapter', classes_of_chapters[0].split(' '))
+ self.assertNotIn('restricted-chapter', classes_of_chapters[1].split(' '))
+
+ sections = self.courseware_page.q(css='.course-index .accordion .course-navigation .chapter-content-container .chapter-menu .menu-item')
+ self.assertEqual(len(sections), 3)
+
+ classes_of_sections = sections.attrs('class')
+ self.assertNotIn('restricted-section', classes_of_sections[0].split(' '))
+ self.assertNotIn('restricted-section', classes_of_sections[1].split(' '))
+ self.assertNotIn('restricted-section', classes_of_sections[2].split(' '))
diff --git a/common/test/acceptance/tests/lms/test_ga_instructor_dashboard.py b/common/test/acceptance/tests/lms/test_ga_instructor_dashboard.py
index 927c76d1a2fe..127d1a45b2fd 100644
--- a/common/test/acceptance/tests/lms/test_ga_instructor_dashboard.py
+++ b/common/test/acceptance/tests/lms/test_ga_instructor_dashboard.py
@@ -16,12 +16,12 @@
from ...pages.lms.ga_instructor_dashboard import InstructorDashboardPage
from ...pages.lms.login_and_register import CombinedLoginAndRegisterPage
from ...tests.biz import PLATFORMER_USER_INFO, A_COMPANY, GaccoBizTestMixin
-from ..ga_helpers import GaccoTestMixin
+from ..ga_role_helpers import GaccoTestRoleMixin
from ..helpers import EventsTestMixin
from lms.envs.ga_bok_choy import EMAIL_FILE_PATH
-class GaccoLmsInstructorDashboardTestMixin(GaccoTestMixin):
+class GaccoLmsInstructorDashboardTestMixin(GaccoTestRoleMixin):
def _create_user(self, user, course_key, staff):
AutoAuthPage(
self.browser,
@@ -71,18 +71,22 @@ def setUp(self):
username_global_staff = 'test_globalstaff_' + self.unique_id[0:6]
username_course_staff = 'test_coursestaff_' + self.unique_id[0:6]
username_course_instructor = 'test_courseinstructor_' + self.unique_id[0:6]
+ username_course_ga_course_scorer = 'test_gacoursescorer_' + self.unique_id[0:6]
self.user_global_staff = {'username': username_global_staff, 'email': username_global_staff + '@example.com'}
self.user_course_staff = {'username': username_course_staff, 'email': username_course_staff + '@example.com'}
self.user_course_instructor = {'username': username_course_instructor, 'email': username_course_instructor + '@example.com'}
+ self.user_course_ga_course_scorer = {'username': username_course_ga_course_scorer, 'email': username_course_ga_course_scorer + '@example.com'}
self._create_user(self.user_global_staff, self.course_key, True)
self._create_user(self.user_course_staff, self.course_key, False)
self._create_user(self.user_course_instructor, self.course_key, False)
+ self._create_user(self.user_course_ga_course_scorer, self.course_key, False)
# Add course team members
CourseTeamFixture(self.course_key, self.user_course_staff['email'], False).install()
CourseTeamFixture(self.course_key, self.user_course_instructor['email'], True).install()
+ self.add_course_role(self.course_key, 'Course Scorer', self.user_course_ga_course_scorer['email'])
# Initialize pages.
self.dashboard_page = DashboardPage(self.browser)
@@ -152,6 +156,20 @@ def test_displayed_optout_checkbox_only_global_staff(self):
# Logout
self.logout_page.visit()
+ ## login as ga lms course staff ##
+ self._login(self.user_course_ga_course_scorer['email'])
+
+ # Visit instructor dashboard and send email section
+ self.instructor_dashboard_page.visit()
+ send_email_section = self.instructor_dashboard_page.select_send_email()
+
+ # Select all
+ send_email_section.select_send_to('all')
+ self.assertFalse(send_email_section.is_visible_optout_container())
+
+ # Logout
+ self.logout_page.visit()
+
def test_send_to_all_include_optout(self):
"""
Scenario:
@@ -189,7 +207,8 @@ def test_send_to_all_include_optout(self):
self.assertItemsEqual([
self.user_global_staff['email'],
self.user_course_instructor['email'],
- self.user_course_staff['email']
+ self.user_course_staff['email'],
+ self.user_course_ga_course_scorer['email'],
], [message['to_addresses'] for message in messages])
self.email_client.clear_messages()
@@ -208,6 +227,7 @@ def test_send_to_all_include_optout(self):
self.user_global_staff['email'],
self.user_course_instructor['email'],
self.user_course_staff['email'],
+ self.user_course_ga_course_scorer['email'],
user_optout['email']
], [message['to_addresses'] for message in messages])
@@ -313,7 +333,7 @@ def _refund_ticket(order_id, amount, tax, currency='JPN'):
_expected_emails = [
user['email'] for user in users
- ] + [self.user_course_instructor['email'], self.user_course_staff['email']]
+ ] + [self.user_course_instructor['email'], self.user_course_staff['email'], self.user_course_ga_course_scorer['email']]
# this filtering is the process for excluding users to be automatically created by fixture.
messages = filter(lambda msg: msg['to_addresses'].startswith('test_'), self.email_client.get_messages())
diff --git a/common/test/acceptance/tests/lms/test_ga_lms.py b/common/test/acceptance/tests/lms/test_ga_lms.py
new file mode 100644
index 000000000000..8d1ff8710d44
--- /dev/null
+++ b/common/test/acceptance/tests/lms/test_ga_lms.py
@@ -0,0 +1,948 @@
+# -*- coding: utf-8 -*-
+"""
+End-to-end tests for the LMS.
+"""
+
+from textwrap import dedent
+from nose.plugins.attrib import attr
+
+from bok_choy.promise import EmptyPromise
+from ..ga_helpers import GA_COURSE_SCORER_USER_INFO, GA_GLOBAL_COURSE_CREATOR_USER_INFO, GA_OLD_COURSE_VIEWER_USER_INFO
+from ..ga_role_helpers import GaccoTestRoleMixin
+from ..helpers import (
+ UniqueCourseTest,
+ EventsTestMixin,
+ load_data_str,
+ generate_course_key,
+ select_option_by_value,
+ element_has_text
+)
+from ...fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
+from ...pages.common.logout import LogoutPage
+from ...pages.lms.auto_auth import AutoAuthPage
+from ...pages.lms.courseware import CoursewarePage
+from ...pages.lms.course_info import CourseInfoPage
+from ...pages.lms.course_nav import CourseNavPage
+from ...pages.lms.course_wiki import CourseWikiPage, CourseWikiEditPage
+from ...pages.lms.dashboard import DashboardPage
+from ...pages.lms.problem import ProblemPage
+from ...pages.lms.progress import ProgressPage
+from ...pages.lms.tab_nav import TabNavPage
+from ...pages.lms.video.video import VideoPage
+from ...pages.studio.settings import SettingsPage
+
+
+@attr('shard_1')
+class CourseWikiTestWithGaGlobalCourseCreator(UniqueCourseTest, GaccoTestRoleMixin):
+ """
+ Tests that verify the course wiki.
+ """
+ def _auto_auth(self):
+ self.auto_auth_with_ga_global_course_creator(self.course_id)
+
+ def setUp(self):
+ """
+ Initialize pages and install a course fixture.
+ """
+ super(CourseWikiTestWithGaGlobalCourseCreator, self).setUp()
+
+ # self.course_info['number'] must be shorter since we are accessing the wiki. See TNL-1751
+ self.course_info['number'] = self.unique_id[0:6]
+
+ self.course_info_page = CourseInfoPage(self.browser, self.course_id)
+ self.course_wiki_page = CourseWikiPage(self.browser, self.course_id)
+ self.course_info_page = CourseInfoPage(self.browser, self.course_id)
+ self.course_wiki_edit_page = CourseWikiEditPage(self.browser, self.course_id, self.course_info)
+ self.tab_nav = TabNavPage(self.browser)
+
+ CourseFixture(
+ self.course_info['org'], self.course_info['number'],
+ self.course_info['run'], self.course_info['display_name']
+ ).install()
+
+ # Auto-auth register for the course
+ self._auto_auth()
+
+ # Access course wiki page
+ self.course_info_page.visit()
+ self.tab_nav.go_to_tab('Wiki')
+
+ def _open_editor(self):
+ self.course_wiki_page.open_editor()
+ self.course_wiki_edit_page.wait_for_page()
+
+ def test_edit_course_wiki(self):
+ """
+ Wiki page by default is editable for students.
+
+ After accessing the course wiki,
+ Replace the content of the default page
+ Confirm new content has been saved
+
+ """
+ content = "hello"
+ self._open_editor()
+ self.course_wiki_edit_page.replace_wiki_content(content)
+ self.course_wiki_edit_page.save_wiki_content()
+ actual_content = unicode(self.course_wiki_page.q(css='.wiki-article p').text[0])
+ self.assertEqual(content, actual_content)
+
+
+@attr('shard_1')
+class CourseWikiTestWithGaCourseScorer(CourseWikiTestWithGaGlobalCourseCreator):
+ """
+ Tests that verify the course wiki.
+ """
+ def _auto_auth(self):
+ self.auto_auth_with_ga_course_scorer(self.course_id)
+
+
+@attr('shard_1')
+class HighLevelTabTestWithGaGlobalCourseCreator(UniqueCourseTest, GaccoTestRoleMixin):
+ """
+ Tests that verify each of the high-level tabs available within a course.
+ """
+
+ def _auto_auth(self):
+ # Auto-auth register for the course
+ self.auto_auth_with_ga_global_course_creator(self.course_id)
+
+ def setUp(self):
+ """
+ Initialize pages and install a course fixture.
+ """
+ super(HighLevelTabTestWithGaGlobalCourseCreator, self).setUp()
+
+ # self.course_info['number'] must be shorter since we are accessing the wiki. See TNL-1751
+ self.course_info['number'] = self.unique_id[0:6]
+
+ self.course_info_page = CourseInfoPage(self.browser, self.course_id)
+ self.progress_page = ProgressPage(self.browser, self.course_id)
+ self.course_nav = CourseNavPage(self.browser)
+ self.tab_nav = TabNavPage(self.browser)
+ self.video = VideoPage(self.browser)
+
+ # Install a course with sections/problems, tabs, updates, and handouts
+ course_fix = CourseFixture(
+ self.course_info['org'], self.course_info['number'],
+ self.course_info['run'], self.course_info['display_name']
+ )
+
+ course_fix.add_update(
+ CourseUpdateDesc(date='January 29, 2014', content='Test course update1')
+ )
+
+ course_fix.add_handout('demoPDF.pdf')
+
+ course_fix.add_children(
+ XBlockFixtureDesc('static_tab', 'Test Static Tab'),
+ XBlockFixtureDesc('chapter', 'Test Section').add_children(
+ XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
+ XBlockFixtureDesc('problem', 'Test Problem 1', data=load_data_str('multiple_choice.xml')),
+ XBlockFixtureDesc('problem', 'Test Problem 2', data=load_data_str('formula_problem.xml')),
+ XBlockFixtureDesc('html', 'Test HTML'),
+ )
+ ),
+ XBlockFixtureDesc('chapter', 'Test Section 2').add_children(
+ XBlockFixtureDesc('sequential', 'Test Subsection 2'),
+ XBlockFixtureDesc('sequential', 'Test Subsection 3'),
+ )
+ ).install()
+
+ self._auto_auth()
+
+ def test_course_info(self):
+ """
+ Navigate to the course info page.
+ """
+
+ # Navigate to the course info page from the progress page
+ self.progress_page.visit()
+ self.tab_nav.go_to_tab('Course Info')
+
+ # Expect just one update
+ self.assertEqual(self.course_info_page.num_updates, 1)
+
+ # Expect a link to the demo handout pdf
+ handout_links = self.course_info_page.handout_links
+ self.assertEqual(len(handout_links), 1)
+ self.assertIn('demoPDF.pdf', handout_links[0])
+
+ def test_progress(self):
+ """
+ Navigate to the progress page.
+ """
+ # Navigate to the progress page from the info page
+ self.course_info_page.visit()
+ self.tab_nav.go_to_tab('Progress')
+
+ # We haven't answered any problems yet, so assume scores are zero
+ # Only problems should have scores; so there should be 2 scores.
+ CHAPTER = 'Test Section'
+ SECTION = 'Test Subsection'
+ EXPECTED_SCORES = [(0, 3), (0, 1)]
+
+ actual_scores = self.progress_page.scores(CHAPTER, SECTION)
+ self.assertEqual(actual_scores, EXPECTED_SCORES)
+
+ def test_static_tab(self):
+ """
+ Navigate to a static tab (course content)
+ """
+ # From the course info page, navigate to the static tab
+ self.course_info_page.visit()
+ self.tab_nav.go_to_tab('Test Static Tab')
+ self.assertTrue(self.tab_nav.is_on_tab('Test Static Tab'))
+
+ def test_wiki_tab_first_time(self):
+ """
+ Navigate to the course wiki tab. When the wiki is accessed for
+ the first time, it is created on the fly.
+ """
+
+ course_wiki = CourseWikiPage(self.browser, self.course_id)
+ # From the course info page, navigate to the wiki tab
+ self.course_info_page.visit()
+ self.tab_nav.go_to_tab('Wiki')
+ self.assertTrue(self.tab_nav.is_on_tab('Wiki'))
+
+ # Assert that a default wiki is created
+ expected_article_name = "{org}.{course_number}.{course_run}".format(
+ org=self.course_info['org'],
+ course_number=self.course_info['number'],
+ course_run=self.course_info['run']
+ )
+ self.assertEqual(expected_article_name, course_wiki.article_name)
+
+ def test_courseware_nav(self):
+ """
+ Navigate to a particular unit in the courseware.
+ """
+ # Navigate to the courseware page from the info page
+ self.course_info_page.visit()
+ self.tab_nav.go_to_tab('Courseware')
+
+ # Check that the courseware navigation appears correctly
+ EXPECTED_SECTIONS = {
+ 'Test Section': ['Test Subsection'],
+ 'Test Section 2': ['Test Subsection 2', 'Test Subsection 3']
+ }
+
+ actual_sections = self.course_nav.sections
+ for section, subsections in EXPECTED_SECTIONS.iteritems():
+ self.assertIn(section, actual_sections)
+ self.assertEqual(actual_sections[section], EXPECTED_SECTIONS[section])
+
+ # Navigate to a particular section
+ self.course_nav.go_to_section('Test Section', 'Test Subsection')
+
+ # Check the sequence items
+ EXPECTED_ITEMS = ['Test Problem 1', 'Test Problem 2', 'Test HTML']
+
+ actual_items = self.course_nav.sequence_items
+ self.assertEqual(len(actual_items), len(EXPECTED_ITEMS))
+ for expected in EXPECTED_ITEMS:
+ self.assertIn(expected, actual_items)
+
+
+@attr('shard_1')
+class HighLevelTabTestWithGaCourseScorer(HighLevelTabTestWithGaGlobalCourseCreator):
+ """
+ Tests that verify each of the high-level tabs available within a course.
+ """
+
+ def _auto_auth(self):
+ # Auto-auth register for the course
+ self.auto_auth_with_ga_course_scorer(self.course_id)
+
+
+class PDFTextBooksTabTestWithGaGlobalCourseCreator(UniqueCourseTest, GaccoTestRoleMixin):
+ """
+ Tests that verify each of the textbook tabs available within a course.
+ """
+ def _auto_auth(self):
+ self.auto_auth_with_ga_global_course_creator(self.course_id)
+
+ def setUp(self):
+ """
+ Initialize pages and install a course fixture.
+ """
+ super(PDFTextBooksTabTestWithGaGlobalCourseCreator, self).setUp()
+
+ self.course_info_page = CourseInfoPage(self.browser, self.course_id)
+ self.tab_nav = TabNavPage(self.browser)
+
+ # Install a course with TextBooks
+ course_fix = CourseFixture(
+ self.course_info['org'], self.course_info['number'],
+ self.course_info['run'], self.course_info['display_name']
+ )
+
+ # Add PDF textbooks to course fixture.
+ for i in range(1, 3):
+ course_fix.add_textbook("PDF Book {}".format(i), [{"title": "Chapter Of Book {}".format(i), "url": ""}])
+
+ course_fix.install()
+
+ # Auto-auth register for the course
+ self._auto_auth()
+
+ def test_verify_textbook_tabs(self):
+ """
+ Test multiple pdf textbooks loads correctly in lms.
+ """
+ self.course_info_page.visit()
+
+ # Verify each PDF textbook tab by visiting, it will fail if correct tab is not loaded.
+ for i in range(1, 3):
+ self.tab_nav.go_to_tab("PDF Book {}".format(i))
+
+
+class PDFTextBooksTabTestWithGaCourseScorer(PDFTextBooksTabTestWithGaGlobalCourseCreator):
+ """
+ Tests that verify each of the textbook tabs available within a course.
+ """
+ def _auto_auth(self):
+ self.auto_auth_with_ga_course_scorer(self.course_id)
+
+
+@attr('shard_1')
+class VisibleToStaffOnlyTest(UniqueCourseTest, GaccoTestRoleMixin):
+ """
+ Tests that content with visible_to_staff_only set to True cannot be viewed by students.
+ """
+ def setUp(self):
+ super(VisibleToStaffOnlyTest, self).setUp()
+
+ course_fix = CourseFixture(
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run'],
+ self.course_info['display_name']
+ )
+
+ course_fix.add_children(
+ XBlockFixtureDesc('chapter', 'Test Section').add_children(
+ XBlockFixtureDesc('sequential', 'Subsection With Locked Unit').add_children(
+ XBlockFixtureDesc('vertical', 'Locked Unit', metadata={'visible_to_staff_only': True}).add_children(
+ XBlockFixtureDesc('html', 'Html Child in locked unit', data="Visible only to staff"),
+ ),
+ XBlockFixtureDesc('vertical', 'Unlocked Unit').add_children(
+ XBlockFixtureDesc('html', 'Html Child in unlocked unit', data="Visible only to all"),
+ )
+ ),
+ XBlockFixtureDesc('sequential', 'Unlocked Subsection').add_children(
+ XBlockFixtureDesc('vertical', 'Test Unit').add_children(
+ XBlockFixtureDesc('html', 'Html Child in visible unit', data="Visible to all"),
+ )
+ ),
+ XBlockFixtureDesc('sequential', 'Locked Subsection', metadata={'visible_to_staff_only': True}).add_children(
+ XBlockFixtureDesc('vertical', 'Test Unit').add_children(
+ XBlockFixtureDesc(
+ 'html', 'Html Child in locked subsection', data="Visible only to staff"
+ )
+ )
+ )
+ )
+ ).install()
+
+ self.courseware_page = CoursewarePage(self.browser, self.course_id)
+ self.course_nav = CourseNavPage(self.browser)
+
+ def test_visible_to_ga_old_course_viewer(self):
+ """
+ Scenario: Content marked 'visible_to_staff_only' is not visible for students in the course
+ Given some of the course content has been marked 'visible_to_staff_only'
+ And I am logged on with an authorized ga_old_course_viewer account
+ Then I can only see content without 'visible_to_staff_only' set to True
+ """
+ AutoAuthPage(
+ self.browser,
+ username=GA_OLD_COURSE_VIEWER_USER_INFO['username'],
+ password=GA_OLD_COURSE_VIEWER_USER_INFO['password'],
+ email=GA_OLD_COURSE_VIEWER_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+
+ self.courseware_page.visit()
+ self.assertEqual(2, len(self.course_nav.sections['Test Section']))
+
+ self.course_nav.go_to_section("Test Section", "Subsection With Locked Unit")
+ self.assertEqual(["Html Child in unlocked unit"], self.course_nav.sequence_items)
+
+ self.course_nav.go_to_section("Test Section", "Unlocked Subsection")
+ self.assertEqual(["Html Child in visible unit"], self.course_nav.sequence_items)
+
+ def test_visible_to_ga_global_course_creator(self):
+ """
+ Scenario: Content marked 'visible_to_staff_only' is not visible for students in the course
+ Given some of the course content has been marked 'visible_to_staff_only'
+ And I am logged on with an authorized ga_global_course_creator account
+ Then I can only see content without 'visible_to_staff_only' set to True
+ """
+ """
+ Scenario: All content is visible for a user marked is_staff (different from course staff)
+ Given some of the course content has been marked 'visible_to_staff_only'
+ And I am logged on with an authorized ga_global_course_creator account
+ Then I can see all course content
+ """
+ AutoAuthPage(
+ self.browser,
+ username=GA_GLOBAL_COURSE_CREATOR_USER_INFO['username'],
+ password=GA_GLOBAL_COURSE_CREATOR_USER_INFO['password'],
+ email=GA_GLOBAL_COURSE_CREATOR_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+
+ self.courseware_page.visit()
+ self.assertEqual(3, len(self.course_nav.sections['Test Section']))
+
+ self.course_nav.go_to_section("Test Section", "Subsection With Locked Unit")
+ self.assertEqual(["Html Child in locked unit", "Html Child in unlocked unit"], self.course_nav.sequence_items)
+
+ self.course_nav.go_to_section("Test Section", "Unlocked Subsection")
+ self.assertEqual(["Html Child in visible unit"], self.course_nav.sequence_items)
+
+ self.course_nav.go_to_section("Test Section", "Locked Subsection")
+ self.assertEqual(["Html Child in locked subsection"], self.course_nav.sequence_items)
+
+ def test_visible_to_ga_course_scorer(self):
+ """
+ Scenario: Content marked 'visible_to_staff_only' is not visible for students in the course
+ Given some of the course content has been marked 'visible_to_staff_only'
+ And I am logged on with an authorized ga_course_scorer account
+ Then I can only see content without 'visible_to_staff_only' set to True
+ """
+ """
+ Scenario: All content is visible for a user marked is_staff (different from course staff)
+ Given some of the course content has been marked 'visible_to_staff_only'
+ And I am logged on with an authorized ga_course_scorer account
+ Then I can see all course content
+ """
+ self.add_course_role(self.course_id, 'Course Scorer', GA_COURSE_SCORER_USER_INFO['email'])
+
+ # Logout and login as a ga_course_scorer
+ AutoAuthPage(
+ self.browser,
+ username=GA_COURSE_SCORER_USER_INFO['username'],
+ password=GA_COURSE_SCORER_USER_INFO['password'],
+ email=GA_COURSE_SCORER_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+
+ self.courseware_page.visit()
+ self.assertEqual(3, len(self.course_nav.sections['Test Section']))
+
+ self.course_nav.go_to_section("Test Section", "Subsection With Locked Unit")
+ self.assertEqual(["Html Child in locked unit", "Html Child in unlocked unit"], self.course_nav.sequence_items)
+
+ self.course_nav.go_to_section("Test Section", "Unlocked Subsection")
+ self.assertEqual(["Html Child in visible unit"], self.course_nav.sequence_items)
+
+ self.course_nav.go_to_section("Test Section", "Locked Subsection")
+ self.assertEqual(["Html Child in locked subsection"], self.course_nav.sequence_items)
+
+
+@attr('shard_1')
+class TooltipTestWithGaGlobalCourseCreator(UniqueCourseTest, GaccoTestRoleMixin):
+ """
+ Tests that tooltips are displayed
+ """
+ def _auto_auth(self):
+ # Auto-auth register for the course
+ self.auto_auth_with_ga_global_course_creator(self.course_id)
+
+ def setUp(self):
+ """
+ Initialize pages and install a course fixture.
+ """
+ super(TooltipTestWithGaGlobalCourseCreator, self).setUp()
+
+ self.course_info_page = CourseInfoPage(self.browser, self.course_id)
+ self.tab_nav = TabNavPage(self.browser)
+
+ course_fix = CourseFixture(
+ self.course_info['org'], self.course_info['number'],
+ self.course_info['run'], self.course_info['display_name']
+ )
+
+ course_fix.add_children(
+ XBlockFixtureDesc('static_tab', 'Test Static Tab'),
+ XBlockFixtureDesc('chapter', 'Test Section').add_children(
+ XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
+ XBlockFixtureDesc('problem', 'Test Problem 1', data=load_data_str('multiple_choice.xml')),
+ XBlockFixtureDesc('problem', 'Test Problem 2', data=load_data_str('formula_problem.xml')),
+ XBlockFixtureDesc('html', 'Test HTML'),
+ )
+ )
+ ).install()
+
+ self.courseware_page = CoursewarePage(self.browser, self.course_id)
+ # Auto-auth register for the course
+ self._auto_auth()
+
+ def test_tooltip(self):
+ """
+ Verify that tooltips are displayed when you hover over the sequence nav bar.
+ """
+ self.course_info_page.visit()
+ self.tab_nav.go_to_tab('Courseware')
+
+ self.assertTrue(self.courseware_page.tooltips_displayed())
+
+
+@attr('shard_1')
+class TooltipTestWithGaCourseScorer(TooltipTestWithGaGlobalCourseCreator):
+ """
+ Tests that tooltips are displayed
+ """
+ def _auto_auth(self):
+ # Auto-auth register for the course
+ self.auto_auth_with_ga_course_scorer(self.course_id)
+
+
+@attr('shard_1')
+class PreRequisiteCourseTest(UniqueCourseTest, GaccoTestRoleMixin):
+ """
+ Tests that pre-requisite course messages are displayed
+ """
+
+ def setUp(self):
+ """
+ Initialize pages and install a course fixture.
+ """
+ super(PreRequisiteCourseTest, self).setUp()
+
+ CourseFixture(
+ self.course_info['org'], self.course_info['number'],
+ self.course_info['run'], self.course_info['display_name']
+ ).install()
+
+ self.prc_info = {
+ 'org': 'test_org',
+ 'number': self.unique_id,
+ 'run': 'prc_test_run',
+ 'display_name': 'PR Test Course' + self.unique_id
+ }
+
+ CourseFixture(
+ self.prc_info['org'], self.prc_info['number'],
+ self.prc_info['run'], self.prc_info['display_name']
+ ).install()
+
+ pre_requisite_course_key = generate_course_key(
+ self.prc_info['org'],
+ self.prc_info['number'],
+ self.prc_info['run']
+ )
+ self.pre_requisite_course_id = unicode(pre_requisite_course_key)
+
+ self.dashboard_page = DashboardPage(self.browser)
+ self.settings_page = SettingsPage(
+ self.browser,
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run']
+
+ )
+ # Auto-auth register for the course
+ AutoAuthPage(self.browser, course_id=self.course_id).visit()
+
+ def test_dashboard_message(self):
+ """
+ Scenario: Any course where there is a Pre-Requisite course Student dashboard should have
+ appropriate messaging.
+ Given that I am on the Student dashboard
+ When I view a course with a pre-requisite course set
+ Then At the bottom of course I should see course requirements message.'
+ """
+
+ # visit dashboard page and make sure there is not pre-requisite course message
+ self.dashboard_page.visit()
+ self.assertFalse(self.dashboard_page.pre_requisite_message_displayed())
+
+ # Logout and Login as a ga_old_course_viewer
+ # self.switch_to_user(GA_OLD_COURSE_VIEWER_USER_INFO, self.course_id)
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(
+ self.browser,
+ username=GA_OLD_COURSE_VIEWER_USER_INFO['username'],
+ password=GA_OLD_COURSE_VIEWER_USER_INFO['password'],
+ email=GA_OLD_COURSE_VIEWER_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # visit dashboard page and make sure there is not pre-requisite course message
+ self.dashboard_page.visit()
+ self.assertFalse(self.dashboard_page.pre_requisite_message_displayed())
+
+ # Logout and Login as a ga_global_course_creator
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(
+ self.browser,
+ username=GA_GLOBAL_COURSE_CREATOR_USER_INFO['username'],
+ password=GA_GLOBAL_COURSE_CREATOR_USER_INFO['password'],
+ email=GA_GLOBAL_COURSE_CREATOR_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # visit dashboard page and make sure there is pre-requisite course message
+ self.dashboard_page.visit()
+ self.assertFalse(self.dashboard_page.pre_requisite_message_displayed())
+
+ # Logout and Login as a ga_course_scorer
+ LogoutPage(self.browser).visit()
+ self.add_course_role(self.course_id, 'Course Scorer', GA_COURSE_SCORER_USER_INFO['email'])
+ AutoAuthPage(
+ self.browser,
+ username=GA_COURSE_SCORER_USER_INFO['username'],
+ password=GA_COURSE_SCORER_USER_INFO['password'],
+ email=GA_COURSE_SCORER_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # visit dashboard page and make sure there is pre-requisite course message
+ self.dashboard_page.visit()
+ self.assertFalse(self.dashboard_page.pre_requisite_message_displayed())
+
+ # 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 pre-requisite course
+ self.settings_page.visit()
+ self._set_pre_requisite_course()
+
+ # Logout and login as a student.
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(self.browser, course_id=self.course_id, staff=False).visit()
+
+ # visit dashboard page again now it should have pre-requisite course message
+ self.dashboard_page.visit()
+ EmptyPromise(lambda: self.dashboard_page.available_courses > 0, 'Dashboard page loaded').fulfill()
+ self.assertTrue(self.dashboard_page.pre_requisite_message_displayed())
+
+ # Logout and Login as a ga_old_course_viewer
+ # self.switch_to_user(GA_OLD_COURSE_VIEWER_USER_INFO)
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(
+ self.browser,
+ username=GA_OLD_COURSE_VIEWER_USER_INFO['username'],
+ password=GA_OLD_COURSE_VIEWER_USER_INFO['password'],
+ email=GA_OLD_COURSE_VIEWER_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # visit dashboard page again now it should have pre-requisite course message
+ self.dashboard_page.visit()
+ EmptyPromise(lambda: self.dashboard_page.available_courses > 0, 'Dashboard page loaded').fulfill()
+ self.assertTrue(self.dashboard_page.pre_requisite_message_displayed())
+
+ # Logout and Login as a ga_global_course_creator
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(
+ self.browser,
+ username=GA_GLOBAL_COURSE_CREATOR_USER_INFO['username'],
+ password=GA_GLOBAL_COURSE_CREATOR_USER_INFO['password'],
+ email=GA_GLOBAL_COURSE_CREATOR_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # visit dashboard page and make sure there is pre-requisite course message
+ self.dashboard_page.visit()
+ EmptyPromise(lambda: self.dashboard_page.available_courses > 0, 'Dashboard page loaded').fulfill()
+ self.assertTrue(self.dashboard_page.pre_requisite_message_displayed())
+
+ # Logout and Login as a ga_course_scorer
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(
+ self.browser,
+ username=GA_COURSE_SCORER_USER_INFO['username'],
+ password=GA_COURSE_SCORER_USER_INFO['password'],
+ email=GA_COURSE_SCORER_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # visit dashboard page and make sure there is pre-requisite course message
+ self.dashboard_page.visit()
+ EmptyPromise(lambda: self.dashboard_page.available_courses > 0, 'Dashboard page loaded').fulfill()
+ self.assertTrue(self.dashboard_page.pre_requisite_message_displayed())
+
+ def _set_pre_requisite_course(self):
+ """
+ set pre-requisite course
+ """
+ select_option_by_value(self.settings_page.pre_requisite_course_options, self.pre_requisite_course_id)
+ self.settings_page.save_changes()
+
+
+@attr('shard_1')
+class ProblemExecutionTestWithGaGlobalCourseCreator(UniqueCourseTest, GaccoTestRoleMixin):
+ """
+ Tests of problems.
+ """
+
+ def _auto_auth(self):
+ # Auto-auth register for the course
+ self.auto_auth_with_ga_global_course_creator(self.course_id)
+
+ def setUp(self):
+ """
+ Initialize pages and install a course fixture.
+ """
+ super(ProblemExecutionTestWithGaGlobalCourseCreator, self).setUp()
+
+ self.course_info_page = CourseInfoPage(self.browser, self.course_id)
+ self.course_nav = CourseNavPage(self.browser)
+ self.tab_nav = TabNavPage(self.browser)
+
+ # Install a course with sections and problems.
+ course_fix = CourseFixture(
+ self.course_info['org'], self.course_info['number'],
+ self.course_info['run'], self.course_info['display_name']
+ )
+
+ course_fix.add_asset(['python_lib.zip'])
+
+ course_fix.add_children(
+ XBlockFixtureDesc('chapter', 'Test Section').add_children(
+ XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
+ XBlockFixtureDesc('problem', 'Python Problem', data=dedent(
+ """\
+
+
+
+ What is the sum of $oneseven and 3?
+
+
+
+
+
+ """
+ ))
+ )
+ )
+ ).install()
+
+ self._auto_auth()
+
+ def test_python_execution_in_problem(self):
+ # Navigate to the problem page
+ self.course_info_page.visit()
+ self.tab_nav.go_to_tab('Courseware')
+ self.course_nav.go_to_section('Test Section', 'Test Subsection')
+
+ problem_page = ProblemPage(self.browser)
+ # text-transform property is uppercase in original, but none in gacco.css
+ self.assertEqual(problem_page.problem_name, 'Python Problem')
+
+ # Does the page have computation results?
+ self.assertIn("What is the sum of 17 and 3?", problem_page.problem_text)
+
+ # Fill in the answer correctly.
+ problem_page.fill_answer("20")
+ problem_page.click_check()
+ self.assertTrue(problem_page.is_correct())
+
+ # Fill in the answer incorrectly.
+ problem_page.fill_answer("4")
+ problem_page.click_check()
+ self.assertFalse(problem_page.is_correct())
+
+
+@attr('shard_1')
+class ProblemExecutionTestWithGaCourseScorer(ProblemExecutionTestWithGaGlobalCourseCreator):
+ """
+ Tests of problems.
+ """
+
+ def _auto_auth(self):
+ # Auto-auth register for the course
+ self.auto_auth_with_ga_course_scorer(self.course_id)
+
+
+@attr('shard_1')
+class EntranceExamTest(UniqueCourseTest, GaccoTestRoleMixin):
+ """
+ Tests that course has an entrance exam.
+ """
+
+ def setUp(self):
+ """
+ Initialize pages and install a course fixture.
+ """
+ super(EntranceExamTest, self).setUp()
+
+ CourseFixture(
+ self.course_info['org'], self.course_info['number'],
+ self.course_info['run'], self.course_info['display_name']
+ ).install()
+
+ self.courseware_page = CoursewarePage(self.browser, self.course_id)
+ self.settings_page = SettingsPage(
+ self.browser,
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run']
+ )
+
+ # Auto-auth register for the course
+ AutoAuthPage(self.browser, course_id=self.course_id).visit()
+
+ def test_entrance_exam_section(self):
+ """
+ Scenario: Any course that is enabled for an entrance exam, should have entrance exam chapter at courseware
+ page.
+ Given that I am on the courseware page
+ When I view the courseware that has an entrance exam
+ Then there should be an "Entrance Exam" chapter.'
+ """
+ entrance_exam_link_selector = '.accordion .course-navigation .chapter .group-heading'
+ # visit courseware page and make sure there is not entrance exam chapter.
+ self.courseware_page.visit()
+ self.courseware_page.wait_for_page()
+ self.assertFalse(element_has_text(
+ page=self.courseware_page,
+ css_selector=entrance_exam_link_selector,
+ text='Entrance Exam'
+ ))
+
+ # Logout and login as a ga_old_course_viewer
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(
+ self.browser,
+ username=GA_OLD_COURSE_VIEWER_USER_INFO['username'],
+ password=GA_OLD_COURSE_VIEWER_USER_INFO['password'],
+ email=GA_OLD_COURSE_VIEWER_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # self.switch_to_user(GA_OLD_COURSE_VIEWER_USER_INFO, self.course_id)
+ # visit courseware page and make sure there is not entrance exam chapter.
+ self.courseware_page.visit()
+ self.courseware_page.wait_for_page()
+ self.assertFalse(element_has_text(
+ page=self.courseware_page,
+ css_selector=entrance_exam_link_selector,
+ text='Entrance Exam'
+ ))
+
+ # Logout and login as a ga_global_course_creator
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(
+ self.browser,
+ username=GA_GLOBAL_COURSE_CREATOR_USER_INFO['username'],
+ password=GA_GLOBAL_COURSE_CREATOR_USER_INFO['password'],
+ email=GA_GLOBAL_COURSE_CREATOR_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # visit courseware page and make sure there is entrance exam chapter.
+ self.courseware_page.visit()
+ self.courseware_page.wait_for_page()
+ self.assertFalse(element_has_text(
+ page=self.courseware_page,
+ css_selector=entrance_exam_link_selector,
+ text='Entrance Exam'
+ ))
+
+ # Logout and login as a ga_course_scorer
+ LogoutPage(self.browser).visit()
+ self.add_course_role(self.course_id, 'Course Scorer', GA_COURSE_SCORER_USER_INFO['email'])
+ AutoAuthPage(
+ self.browser,
+ username=GA_COURSE_SCORER_USER_INFO['username'],
+ password=GA_COURSE_SCORER_USER_INFO['password'],
+ email=GA_COURSE_SCORER_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # visit courseware page and make sure there is entrance exam chapter.
+ self.courseware_page.visit()
+ self.courseware_page.wait_for_page()
+ self.assertFalse(element_has_text(
+ page=self.courseware_page,
+ css_selector=entrance_exam_link_selector,
+ text='Entrance Exam'
+ ))
+
+ # 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.wait_for_page()
+ self.assertTrue(self.settings_page.is_browser_on_page())
+ 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 course info page and make sure there is an "Entrance Exam" section.
+ self.courseware_page.visit()
+ self.courseware_page.wait_for_page()
+ self.assertTrue(element_has_text(
+ page=self.courseware_page,
+ css_selector=entrance_exam_link_selector,
+ text='Entrance Exam'
+ ))
+
+ # Logout and login as a ga_old_course_viewer
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(
+ self.browser,
+ username=GA_OLD_COURSE_VIEWER_USER_INFO['username'],
+ password=GA_OLD_COURSE_VIEWER_USER_INFO['password'],
+ email=GA_OLD_COURSE_VIEWER_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # self.switch_to_user(GA_OLD_COURSE_VIEWER_USER_INFO, self.course_id)
+ # visit courseware page and make sure there is not entrance exam chapter.
+
+ # visit course info page and make sure there is an "Entrance Exam" section.
+ self.courseware_page.visit()
+ self.courseware_page.wait_for_page()
+ self.assertTrue(element_has_text(
+ page=self.courseware_page,
+ css_selector=entrance_exam_link_selector,
+ text='Entrance Exam'
+ ))
+
+ # Logout and login as a ga_global_course_creator
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(
+ self.browser,
+ username=GA_GLOBAL_COURSE_CREATOR_USER_INFO['username'],
+ password=GA_GLOBAL_COURSE_CREATOR_USER_INFO['password'],
+ email=GA_GLOBAL_COURSE_CREATOR_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # visit courseware page and make sure there is entrance exam chapter.
+ self.courseware_page.visit()
+ self.courseware_page.wait_for_page()
+ self.assertTrue(element_has_text(
+ page=self.courseware_page,
+ css_selector=entrance_exam_link_selector,
+ text='Entrance Exam'
+ ))
+
+ # Logout and login as a ga_course_scorer
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(
+ self.browser,
+ username=GA_COURSE_SCORER_USER_INFO['username'],
+ password=GA_COURSE_SCORER_USER_INFO['password'],
+ email=GA_COURSE_SCORER_USER_INFO['email'],
+ course_id=self.course_id
+ ).visit()
+ # visit courseware page and make sure there is entrance exam chapter.
+ self.courseware_page.visit()
+ self.courseware_page.wait_for_page()
+ self.assertTrue(element_has_text(
+ page=self.courseware_page,
+ css_selector=entrance_exam_link_selector,
+ text='Entrance Exam'
+ ))
diff --git a/common/test/acceptance/tests/lms/test_ga_resign.py b/common/test/acceptance/tests/lms/test_ga_resign.py
index 0dfed09c9203..c14142e0fd86 100644
--- a/common/test/acceptance/tests/lms/test_ga_resign.py
+++ b/common/test/acceptance/tests/lms/test_ga_resign.py
@@ -72,11 +72,11 @@ def test_resign_success(self):
resign_complete_page = ResignCompletePage(self.browser).wait_for_page()
bok_choy.browser.save_screenshot(self.browser, 'test_resign_success__2')
- # Fail to login and redirect to disabled account page
+ # Fail to login
login_page = CombinedLoginAndRegisterPage(self.browser, start_page='login')
login_page.visit()
login_page.login(self.email, self.password)
- DisabledAccountPage(self.browser).wait_for_page()
+ login_page.wait_for_errors()
bok_choy.browser.save_screenshot(self.browser, 'test_resign_success__3')
def test_resign_with_empty_reason(self):
diff --git a/common/test/acceptance/tests/lms/test_ga_terminated.py b/common/test/acceptance/tests/lms/test_ga_terminated.py
index dbc8b4bceba4..3409c7bfb622 100644
--- a/common/test/acceptance/tests/lms/test_ga_terminated.py
+++ b/common/test/acceptance/tests/lms/test_ga_terminated.py
@@ -5,7 +5,7 @@
from bok_choy.web_app_test import WebAppTest
from ..helpers import UniqueCourseTest
-from ..ga_helpers import GaccoTestMixin, GA_OLD_COURSE_VIEWER_USER_INFO
+from ..ga_helpers import GaccoTestMixin, GA_GLOBAL_COURSE_CREATOR_USER_INFO, GA_OLD_COURSE_VIEWER_USER_INFO
from ...pages.lms.auto_auth import AutoAuthPage
from ...pages.common.logout import LogoutPage
from ...pages.lms.ga_dashboard import DashboardPage
@@ -69,6 +69,13 @@ def test_dashboard_message_and_link(self):
self.assertEqual(self.dashboard_page.hidden_course_text, "")
self.assertTrue(self.dashboard_page.is_hidden_course_link_active(self.course_id))
+ # Logout and login as a ga_global_course_creator.
+ self.switch_to_user(GA_GLOBAL_COURSE_CREATOR_USER_INFO, self.course_id)
+
+ self.dashboard_page.visit()
+ self.assertEqual(self.dashboard_page.hidden_course_text, "")
+ self.assertTrue(self.dashboard_page.is_hidden_course_link_active(self.course_id))
+
def test_dashboard_unenroll_when_course_has_terminated(self):
"""
Scenario:
@@ -140,3 +147,10 @@ def test_dashboard_message_and_link(self):
self.dashboard_page.visit()
self.assertEqual(self.dashboard_page.hidden_course_text, "")
self.assertTrue(self.dashboard_page.is_hidden_course_link_active(self.course_id))
+
+ # Logout and login as a ga_global_course_creator.
+ self.switch_to_user(GA_GLOBAL_COURSE_CREATOR_USER_INFO, self.course_id)
+
+ self.dashboard_page.visit()
+ self.assertEqual(self.dashboard_page.hidden_course_text, "")
+ self.assertTrue(self.dashboard_page.is_hidden_course_link_active(self.course_id))
diff --git a/common/test/acceptance/tests/lms/test_ga_user_preview.py b/common/test/acceptance/tests/lms/test_ga_user_preview.py
new file mode 100644
index 000000000000..8baa263ce3e5
--- /dev/null
+++ b/common/test/acceptance/tests/lms/test_ga_user_preview.py
@@ -0,0 +1,338 @@
+# -*- coding: utf-8 -*-
+"""
+Tests the "preview" selector in the LMS that allows changing between Staff, Student, and Content Groups.
+"""
+from nose.plugins.attrib import attr
+
+from ..ga_role_helpers import GaccoTestRoleMixin
+from ..helpers import UniqueCourseTest, create_user_partition_json
+from ...pages.studio.auto_auth import AutoAuthPage
+from ...pages.lms.courseware import CoursewarePage
+from ...pages.lms.staff_view import StaffPage
+from ...fixtures.course import CourseFixture, XBlockFixtureDesc
+from xmodule.partitions.partitions import Group
+from textwrap import dedent
+
+
+@attr('shard_3')
+class StaffViewTest(UniqueCourseTest):
+ """
+ Tests that verify the staff view.
+ """
+ USERNAME = "STAFF_TESTER"
+ EMAIL = "johndoe@example.com"
+
+ def _auto_auth(self):
+ AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
+ course_id=self.course_id, staff=True).visit()
+
+ def setUp(self):
+ super(StaffViewTest, self).setUp()
+
+ self.courseware_page = CoursewarePage(self.browser, self.course_id)
+
+ # Install a course with sections/problems, tabs, updates, and handouts
+ self.course_fixture = CourseFixture(
+ self.course_info['org'], self.course_info['number'],
+ self.course_info['run'], self.course_info['display_name']
+ )
+
+ self.populate_course_fixture(self.course_fixture) # pylint: disable=no-member
+
+ self.course_fixture.install()
+
+ # Auto-auth register for the course.
+ # Do this as global staff so that you will see the Staff View
+ self._auto_auth()
+
+ def _goto_staff_page(self):
+ """
+ Open staff page with assertion
+ """
+ self.courseware_page.visit()
+ staff_page = StaffPage(self.browser, self.course_id)
+ self.assertEqual(staff_page.staff_view_mode, 'Staff')
+ return staff_page
+
+
+@attr('shard_3')
+class CourseWithoutContentGroupsTest(StaffViewTest):
+ """
+ Setup for tests that have no content restricted to specific content groups.
+ """
+
+ def populate_course_fixture(self, course_fixture):
+ """
+ Populates test course with chapter, sequential, and 2 problems.
+ """
+ problem_data = dedent("""
+
+ Choose Yes.
+
+
+ Yes
+
+
+
+ """)
+
+ course_fixture.add_children(
+ XBlockFixtureDesc('chapter', 'Test Section').add_children(
+ XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
+ XBlockFixtureDesc('problem', 'Test Problem 1', data=problem_data),
+ XBlockFixtureDesc('problem', 'Test Problem 2', data=problem_data)
+ )
+ )
+ )
+
+
+@attr('shard_3')
+class StaffViewToggleTest(CourseWithoutContentGroupsTest, GaccoTestRoleMixin):
+ """
+ Tests for the staff view toggle button.
+ """
+ def test_instructor_tab_visibility_with_ga_global_course_creator(self):
+ """
+ Test that the instructor tab is always hidden by GaGlobalCourseCreator.
+ """
+ self.logout()
+ self.auto_auth_with_ga_global_course_creator(self.course_id)
+ self.courseware_page.visit()
+ self.assertFalse(self.courseware_page.has_tab('Instructor'))
+
+ def test_instructor_tab_visibility_with_ga_course_scorer(self):
+ """
+ Test that the instructor tab is hidden when viewing as a student.
+ """
+ self.logout()
+ self.auto_auth_with_ga_course_scorer(self.course_id)
+ course_page = self._goto_staff_page()
+ self.assertTrue(course_page.has_tab('Instructor'))
+ course_page.set_staff_view_mode('Student')
+ self.assertEqual(course_page.staff_view_mode, 'Student')
+ self.assertFalse(course_page.has_tab('Instructor'))
+
+
+@attr('shard_3')
+class StaffDebugTestWithGaCourseScorer(CourseWithoutContentGroupsTest, GaccoTestRoleMixin):
+ """
+ Tests that verify the staff debug info.
+ """
+ def _auto_auth(self):
+ self.user_info = self.auto_auth_with_ga_course_scorer(self.course_id)
+
+ def test_enabled_staff_debug(self):
+ """
+ Test that ga_course_scorer can view staff debug info
+ """
+ staff_page = self._goto_staff_page()
+
+ # 'Staff Debug Info' is capitalized.
+ # 'text-transform: uppercase' is set for .instructor-info-action
+ # in lms/static/sass/course/courseware/_courseware.scss
+ self.assertTrue(u'STAFF DEBUG INFO' in staff_page.q(css='a.instructor-info-action').text)
+
+ def test_reset_attempts_empty(self):
+ """
+ Test that we reset even when there is no student state
+ """
+
+ staff_debug_page = self._goto_staff_page().open_staff_debug_info()
+ staff_debug_page.reset_attempts()
+ msg = staff_debug_page.idash_msg[0]
+ self.assertEqual(u'Successfully reset the attempts '
+ 'for user {}'.format(self.user_info['username']), msg)
+
+ def test_reset_attempts_state(self):
+ """
+ Successfully reset the student attempts
+ """
+ staff_page = self._goto_staff_page()
+ staff_page.answer_problem()
+
+ staff_debug_page = staff_page.open_staff_debug_info()
+ staff_debug_page.reset_attempts()
+ msg = staff_debug_page.idash_msg[0]
+ self.assertEqual(u'Successfully reset the attempts '
+ 'for user {}'.format(self.user_info['username']), msg)
+
+ def test_student_by_email(self):
+ """
+ Successfully reset the student attempts using their email address
+ """
+ staff_page = self._goto_staff_page()
+ staff_page.answer_problem()
+
+ staff_debug_page = staff_page.open_staff_debug_info()
+ staff_debug_page.reset_attempts(self.user_info['email'])
+ msg = staff_debug_page.idash_msg[0]
+ self.assertEqual(u'Successfully reset the attempts '
+ 'for user {}'.format(self.user_info['email']), msg)
+
+ def test_reset_attempts_for_problem_loaded_via_ajax(self):
+ """
+ Successfully reset the student attempts for problem loaded via ajax.
+ """
+ staff_page = self._goto_staff_page()
+ staff_page.load_problem_via_ajax()
+ staff_page.answer_problem()
+
+ staff_debug_page = staff_page.open_staff_debug_info()
+ staff_debug_page.reset_attempts()
+ msg = staff_debug_page.idash_msg[0]
+ self.assertEqual(u'Successfully reset the attempts '
+ 'for user {}'.format(self.user_info['username']), msg)
+
+
+@attr('shard_3')
+class StaffDebugTestWithGaGlobalCourseCreator(CourseWithoutContentGroupsTest, GaccoTestRoleMixin):
+ """
+ Tests that verify the staff debug info.
+ """
+ def _auto_auth(self):
+ self.user_info = self.auto_auth_with_ga_global_course_creator(self.course_id)
+
+ def test_disabled_staff_debug(self):
+ """
+ Test that ga_global_course_creator cannot view staff debug info
+ """
+ courseware_page = self.courseware_page.visit()
+
+ self.assertFalse(courseware_page.q(css='a.instructor-info-action').is_present())
+
+
+@attr('shard_3')
+class StudentHistoryViewTestWithGaCourseScorer(CourseWithoutContentGroupsTest, GaccoTestRoleMixin):
+ """
+ Tests that verify the Student History View.
+ """
+ def _auto_auth(self):
+ self.user_info = self.auto_auth_with_ga_course_scorer(self.course_id)
+
+ def test_enabled_student_history_view(self):
+ """
+ Test that ga_course_scorer can view Student history
+ """
+ staff_page = self._goto_staff_page()
+
+ # 'Submission history' is capitalized.
+ # 'text-transform: uppercase' is set for .instructor-info-action
+ # in lms/static/sass/course/courseware/_courseware.scss
+ self.assertTrue(u'SUBMISSION HISTORY' in staff_page.q(css='a.instructor-info-action').text)
+
+
+@attr('shard_3')
+class StudentHistoryViewTestWithGaGlobalCourseCreator(CourseWithoutContentGroupsTest, GaccoTestRoleMixin):
+ """
+ Tests that verify the Student History View.
+ """
+ def _auto_auth(self):
+ self.user_info = self.auto_auth_with_ga_global_course_creator(self.course_id)
+
+ def test_disabled_student_history_view(self):
+ """
+ Test that ga_global_course_creator can view Student history
+ """
+ courseware_page = self.courseware_page.visit()
+ self.assertFalse(courseware_page.q(css='a.instructor-info-action').is_present())
+
+
+@attr('shard_3')
+class CourseWithContentGroupsTest(StaffViewTest, GaccoTestRoleMixin):
+ """
+ Verifies that changing the "View this course as" selector works properly for content groups.
+ """
+
+ def _auto_auth(self):
+ self.auto_auth_with_ga_global_course_creator(self.course_id)
+
+ def setUp(self):
+ super(CourseWithContentGroupsTest, self).setUp()
+ # pylint: disable=protected-access
+ self.course_fixture._update_xblock(self.course_fixture._course_location, {
+ "metadata": {
+ u"user_partitions": [
+ create_user_partition_json(
+ 0,
+ 'Configuration alpha,beta',
+ 'Content Group Partition',
+ [Group("0", 'alpha'), Group("1", 'beta')],
+ scheme="cohort"
+ )
+ ],
+ },
+ })
+
+ def populate_course_fixture(self, course_fixture):
+ """
+ Populates test course with chapter, sequential, and 3 problems.
+ One problem is visible to all, one problem is visible only to Group "alpha", and
+ one problem is visible only to Group "beta".
+ """
+ problem_data = dedent("""
+
+ Choose Yes.
+
+
+ Yes
+
+
+
+ """)
+
+ self.alpha_text = "VISIBLE TO ALPHA"
+ self.beta_text = "VISIBLE TO BETA"
+ self.everyone_text = "VISIBLE TO EVERYONE"
+
+ course_fixture.add_children(
+ XBlockFixtureDesc('chapter', 'Test Section').add_children(
+ XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
+ XBlockFixtureDesc('vertical', 'Test Unit').add_children(
+ XBlockFixtureDesc(
+ 'problem', self.alpha_text, data=problem_data, metadata={"group_access": {0: [0]}}
+ ),
+ XBlockFixtureDesc(
+ 'problem', self.beta_text, data=problem_data, metadata={"group_access": {0: [1]}}
+ ),
+ XBlockFixtureDesc('problem', self.everyone_text, data=problem_data)
+ )
+ )
+ )
+ )
+
+ def test_staff_sees_all_problems_with_ga_global_course_creator(self):
+ """
+ Scenario: GaGlobalCourseCreator see all problems
+ Given I have a course with a cohort user partition
+ And problems that are associated with specific groups in the user partition
+ When I view the courseware in the LMS with staff access
+ Then I see all the problems, regardless of their group_access property
+ """
+ self.logout()
+ self.auto_auth_with_ga_global_course_creator(self.course_id)
+ self.courseware_page.visit()
+ verify_expected_problem_visibility(self, self.courseware_page, [self.alpha_text, self.beta_text, self.everyone_text])
+
+ def test_staff_sees_all_problems_with_ga_course_scorer(self):
+ """
+ Scenario: GaCourseScorer see all problems
+ Given I have a course with a cohort user partition
+ And problems that are associated with specific groups in the user partition
+ When I view the courseware in the LMS with staff access
+ Then I see all the problems, regardless of their group_access property
+ """
+ self.logout()
+ self.auto_auth_with_ga_course_scorer(self.course_id)
+ self.courseware_page.visit()
+ verify_expected_problem_visibility(self, self.courseware_page, [self.alpha_text, self.beta_text, self.everyone_text])
+
+
+def verify_expected_problem_visibility(test, courseware_page, expected_problems):
+ """
+ Helper method that checks that the expected problems are visible on the current page.
+ """
+ test.assertEqual(
+ len(expected_problems), courseware_page.num_xblock_components, "Incorrect number of visible problems"
+ )
+ for index, expected_problem in enumerate(expected_problems):
+ test.assertIn(expected_problem, courseware_page.xblock_components[index].text)
diff --git a/common/test/acceptance/tests/studio/test_ga_studio_outline.py b/common/test/acceptance/tests/studio/test_ga_studio_outline.py
new file mode 100644
index 000000000000..28dec5b065da
--- /dev/null
+++ b/common/test/acceptance/tests/studio/test_ga_studio_outline.py
@@ -0,0 +1,133 @@
+"""
+Acceptance tests for studio related to the outline page.
+"""
+import bok_choy.browser
+import ddt
+
+from base_studio_test import StudioCourseTest
+from ..ga_helpers import GaccoTestMixin, SUPER_USER_INFO
+from ...fixtures.course import CourseFixture, XBlockFixtureDesc
+from ...pages.lms.ga_django_admin import DjangoAdminPage
+from ...pages.studio.ga_overview import CourseOutlinePage
+
+
+@ddt.ddt
+class SettingProgressRestrictionTest(StudioCourseTest, GaccoTestMixin):
+
+ def setUp(self):
+ super(SettingProgressRestrictionTest, self).setUp()
+
+ self.course_info = {
+ 'org': 'test_org_00003',
+ 'number': self._testMethodName,
+ 'run': 'test_run_00003',
+ 'display_name': 'Progress Restriction Course'
+ }
+
+ self.course_fixture = CourseFixture(
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run'],
+ self.course_info['display_name']
+ )
+ self.course_fixture.add_children(
+ XBlockFixtureDesc('chapter', '1').add_children(
+ XBlockFixtureDesc('sequential', '1.1').add_children(
+ XBlockFixtureDesc('vertical', '1.1.1'),
+ XBlockFixtureDesc('vertical', '1.1.2')
+ ),
+ XBlockFixtureDesc('sequential', '1.2').add_children(
+ XBlockFixtureDesc('vertical', '1.2.1'),
+ XBlockFixtureDesc('vertical', '1.2.2')
+ )
+ ),
+ XBlockFixtureDesc('chapter', '2').add_children(
+ XBlockFixtureDesc('sequential', '2.1').add_children(
+ XBlockFixtureDesc('vertical', '2.1.1'),
+ )
+ )
+ )
+ self.course_fixture.install()
+
+ self.user = self.course_fixture.user
+ self.log_in(self.user, True)
+
+ self.course_outline_page = CourseOutlinePage(
+ self.browser,
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run']
+ )
+
+ def _set_progress_restriction(self):
+ self.switch_to_user(SUPER_USER_INFO)
+
+ DjangoAdminPage(self.browser).visit().click_add('ga_optional', 'courseoptionalconfiguration').input({
+ 'enabled': True,
+ 'key': 'progress-restriction-settings',
+ 'course_key': self.course_id,
+ }).save()
+
+ @ddt.data(
+ (0, 0, 0),
+ (0, 0, 1),
+ (0, 1, 0),
+ (0, 1, 1),
+ (1, 0, 0),
+ )
+ @ddt.unpack
+ def test_show_settings_form_for_passing_mark(self,
+ section_at,
+ subsection_at,
+ unit_at):
+ """
+ test for course with setting "progress-restriction-settings"
+ """
+ self._set_progress_restriction()
+ self.course_outline_page.visit()
+ self.course_outline_page.expand_all_subsections()
+ unit = self.course_outline_page.section_at(section_at).subsection_at(subsection_at).unit_at(unit_at)
+ modal = unit.edit()
+ bok_choy.browser.save_screenshot(self.browser, 'show_settings_form_for_passing_mark')
+ self.assertTrue('Passing Mark' in modal.find_css('.modal-section-title').text)
+
+ @ddt.data(
+ (0, 0, 0),
+ (0, 0, 1),
+ (0, 1, 0),
+ (0, 1, 1),
+ (1, 0, 0),
+ )
+ @ddt.unpack
+ def test_does_not_show_settings_form_for_passing_mark(self,
+ section_at,
+ subsection_at,
+ unit_at):
+ """
+ test for course without setting "progress-restriction-settings"
+ """
+ self.course_outline_page.visit()
+ self.course_outline_page.expand_all_subsections()
+ unit = self.course_outline_page.section_at(section_at).subsection_at(subsection_at).unit_at(unit_at)
+ modal = unit.edit()
+ self.assertFalse('Passing Mark' in modal.find_css('.modal-section-title').text)
+
+ @ddt.data(
+ '-1',
+ '101',
+ 'abcde',
+ )
+ def test_validate_input_for_passing_mark(self, input_value):
+ """
+ test invalid input for passing mark"
+ """
+ self._set_progress_restriction()
+ self.course_outline_page.visit()
+ self.course_outline_page.expand_all_subsections()
+ unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0)
+ modal = unit.edit()
+ self.assertEqual(u'', modal.find_css('#progress-restriction-passing-mark-error-message').text[0])
+ modal.find_css('#progress-restriction-passing-mark').fill(input_value)
+ modal.save()
+ self.assertEqual(u'Please enter an integer between 0 and 100.',
+ modal.find_css('#progress-restriction-passing-mark-error-message').text[0])
diff --git a/common/test/db_fixtures/ga_role_user.json b/common/test/db_fixtures/ga_role_user.json
index cb20d68946d6..62143eb28d0d 100644
--- a/common/test/db_fixtures/ga_role_user.json
+++ b/common/test/db_fixtures/ga_role_user.json
@@ -37,5 +37,75 @@
"role": "ga_old_course_viewer",
"user": 9092
}
+ },
+ {
+ "pk": 9093,
+ "model": "auth.user",
+ "fields": {
+ "date_joined": "2017-01-01 00:00:00",
+ "username": "Ga_Global_Course_Creator",
+ "first_name": "Ga_Global_Course_Creator",
+ "last_name": "test",
+ "email": "ga_global_course_creator@example.com",
+ "password": "Ga_Global_Course_Creator1",
+ "is_active": true
+ }
+ },
+ {
+ "pk": 9093,
+ "model": "student.userprofile",
+ "fields": {
+ "user": 9093,
+ "name": "ga course editor test",
+ "courseware": "course.xml",
+ "allow_certificate": true
+ }
+ },
+ {
+ "pk": 9093,
+ "model": "student.registration",
+ "fields": {
+ "user": 9093,
+ "activation_key": "TuC53PTAh6Kb3wEyaRi3HFQcKieB9093"
+ }
+ },
+ {
+ "pk": 9093,
+ "model": "student.courseaccessrole",
+ "fields": {
+ "role": "ga_global_course_creator",
+ "user": 9093
+ }
+ },
+ {
+ "pk": 9094,
+ "model": "auth.user",
+ "fields": {
+ "date_joined": "2017-01-01 00:00:00",
+ "username": "Ga_Course_Scorer",
+ "first_name": "Ga_Course_Scorer",
+ "last_name": "test",
+ "email": "ga_course_scorer@example.com",
+ "password": "Ga_Course_Scorer1",
+ "is_active": true
+ }
+ },
+ {
+ "pk": 9094,
+ "model": "student.userprofile",
+ "fields": {
+ "user": 9094,
+ "name": "ga lms course staff test",
+ "courseware": "course.xml",
+ "allow_certificate": true
+ }
+ },
+ {
+ "pk": 9094,
+ "model": "student.registration",
+ "fields": {
+ "user": 9094,
+ "activation_key": "TuC53PTAh6Kb3wEyaRi3HFQcKieB9094"
+ }
}
]
diff --git a/conf/locale/ja_JP/LC_MESSAGES/django.po b/conf/locale/ja_JP/LC_MESSAGES/django.po
index f92e7d0d0a80..37806c80cb17 100644
--- a/conf/locale/ja_JP/LC_MESSAGES/django.po
+++ b/conf/locale/ja_JP/LC_MESSAGES/django.po
@@ -9791,7 +9791,7 @@ msgstr "プロファイル画像を削除"
#: lms/templates/manage_user_standing.html
msgid "Students whose accounts have been disabled"
-msgstr "失効となっているアカウント"
+msgstr "失効となっているアカウントです"
#: lms/templates/manage_user_standing.html
msgid "(reload your page to refresh)"
diff --git a/conf/locale/ja_JP/LC_MESSAGES/mako.po b/conf/locale/ja_JP/LC_MESSAGES/mako.po
index 5acbb6c4f3f7..f54bf7760cf0 100644
--- a/conf/locale/ja_JP/LC_MESSAGES/mako.po
+++ b/conf/locale/ja_JP/LC_MESSAGES/mako.po
@@ -1083,7 +1083,7 @@ msgstr "プロファイル画像を削除"
#: lms/templates/manage_user_standing.html
msgid "Students whose accounts have been disabled"
-msgstr "失効となっているアカウント"
+msgstr "失効となっているアカウントです"
#: lms/templates/manage_user_standing.html
msgid "(reload your page to refresh)"
diff --git a/ga/conf/locale/ja_JP/LC_MESSAGES/biz.po b/ga/conf/locale/ja_JP/LC_MESSAGES/biz.po
index 0abbe2bda526..5fdf75716e70 100644
--- a/ga/conf/locale/ja_JP/LC_MESSAGES/biz.po
+++ b/ga/conf/locale/ja_JP/LC_MESSAGES/biz.po
@@ -319,9 +319,12 @@ msgstr "{0}には重複した値を入力できません。"
##### Course Operation
#: biz/templates/ga_course_operation/survey.html
-msgid "The survey result can be downloaded as a CSV file. When you try to open a CSV file as it is in Excel, because that would garbled in the case that contains the Japanese, once you save the file, please re- open it in a text editor that corresponds to the 'UTF-8'. In addition, data on the date and time in the CSV file all UTC we (the world standard time, from Japan Standard Time minus 9 hours) are listed in the."
-msgstr "アンケート結果がCSVファイルでダウンロードできます。
"
-"CSVファイルをそのままExcelで開こうとすると、日本語が含まれている場合に文字化けしてしまいますので、一旦ファイルを保存した後、「UTF-8」に対応したテキストエディタで開き直してください。また、CSVファイル内の日時に関するデータは全てUTC(世界標準時、日本標準時からマイナス9時間)で記載されています。"
+msgid ""
+"The survey result can be downloaded as a CSV file.
"
+"When you try to open a CSV file as it is in Excel, because that would garbled in the case that contains the Japanese, once you save the file, please re-open it in a text editor that corresponds to the 'UTF-8'."
+msgstr ""
+"アンケート結果がCSVファイルでダウンロードできます。
"
+"CSVファイルをそのままExcelで開こうとすると、日本語が含まれている場合に文字化けしてしまいますので、一旦ファイルを保存した後、「UTF-8」に対応したテキストエディタで開き直してください。"
##### Course Selection
#: biz/djangoapps/util/decorators.py
diff --git a/ga/conf/locale/ja_JP/LC_MESSAGES/gacco.po b/ga/conf/locale/ja_JP/LC_MESSAGES/gacco.po
index 4830dba1b96d..f86914929573 100644
--- a/ga/conf/locale/ja_JP/LC_MESSAGES/gacco.po
+++ b/ga/conf/locale/ja_JP/LC_MESSAGES/gacco.po
@@ -1102,12 +1102,16 @@ msgid "Display Order"
msgstr "表示順"
#: lms/djangoapps/ga_advanced_course/models.py
-msgid "Start Time"
-msgstr "開始時間"
+msgid "Start Date (JST)"
+msgstr "開始日 (JST)"
#: lms/djangoapps/ga_advanced_course/models.py
-msgid "End Time"
-msgstr "終了時間"
+msgid "Start Time (JST)"
+msgstr "開始時間 (JST)"
+
+#: lms/djangoapps/ga_advanced_course/models.py
+msgid "End Time (JST)"
+msgstr "終了時間 (JST)"
#: lms/djangoapps/ga_advanced_course/models.py
msgid "Capacity"
@@ -1193,6 +1197,10 @@ msgstr "ユーザーのオプション機能設定"
msgid "Hide the e-mail on the Account Settings"
msgstr "アカウント設定画面のメールアドレス欄非表示"
+#: openedx/core/djangoapps/ga_optional/models.py
+msgid "Progress Restriction by Correct Answer Rate"
+msgstr "問題正答率による進行制限"
+
#: cms/templates/settings.html
msgid "Custom Logo for Settings"
msgstr "カスタムロゴ設定"
@@ -1643,6 +1651,14 @@ msgstr "%Y年%b%d日 %H:%M"
msgid "Account is not valid anymore"
msgstr "このアカウントは既に退会しています"
+#: lms/templates/disabled_account.html
+msgid "The user is resigned."
+msgstr "指定されたユーザーは退会済みです。"
+
+#: lms/templates/disabled_account.html
+msgid "Resigned User"
+msgstr "退会済みユーザー"
+
#: lms/templates/disabled_account.html
msgid "The log-in email address is already withdrawn from gacco."
msgstr "ログイン時に利用されたメールアドレスは既に退会されています。"
@@ -1756,3 +1772,19 @@ msgstr ""
#: lms/static/coffee/src/instructor_dashboard/data_download.js
msgid "Error generating playback status report. Please try again."
msgstr "動画視聴状況レポート作成に失敗しました。再試行してください。"
+
+#: lms/templates/instructor/instructor_dashboard_2/membership.html
+msgid "Course Scorer"
+msgstr "講座スタッフ(採点用権限)"
+
+#: lms/templates/instructor/instructor_dashboard_2/membership.html
+msgid ""
+"Course scorer members help you manage your course. Course scorer can browse "
+"to the students' reports, and view the assignment submission history. Course scorer does not "
+"have access to Studio."
+msgstr ""
+"講座スタッフ(採点用権限)は講座の一部の管理業務を支援することができます。受講者のレポートの閲覧や採点、課題提出履歴を見ることができます。Studioへのアクセス権はありません。"
+
+#: lms/templates/instructor/instructor_dashboard_2/membership.html
+msgid "Add Course Scorer"
+msgstr "講座スタッフ(採点用権限)を追加する"
diff --git a/ga/conf/locale/ja_JP/LC_MESSAGES/gaccojs.po b/ga/conf/locale/ja_JP/LC_MESSAGES/gaccojs.po
index 666eb2012234..936c951c12c4 100644
--- a/ga/conf/locale/ja_JP/LC_MESSAGES/gaccojs.po
+++ b/ga/conf/locale/ja_JP/LC_MESSAGES/gaccojs.po
@@ -184,6 +184,10 @@ msgstr "選びなおす"
msgid "Is this your final answer?"
msgstr "解答を提出します。よろしいですか?"
+#: common/lib/xmodule/xmodule/js/src/sequence/display.coffee
+msgid "You need to make correct answers for the some problems in the before quiz."
+msgstr "前の課題に合格するまでアクセスできません。"
+
#: lms/static/js/progress_report.js
msgid "Course"
msgstr "コース"
@@ -283,7 +287,7 @@ msgstr "このサイトを利用するための言語です。現在、このサ
#: lms/static/js/student_account/views/account_settings_fields.js
msgid "An email has been sent to {email_address}. Follow the link in the email to resign."
-msgstr "まだ退会手続きは完了していません。「{email_address}」宛に確認メールが送信されます。メール文中にある退会手続き用のリンクをクリックをすることで、退会処理をすることができます。
なお、退会したメールアドレスの再利用や再登録はできませんのでご注意ください。"
+msgstr "まだ退会手続きは完了していません。「{email_address}」宛に確認メールが送信されます。メール文中にある退会手続き用のリンクをクリックをすることで、退会処理をすることができます。"
#: lms/templates/student_account/register.underscore
msgid "
Asterisk (*) marked fields are required."
@@ -545,6 +549,14 @@ msgstr "時:"
msgid "Minutes:"
msgstr "分:"
+#: cms/templates/js/progress-restriction-editor.underscore
+msgid "Passing Mark"
+msgstr "合格基準"
+
+#: cms/templates/js/progress-restriction-editor.underscore
+msgid "Pass Mark Needed for Progress:"
+msgstr "合格に必要な正答率:"
+
#: cms/static/js/views/modals/course_outline_modals.js
msgid "Please enter the date on and after 1900."
msgstr "1900年以降の日付を入力して下さい。"
diff --git a/lms/djangoapps/bulk_email/tasks.py b/lms/djangoapps/bulk_email/tasks.py
index b7d9656cd4b7..abbdbaf615a1 100644
--- a/lms/djangoapps/bulk_email/tasks.py
+++ b/lms/djangoapps/bulk_email/tasks.py
@@ -43,7 +43,7 @@
from courseware.courses import get_course
from openedx.core.lib.courses import course_image_url
from student.models import UserStanding
-from student.roles import CourseStaffRole, CourseInstructorRole
+from student.roles import CourseStaffRole, CourseInstructorRole, GaCourseScorerRole
from instructor_task.models import InstructorTask
from instructor_task.subtasks import (
SubtaskStatus,
@@ -120,7 +120,9 @@ def _get_recipient_querysets(user_id, to_option, course_id):
else:
staff_qset = CourseStaffRole(course_id).users_with_role()
instructor_qset = CourseInstructorRole(course_id).users_with_role()
- staff_instructor_qset = (staff_qset | instructor_qset).distinct()
+ # Note: Include GaCourseScorer in mail sending (#2150)
+ ga_course_scorer_qset = GaCourseScorerRole(course_id).users_with_role()
+ staff_instructor_qset = (staff_qset | instructor_qset | ga_course_scorer_qset).distinct()
if to_option == SEND_TO_STAFF:
return [use_read_replica_if_available(staff_instructor_qset)]
@@ -165,7 +167,9 @@ def _get_advanced_course_recipient_querysets(course_id, advanced_course_id):
# members here
staff_qset = CourseStaffRole(course_id).users_with_role()
instructor_qset = CourseInstructorRole(course_id).users_with_role()
- staff_instructor_qset = (staff_qset | instructor_qset).distinct()
+ # Note: Include GaCourseScorer in mail sending (#2150)
+ ga_course_scorer_qset = GaCourseScorerRole(course_id).users_with_role()
+ staff_instructor_qset = (staff_qset | instructor_qset | ga_course_scorer_qset).distinct()
unpurchased_staff_qset = staff_instructor_qset.exclude(
id__in=purchased_user_ids,
diff --git a/lms/djangoapps/bulk_email/tests/test_tasks.py b/lms/djangoapps/bulk_email/tests/test_tasks.py
index 5e729ee90317..84b55b1821fe 100644
--- a/lms/djangoapps/bulk_email/tests/test_tasks.py
+++ b/lms/djangoapps/bulk_email/tests/test_tasks.py
@@ -33,7 +33,11 @@
from xmodule.modulestore.tests.factories import CourseFactory
from bulk_email.models import CourseEmail, Optout, SEND_TO_ALL, SEND_TO_ALL_INCLUDE_OPTOUT, SEND_TO_ADVANCED_COURSE
-
+from courseware.tests.factories import (
+ GaCourseScorerFactory,
+ InstructorFactory,
+ StaffFactory,
+)
from instructor_task.tasks import send_bulk_course_email
from instructor_task.subtasks import update_subtask_status, SubtaskStatus
from instructor_task.models import InstructorTask
@@ -229,6 +233,18 @@ def test_successful(self):
get_conn.return_value.send_messages.side_effect = cycle([None])
self._test_run_with_task(send_bulk_course_email, 'emailed', num_emails, num_emails)
+ def test_successful_includes_course_staff(self):
+ # Select number of emails to fit into a single subtask.
+ num_emails = settings.BULK_EMAIL_EMAILS_PER_TASK
+ # Add course staff
+ StaffFactory(course_key=self.course.id)
+ GaCourseScorerFactory(course_key=self.course.id)
+ # We also send email to the instructor:
+ self._create_students(num_emails - 3)
+ with patch('bulk_email.tasks.get_connection', autospec=True) as get_conn:
+ get_conn.return_value.send_messages.side_effect = cycle([None])
+ self._test_run_with_task(send_bulk_course_email, 'emailed', num_emails, num_emails)
+
def test_successful_twice(self):
# Select number of emails to fit into a single subtask.
num_emails = settings.BULK_EMAIL_EMAILS_PER_TASK
diff --git a/lms/djangoapps/ccx/models.py b/lms/djangoapps/ccx/models.py
index 2baf72ee886c..5167fbd2bc53 100644
--- a/lms/djangoapps/ccx/models.py
+++ b/lms/djangoapps/ccx/models.py
@@ -6,9 +6,10 @@
from django.contrib.auth.models import User
from django.db import models
-from django.utils.timezone import UTC
+from pytz import utc
from lazy import lazy
+from openedx.core.lib.time_zone_utils import get_time_zone_abbr
from xmodule_django.models import CourseKeyField, LocationKeyField
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
@@ -68,43 +69,43 @@ def max_student_enrollments_allowed(self):
def has_started(self):
"""Return True if the CCX start date is in the past"""
- return datetime.now(UTC()) > self.start
+ return datetime.now(utc) > self.start
def has_ended(self):
"""Return True if the CCX due date is set and is in the past"""
if self.due is None:
return False
- return datetime.now(UTC()) > self.due
+ return datetime.now(utc) > self.due
- def start_datetime_text(self, format_string="SHORT_DATE"):
+ def start_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""Returns the desired text representation of the CCX start datetime
- The returned value is always expressed in UTC
+ The returned value is in specified time zone, defaulted to UTC.
"""
i18n = self.course.runtime.service(self.course, "i18n")
strftime = i18n.strftime
- value = strftime(self.start, format_string)
+ value = strftime(self.start.astimezone(time_zone), format_string)
if format_string == 'DATE_TIME':
- value += u' UTC'
+ value += ' ' + get_time_zone_abbr(time_zone, self.start)
return value
- def end_datetime_text(self, format_string="SHORT_DATE"):
+ def end_datetime_text(self, format_string="SHORT_DATE", time_zone=utc):
"""Returns the desired text representation of the CCX due datetime
If the due date for the CCX is not set, the value returned is the empty
string.
- The returned value is always expressed in UTC
+ The returned value is in specified time zone, defaulted to UTC.
"""
if self.due is None:
return ''
i18n = self.course.runtime.service(self.course, "i18n")
strftime = i18n.strftime
- value = strftime(self.due, format_string)
+ value = strftime(self.due.astimezone(time_zone), format_string)
if format_string == 'DATE_TIME':
- value += u' UTC'
+ value += ' ' + get_time_zone_abbr(time_zone, self.due)
return value
diff --git a/lms/djangoapps/ccx/tests/test_models.py b/lms/djangoapps/ccx/tests/test_models.py
index 2a1c9750928d..b94ecd8303a9 100644
--- a/lms/djangoapps/ccx/tests/test_models.py
+++ b/lms/djangoapps/ccx/tests/test_models.py
@@ -1,10 +1,11 @@
"""
tests for the models
"""
+import ddt
from datetime import datetime, timedelta
-from django.utils.timezone import UTC
from mock import patch
from nose.plugins.attrib import attr
+from pytz import timezone, utc
from student.roles import CourseCcxCoachRole
from student.tests.factories import (
AdminFactory,
@@ -22,6 +23,7 @@
from ..overrides import override_field_for_ccx
+@ddt.ddt
@attr('shard_1')
class TestCCX(ModuleStoreTestCase):
"""Unit tests for the CustomCourseForEdX model
@@ -64,7 +66,7 @@ def test_ccx_start_is_correct(self):
For this reason we test the difference between and make sure it is less
than one second.
"""
- expected = datetime.now(UTC())
+ expected = datetime.now(utc)
self.set_ccx_override('start', expected)
actual = self.ccx.start # pylint: disable=no-member
diff = expected - actual
@@ -72,7 +74,7 @@ def test_ccx_start_is_correct(self):
def test_ccx_start_caching(self):
"""verify that caching the start property works to limit queries"""
- now = datetime.now(UTC())
+ now = datetime.now(utc)
self.set_ccx_override('start', now)
with check_mongo_calls(1):
# these statements are used entirely to demonstrate the
@@ -89,7 +91,7 @@ def test_ccx_due_without_override(self):
def test_ccx_due_is_correct(self):
"""verify that the due datetime for a ccx is correctly retrieved"""
- expected = datetime.now(UTC())
+ expected = datetime.now(utc)
self.set_ccx_override('due', expected)
actual = self.ccx.due # pylint: disable=no-member
diff = expected - actual
@@ -97,7 +99,7 @@ def test_ccx_due_is_correct(self):
def test_ccx_due_caching(self):
"""verify that caching the due property works to limit queries"""
- expected = datetime.now(UTC())
+ expected = datetime.now(utc)
self.set_ccx_override('due', expected)
with check_mongo_calls(1):
# these statements are used entirely to demonstrate the
@@ -109,7 +111,7 @@ def test_ccx_due_caching(self):
def test_ccx_has_started(self):
"""verify that a ccx marked as starting yesterday has started"""
- now = datetime.now(UTC())
+ now = datetime.now(utc)
delta = timedelta(1)
then = now - delta
self.set_ccx_override('start', then)
@@ -117,7 +119,7 @@ def test_ccx_has_started(self):
def test_ccx_has_not_started(self):
"""verify that a ccx marked as starting tomorrow has not started"""
- now = datetime.now(UTC())
+ now = datetime.now(utc)
delta = timedelta(1)
then = now + delta
self.set_ccx_override('start', then)
@@ -125,7 +127,7 @@ def test_ccx_has_not_started(self):
def test_ccx_has_ended(self):
"""verify that a ccx that has a due date in the past has ended"""
- now = datetime.now(UTC())
+ now = datetime.now(utc)
delta = timedelta(1)
then = now - delta
self.set_ccx_override('due', then)
@@ -134,7 +136,7 @@ def test_ccx_has_ended(self):
def test_ccx_has_not_ended(self):
"""verify that a ccx that has a due date in the future has not eneded
"""
- now = datetime.now(UTC())
+ now = datetime.now(utc)
delta = timedelta(1)
then = now + delta
self.set_ccx_override('due', then)
@@ -151,7 +153,7 @@ def test_ccx_without_due_date_has_not_ended(self):
}))
def test_start_datetime_short_date(self):
"""verify that the start date for a ccx formats properly by default"""
- start = datetime(2015, 1, 1, 12, 0, 0, tzinfo=UTC())
+ start = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015"
self.set_ccx_override('start', start)
actual = self.ccx.start_datetime_text() # pylint: disable=no-member
@@ -162,18 +164,34 @@ def test_start_datetime_short_date(self):
}))
def test_start_datetime_date_time_format(self):
"""verify that the DATE_TIME format also works as expected"""
- start = datetime(2015, 1, 1, 12, 0, 0, tzinfo=UTC())
+ start = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015 at 12:00 UTC"
self.set_ccx_override('start', start)
actual = self.ccx.start_datetime_text('DATE_TIME') # pylint: disable=no-member
self.assertEqual(expected, actual)
+ @ddt.data((datetime(2015, 11, 1, 8, 59, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:59 PDT"),
+ (datetime(2015, 11, 1, 9, 00, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:00 PST"))
+ @ddt.unpack
+ def test_start_date_time_zone(self, start_date_time, expected_short_date, expected_date_time):
+ """
+ verify that start date is correctly converted when time zone specified
+ during normal daylight hours and daylight savings hours
+ """
+ time_zone = timezone('America/Los_Angeles')
+
+ self.set_ccx_override('start', start_date_time)
+ actual_short_date = self.ccx.start_datetime_text(time_zone=time_zone) # pylint: disable=no-member
+ actual_datetime = self.ccx.start_datetime_text('DATE_TIME', time_zone) # pylint: disable=no-member
+ self.assertEqual(expected_short_date, actual_short_date)
+ self.assertEqual(expected_date_time, actual_datetime)
+
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"SHORT_DATE_FORMAT": "%b %d, %Y",
}))
def test_end_datetime_short_date(self):
"""verify that the end date for a ccx formats properly by default"""
- end = datetime(2015, 1, 1, 12, 0, 0, tzinfo=UTC())
+ end = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015"
self.set_ccx_override('due', end)
actual = self.ccx.end_datetime_text() # pylint: disable=no-member
@@ -184,12 +202,28 @@ def test_end_datetime_short_date(self):
}))
def test_end_datetime_date_time_format(self):
"""verify that the DATE_TIME format also works as expected"""
- end = datetime(2015, 1, 1, 12, 0, 0, tzinfo=UTC())
+ end = datetime(2015, 1, 1, 12, 0, 0, tzinfo=utc)
expected = "Jan 01, 2015 at 12:00 UTC"
self.set_ccx_override('due', end)
actual = self.ccx.end_datetime_text('DATE_TIME') # pylint: disable=no-member
self.assertEqual(expected, actual)
+ @ddt.data((datetime(2015, 11, 1, 8, 59, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:59 PDT"),
+ (datetime(2015, 11, 1, 9, 00, 00, tzinfo=utc), "Nov 01, 2015", "Nov 01, 2015 at 01:00 PST"))
+ @ddt.unpack
+ def test_end_datetime_time_zone(self, end_date_time, expected_short_date, expected_date_time):
+ """
+ verify that end date is correctly converted when time zone specified
+ during normal daylight hours and daylight savings hours
+ """
+ time_zone = timezone('America/Los_Angeles')
+
+ self.set_ccx_override('due', end_date_time)
+ actual_short_date = self.ccx.end_datetime_text(time_zone=time_zone) # pylint: disable=no-member
+ actual_datetime = self.ccx.end_datetime_text('DATE_TIME', time_zone) # pylint: disable=no-member
+ self.assertEqual(expected_short_date, actual_short_date)
+ self.assertEqual(expected_date_time, actual_datetime)
+
@patch('util.date_utils.ugettext', fake_ugettext(translations={
"DATE_TIME_FORMAT": "%b %d, %Y at %H:%M",
}))
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py
index 2b8af9fbc0ab..1ff27d89bfab 100644
--- a/lms/djangoapps/courseware/access.py
+++ b/lms/djangoapps/courseware/access.py
@@ -42,6 +42,8 @@
CourseInstructorRole,
CourseStaffRole,
GaAnalyzerRole,
+ GaCourseScorerRole,
+ GaGlobalCourseCreatorRole,
GaOldCourseViewerStaffRole,
GlobalStaff,
SupportStaffRole,
@@ -70,6 +72,7 @@
log = logging.getLogger(__name__)
GA_ACCESS_CHECK_TYPE_ANALYZER = 'ga_analyzer'
GA_ACCESS_CHECK_TYPE_OLD_COURSE_VIEW = 'ga_old_course_view'
+GA_ACCESS_CHECK_TYPE_GLOBAL_COURSE_CREATOR = 'ga_global_course_creator'
def has_access(user, action, obj, course_key=None):
@@ -311,6 +314,9 @@ def _can_enroll_courselike(user, courselike):
is_old_course_viewer = _has_access_string(user, GA_ACCESS_CHECK_TYPE_OLD_COURSE_VIEW, 'global')
if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course_key):
return ACCESS_GRANTED
+ # Note: GaGlobalCourseCreator can enroll courses regardless of the course start or terminate (#2150)
+ if _has_access_string(user, GA_ACCESS_CHECK_TYPE_GLOBAL_COURSE_CREATOR, 'global'):
+ return ACCESS_GRANTED
if _has_staff_access_to_descriptor(user, courselike, course_key):
return ACCESS_GRANTED
@@ -365,7 +371,8 @@ def can_load():
)
return (
- ACCESS_GRANTED if (response or _has_staff_access_to_descriptor(user, courselike, courselike.id))
+ ACCESS_GRANTED if (response or _has_staff_access_to_descriptor(user, courselike, courselike.id)
+ or _has_access_string(user, GA_ACCESS_CHECK_TYPE_GLOBAL_COURSE_CREATOR, 'global'))
else response
)
@@ -391,6 +398,7 @@ def can_see_in_catalog():
return (
_has_catalog_visibility(courselike, CATALOG_VISIBILITY_CATALOG_AND_ABOUT)
or _has_staff_access_to_descriptor(user, courselike, courselike.id)
+ or _has_access_string(user, GA_ACCESS_CHECK_TYPE_GLOBAL_COURSE_CREATOR, 'global')
)
def can_see_about_page():
@@ -403,6 +411,7 @@ def can_see_about_page():
_has_catalog_visibility(courselike, CATALOG_VISIBILITY_CATALOG_AND_ABOUT)
or _has_catalog_visibility(courselike, CATALOG_VISIBILITY_ABOUT)
or _has_staff_access_to_descriptor(user, courselike, courselike.id)
+ or _has_access_string(user, GA_ACCESS_CHECK_TYPE_GLOBAL_COURSE_CREATOR, 'global')
)
checkers = {
@@ -544,7 +553,8 @@ def can_load():
)
return (
- ACCESS_GRANTED if (response or _has_staff_access_to_descriptor(user, descriptor, course_key))
+ ACCESS_GRANTED if (response or _has_staff_access_to_descriptor(user, descriptor, course_key)
+ or _has_access_string(user, GA_ACCESS_CHECK_TYPE_GLOBAL_COURSE_CREATOR, 'global'))
else response
)
@@ -663,12 +673,22 @@ def check_old_course_view():
else ACCESS_DENIED
)
+ def check_ga_global_course_creator():
+ """Checks for ga_global_course_creator access. """
+ if perm != 'global':
+ return ACCESS_DENIED
+ return (
+ ACCESS_GRANTED if GaGlobalCourseCreatorRole().has_user(user)
+ else ACCESS_DENIED
+ )
+
checkers = {
'staff': check_staff,
'support': check_support,
'certificates': check_support,
GA_ACCESS_CHECK_TYPE_ANALYZER: check_ga_analyzer,
GA_ACCESS_CHECK_TYPE_OLD_COURSE_VIEW: check_old_course_view,
+ GA_ACCESS_CHECK_TYPE_GLOBAL_COURSE_CREATOR: check_ga_global_course_creator,
}
return _dispatch(checkers, action, user, perm)
@@ -750,14 +770,20 @@ def _has_access_to_course(user, access_level, course_key):
debug("Allow: user.is_staff")
return ACCESS_GRANTED
+ # Note: GaGlobalCourseCreator is not a staff at LMS (#2150)
+ if _has_access_string(user, GA_ACCESS_CHECK_TYPE_GLOBAL_COURSE_CREATOR, 'global'):
+ return ACCESS_DENIED
+
if access_level not in ('staff', 'instructor'):
log.debug("Error in access._has_access_to_course access_level=%s unknown", access_level)
debug("Deny: unknown access level")
return ACCESS_DENIED
+ # Note: GaCourseScorer is a staff at LMS (#2150)
staff_access = (
CourseStaffRole(course_key).has_user(user) or
- OrgStaffRole(course_key.org).has_user(user)
+ OrgStaffRole(course_key.org).has_user(user) or
+ GaCourseScorerRole(course_key).has_user(user)
)
if staff_access and access_level == 'staff':
debug("Allow: user has course staff access")
diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py
index f3265a72e786..fbecbcbd7ccb 100644
--- a/lms/djangoapps/courseware/date_summary.py
+++ b/lms/djangoapps/courseware/date_summary.py
@@ -8,13 +8,15 @@
from babel.dates import format_timedelta
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
+from django.utils.translation import ugettext_lazy
from django.utils.translation import to_locale, get_language
from edxmako.shortcuts import render_to_string
from lazy import lazy
-import pytz
+from pytz import utc
from course_modes.models import CourseMode
from lms.djangoapps.verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification
+from openedx.core.lib.time_zone_utils import get_time_zone_abbr, get_user_time_zone
from student.models import CourseEnrollment
@@ -50,7 +52,7 @@ def date_format(self):
The format to display this date in. By default, displays like Jan
01, 2015.
"""
- return '%b %d, %Y'
+ return u'%b %d, %Y'
@property
def link(self):
@@ -62,34 +64,20 @@ def link_text(self):
"""The text of the link."""
return ''
+ @property
+ def time_zone(self):
+ """The time zone to display in"""
+ return get_user_time_zone(self.user)
+
def __init__(self, course, user):
self.course = course
self.user = user
def get_context(self):
"""Return the template context used to render this summary block."""
- date = ''
- if self.date is not None:
- # Translators: relative_date is a fuzzy description of the
- # time from now until absolute_date. For example,
- # absolute_date might be "Jan 01, 2020", and if today were
- # December 5th, 2020, relative_date would be "1 month".
- locale = to_locale(get_language())
- try:
- relative_date = format_timedelta(self.date - datetime.now(pytz.UTC), locale=locale)
- # Babel doesn't have translations for Esperanto, so we get
- # a KeyError when testing translations with
- # ?preview-lang=eo. This should not happen with any other
- # languages. See https://github.com/python-babel/babel/issues/107
- except KeyError:
- relative_date = format_timedelta(self.date - datetime.now(pytz.UTC))
- date = _("in {relative_date} - {absolute_date}").format(
- relative_date=relative_date,
- absolute_date=self.date.strftime(self.date_format),
- )
return {
'title': self.title,
- 'date': date,
+ 'date': self._format_date(),
'description': self.description,
'css_class': self.css_class,
'link': self.link,
@@ -102,6 +90,35 @@ def render(self):
"""
return render_to_string('courseware/date_summary.html', self.get_context())
+ def _format_date(self):
+ """
+ Return this block's date in a human-readable format. If the date
+ is None, returns the empty string.
+ """
+ if self.date is None:
+ return ''
+ locale = to_locale(get_language())
+ delta = self.date - datetime.now(utc)
+ try:
+ relative_date = format_timedelta(delta, locale=locale)
+ # Babel doesn't have translations for Esperanto, so we get
+ # a KeyError when testing translations with
+ # ?preview-lang=eo. This should not happen with any other
+ # languages. See https://github.com/python-babel/babel/issues/107
+ except KeyError:
+ relative_date = format_timedelta(delta)
+ date_has_passed = delta.days < 0
+ # Translators: 'absolute' is a date such as "Jan 01,
+ # 2020". 'relative' is a fuzzy description of the time until
+ # 'absolute'. For example, 'absolute' might be "Jan 01, 2020",
+ # and if today were December 5th, 2020, 'relative' would be "1
+ # month".
+ date_format = _(u"{relative} ago - {absolute}") if date_has_passed else _(u"in {relative} - {absolute}")
+ return date_format.format(
+ relative=relative_date,
+ absolute=self.date.astimezone(self.time_zone).strftime(self.date_format.encode('utf-8')).decode('utf-8'),
+ )
+
@property
def is_enabled(self):
"""
@@ -111,11 +128,11 @@ def is_enabled(self):
future.
"""
if self.date is not None:
- return datetime.now(pytz.UTC) <= self.date
+ return datetime.now(utc) <= self.date
return False
def __repr__(self):
- return 'DateSummary: "{title}" {date} is_enabled={is_enabled}'.format(
+ return u'DateSummary: "{title}" {date} is_enabled={is_enabled}'.format(
title=self.title,
date=self.date,
is_enabled=self.is_enabled
@@ -128,7 +145,10 @@ class TodaysDate(DateSummary):
"""
css_class = 'todays-date'
is_enabled = True
- date_format = u'%b %d, %Y (%H:%M {utc})'.format(utc=_('UTC'))
+
+ @property
+ def date_format(self):
+ return u'%b %d, %Y (%H:%M {tz_abbr})'.format(tz_abbr=get_time_zone_abbr(self.time_zone))
# The date is shown in the title, no need to display it again.
def get_context(self):
@@ -138,11 +158,13 @@ def get_context(self):
@property
def date(self):
- return datetime.now(pytz.UTC)
+ return datetime.now(utc)
@property
def title(self):
- return _('Today is {date}').format(date=datetime.now(pytz.UTC).strftime(self.date_format))
+ return _(u'Today is {date}').format(
+ date=self.date.astimezone(self.time_zone).strftime(self.date_format.encode('utf-8')).decode('utf-8')
+ )
class CourseStartDate(DateSummary):
@@ -150,7 +172,7 @@ class CourseStartDate(DateSummary):
Displays the start date of the course.
"""
css_class = 'start-date'
- title = _('Course Starts')
+ title = ugettext_lazy('Course Starts')
@property
def date(self):
@@ -162,7 +184,7 @@ class CourseEndDate(DateSummary):
Displays the end date of the course.
"""
css_class = 'end-date'
- title = _('Course End')
+ title = ugettext_lazy('Course End')
@property
def is_enabled(self):
@@ -170,8 +192,12 @@ def is_enabled(self):
@property
def description(self):
- if datetime.now(pytz.UTC) <= self.date:
- return _('To earn a certificate, you must complete all requirements before this date.')
+ if datetime.now(utc) <= self.date:
+ mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
+ if is_active and CourseMode.is_eligible_for_certificate(mode):
+ return _('To earn a certificate, you must complete all requirements before this date.')
+ else:
+ return _('After this date, course content will be archived.')
return _('This course is archived, which means you can review course content but it is no longer active.')
@property
@@ -185,9 +211,12 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
Verified track.
"""
css_class = 'verified-upgrade-deadline'
- title = _('Verification Upgrade Deadline')
- description = _('You are still eligible to upgrade to a Verified Certificate!')
- link_text = _('Upgrade to Verified Certificate')
+ title = ugettext_lazy('Verification Upgrade Deadline')
+ description = ugettext_lazy(
+ 'You are still eligible to upgrade to a Verified Certificate! '
+ 'Pursue it to highlight the knowledge and skills you gain in this course.'
+ )
+ link_text = ugettext_lazy('Upgrade to Verified Certificate')
@property
def link(self):
@@ -281,7 +310,7 @@ def deadline_has_passed(self):
Return True if a verification deadline exists, and has already passed.
"""
deadline = self.date
- return deadline is not None and deadline <= datetime.now(pytz.UTC)
+ return deadline is not None and deadline <= datetime.now(utc)
def must_retry(self):
"""Return True if the user must re-submit verification, False otherwise."""
diff --git a/lms/djangoapps/courseware/ga_access.py b/lms/djangoapps/courseware/ga_access.py
index a6e9e92447d2..d13a86efb671 100644
--- a/lms/djangoapps/courseware/ga_access.py
+++ b/lms/djangoapps/courseware/ga_access.py
@@ -1,6 +1,6 @@
from django.conf import settings
-from courseware.access import has_access, GA_ACCESS_CHECK_TYPE_OLD_COURSE_VIEW
+from courseware.access import has_access, GA_ACCESS_CHECK_TYPE_GLOBAL_COURSE_CREATOR, GA_ACCESS_CHECK_TYPE_OLD_COURSE_VIEW
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from student.models import CourseEnrollment
from student.roles import CourseBetaTesterRole
@@ -20,16 +20,18 @@ def is_terminated(courselike, user):
is_global_staff = has_access(user, 'staff', 'global')
is_old_course_viewer = has_access(user, GA_ACCESS_CHECK_TYPE_OLD_COURSE_VIEW, 'global')
+ is_ga_global_course_creator = has_access(user, GA_ACCESS_CHECK_TYPE_GLOBAL_COURSE_CREATOR, 'global')
is_course_staff = has_access(user, 'staff', courselike)
is_course_beta_tester = CourseBetaTesterRole(courselike.id).has_user(user)
- # Note1: Even if course terminate date has passed, GlobalStaff and OldCourseViewer can access it. (#2197)
+ # Note1: Even if course terminate date has passed, GlobalStaff, OldCourseViewer and GaGlobalCourseCreator can access it. (#2197, #2150)
# Note2: Even if self-paced course and its individual end date has passed,
- # GlobalStaff, OldCourseViewer, CourseAdmin(Instructor), CourseStaff and BetaTester can access it. (#2197)
+ # GlobalStaff, OldCourseViewer, GaGlobalCourseCreator, CourseAdmin(Instructor), CourseStaff and BetaTester can access it. (#2197, #2150)
return (
- not (is_global_staff or is_old_course_viewer) and _is_terminated(courselike)
+ not (is_global_staff or is_old_course_viewer or is_ga_global_course_creator) and _is_terminated(courselike)
) or (
- not (is_global_staff or is_old_course_viewer or is_course_staff or is_course_beta_tester) and _is_individual_closed(courselike, user)
+ not (is_global_staff or is_old_course_viewer or is_ga_global_course_creator or is_course_staff or is_course_beta_tester)
+ and _is_individual_closed(courselike, user)
)
diff --git a/lms/djangoapps/courseware/ga_progress_restriction.py b/lms/djangoapps/courseware/ga_progress_restriction.py
new file mode 100644
index 000000000000..b84efab872e0
--- /dev/null
+++ b/lms/djangoapps/courseware/ga_progress_restriction.py
@@ -0,0 +1,138 @@
+"""
+Progress Restriction
+"""
+import json
+from courseware.models import StudentModuleHistory
+
+PROGRESS_RESTRICTION_TYPE = {
+ 'no-restriction': 'No Restriction',
+ 'correct-answer-rate': 'Correct Answer Rate',
+}
+UNCONDITIONAL_PASSING = 100
+
+
+class ProgressRestriction(object):
+ def __init__(self, course_key, user, course_module):
+ self.dict_section_name_by_vertical_name = {}
+
+ self.list_restricted_chapter_name = []
+ self.list_restricted_section_name = []
+
+ self.list_restricted_chapter_index = []
+ self.dict_restricted_sections_by_chapter_index = {}
+ self.dict_restricted_verticals_by_section_name = {}
+
+ if course_module:
+ student_module_histories = StudentModuleHistory.objects.filter(
+ student_module__course_id=course_key,
+ student_module__student_id=user.id,
+ )
+
+ passed_restricted_vertical = False
+ for chapter_idx, chapter in enumerate(course_module.get_display_items()):
+ if chapter.visible_to_staff_only:
+ # visible_to_staff_only is not used as condition
+ # of progress restriction
+ continue
+
+ if passed_restricted_vertical:
+ self.list_restricted_chapter_name.append(unicode(chapter.location.name))
+ self.list_restricted_chapter_index.append(chapter_idx)
+
+ restricted_sections = []
+
+ for section_idx, section in enumerate(chapter.get_display_items()):
+ if section.visible_to_staff_only:
+ # visible_to_staff_only is not used as condition
+ # of progress restriction
+ continue
+
+ if passed_restricted_vertical:
+ self.list_restricted_section_name.append(unicode(section.location.name))
+ restricted_sections.append(section_idx + 1)
+
+ restricted_verticals = []
+
+ for vertical_idx, vertical in enumerate(section.get_display_items()):
+ if vertical.visible_to_staff_only:
+ # visible_to_staff_only is not used as condition
+ # of progress restriction
+ continue
+
+ if passed_restricted_vertical:
+ restricted_verticals.append(vertical_idx)
+
+ is_restricted_vertical = passed_restricted_vertical
+ if not is_restricted_vertical and vertical.progress_restriction['type'] == PROGRESS_RESTRICTION_TYPE['correct-answer-rate']:
+ problems = [
+ {
+ 'location': component.location,
+ 'count': component.get_score()['total'],
+ 'whole_point_addition': component.whole_point_addition
+ } for component in vertical.get_children() if hasattr(component, 'category') and component.category == u'problem'
+ ]
+ problem_count = sum([p['count'] for p in problems])
+
+ if problem_count:
+ problems = filter(lambda p: not p['whole_point_addition'], problems)
+ correct_count = problem_count - sum([p['count'] for p in problems])
+ has_answered_correctly = {}
+ for sm in student_module_histories.filter(student_module__module_state_key__in=[p['location'] for p in problems]):
+ st = json.loads(sm.state)
+ for k in st.get('correct_map', []):
+ if st['correct_map'][k]['correctness'] == 'correct':
+ has_answered_correctly[k] = True
+ correct_count += len(filter(lambda v: v, has_answered_correctly.values()))
+
+ is_restricted_vertical = (100 * correct_count / problem_count) < vertical.progress_restriction.get('passing_mark', 0)
+
+ self.dict_section_name_by_vertical_name[unicode(vertical.location.name)] = unicode(section.location.name)
+
+ if is_restricted_vertical and not passed_restricted_vertical:
+ passed_restricted_vertical = True
+
+ if restricted_verticals:
+ self.dict_restricted_verticals_by_section_name[unicode(section.location.name)] = restricted_verticals
+
+ self.dict_restricted_sections_by_chapter_index[chapter_idx] = restricted_sections
+
+ def get_restricted_list_in_section(self, section):
+ """
+ Returns a list of sequence numbers of restricted vertical-blocks in specific section
+ """
+ return self.dict_restricted_verticals_by_section_name.get(unicode(section), [])
+
+ def get_restricted_list_in_same_section(self, vertical_name):
+ """
+ Returns a list of sequence numbers of restricted vertical-blocks
+ in section including specific vertical-block
+ """
+ return self.get_restricted_list_in_section(self.dict_section_name_by_vertical_name[vertical_name]) if vertical_name in self.dict_section_name_by_vertical_name else []
+
+ def get_restricted_chapters(self):
+ """
+ Returns a list of restricted chapters' sequence numbers, to use to gray out toc
+ """
+ return self.list_restricted_chapter_index
+
+ def get_restricted_sections(self):
+ """
+ Returns a dict of chapters' numbers as keys and
+ list of restricted sections' sequence numbers as values
+ to use to gray out toc
+ """
+ return self.dict_restricted_sections_by_chapter_index
+
+ def is_restricted_chapter(self, chapter):
+ """
+ Returns a boolean if chapter is restricted
+ when the first vertical-block in the chapter is restricted the chapter is restricted
+ """
+ return chapter in self.list_restricted_chapter_name
+
+ def is_restricted_section(self, section):
+ """
+ Returns a boolean if section is restricted
+ when the first vertical-block in the section is restricted the section is restricted
+ """
+ return section in self.list_restricted_section_name
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 873a4ec41543..857abc63d02d 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -28,6 +28,7 @@
from capa.xqueue_interface import XQueueInterface
from courseware.access import has_access, get_user_role
+from courseware.ga_progress_restriction import ProgressRestriction
from courseware.masquerade import (
MasqueradingKeyValueStore,
filter_displayed_blocks,
@@ -49,6 +50,8 @@
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
+from openedx.core.djangoapps.ga_optional.api import is_available
+from openedx.core.djangoapps.ga_optional.models import PROGRESS_RESTRICTION_OPTION_KEY
from openedx.core.lib.xblock_utils import (
replace_course_urls,
replace_jump_to_id_urls,
@@ -58,7 +61,7 @@
request_token as xblock_request_token,
)
from student.models import anonymous_id_for_user, user_by_anonymous_id
-from student.roles import CourseBetaTesterRole
+from student.roles import CourseBetaTesterRole, GaCourseScorerRole, GaGlobalCourseCreatorRole
from xblock.core import XBlock
from xblock.django.request import django_to_webob_request, webob_to_django_response
from xblock_django.user_service import DjangoXBlockUserService
@@ -648,7 +651,11 @@ def rebind_noauth_module_to_user(module, real_user):
staff_access = has_access(user, 'staff', descriptor, course_id)
instructor_access = bool(has_access(user, 'instructor', descriptor, course_id))
if staff_access:
- block_wrappers.append(partial(add_staff_markup, user, instructor_access, disable_staff_debug_info))
+ # Note: GaGlobalCourseCreator has not access to staff tools (#2150)
+ if not GaGlobalCourseCreatorRole().has_user(user):
+ # Note: GaCourseScorer has access to staff tools, but cannot see the studio link (#2150)
+ block_wrappers.append(partial(add_staff_markup, user, instructor_access, disable_staff_debug_info,
+ enable_studio_link=not GaCourseScorerRole(course_id).has_user(user)))
# These modules store data using the anonymous_student_id as a key.
# To prevent loss of data, we will continue to provide old modules with
@@ -1017,6 +1024,27 @@ 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)
+
+ # add list of subsections(sequence numbers) restricted by progress restriction
+ # to unblock next subsection when passed
+ staff_access = has_access(request.user, 'staff', course.id)
+ if (suffix == 'problem_check' and
+ is_available(PROGRESS_RESTRICTION_OPTION_KEY, course.id) and
+ not staff_access):
+ field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
+ course_key, request.user, course, depth=2)
+ course_module = get_module_for_descriptor(
+ request.user, request, course, field_data_cache, course.id, course=course
+ )
+ progress_restriction = ProgressRestriction(course.id, request.user, course_module)
+
+ restricted_list = progress_restriction.get_restricted_list_in_same_section(instance.parent.name)
+ restricted_chapters = progress_restriction.get_restricted_chapters()
+ restricted_sections = progress_restriction.get_restricted_sections()
+ resp = append_data_to_webob_response(resp, {'restricted_list': restricted_list,
+ 'restricted_chapters': restricted_chapters,
+ 'restricted_sections': restricted_sections})
+
if suffix == 'problem_check' \
and course \
and getattr(course, 'entrance_exam_enabled', False) \
diff --git a/lms/djangoapps/courseware/tests/factories.py b/lms/djangoapps/courseware/tests/factories.py
index 0f968380cd5e..e887af57b961 100644
--- a/lms/djangoapps/courseware/tests/factories.py
+++ b/lms/djangoapps/courseware/tests/factories.py
@@ -15,6 +15,8 @@
CourseInstructorRole,
CourseStaffRole,
CourseBetaTesterRole,
+ GaCourseScorerRole,
+ GaGlobalCourseCreatorRole,
GaOldCourseViewerStaffRole,
GlobalStaff,
OrgStaffRole,
@@ -132,6 +134,30 @@ def set_staff(self, create, extracted, **kwargs):
GaOldCourseViewerStaffRole().add_users(self)
+class GaGlobalCourseCreatorFactory(UserFactory):
+ """
+ Returns a User object with GaGlobalCourseCreator members access
+ """
+ last_name = "GaGlobalCourseCreator"
+
+ @factory.post_generation
+ def set_staff(self, create, extracted, **kwargs):
+ GaGlobalCourseCreatorRole().add_users(self)
+
+
+class GaCourseScorerFactory(UserFactory):
+ """
+ Returns a User object with GaCourseScorer members access
+ """
+ last_name = "GaCourseScorer"
+
+ @factory.post_generation
+ def course_key(self, create, extracted, **kwargs):
+ if extracted is None:
+ raise ValueError("Must specify a CourseKey for a ga_course_scorer user")
+ GaCourseScorerRole(extracted).add_users(self)
+
+
class StudentModuleFactory(DjangoModelFactory):
class Meta(object):
model = StudentModule
diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py
index a23a630187e7..ebc15253445b 100644
--- a/lms/djangoapps/courseware/tests/test_access.py
+++ b/lms/djangoapps/courseware/tests/test_access.py
@@ -22,6 +22,8 @@
from courseware.tests.factories import (
BetaTesterFactory,
GlobalStaffFactory,
+ GaCourseScorerFactory,
+ GaGlobalCourseCreatorFactory,
GaOldCourseViewerStaffFactory,
InstructorFactory,
StaffFactory,
@@ -79,6 +81,8 @@ def setUp(self):
self.course_instructor = InstructorFactory(course_key=self.course.id)
self.staff = GlobalStaffFactory()
self.ga_old_course_viewer = GaOldCourseViewerStaffFactory()
+ self.ga_global_course_creator = GaGlobalCourseCreatorFactory()
+ self.ga_course_scorer = GaCourseScorerFactory(course_key=self.course.id)
def verify_access(self, mock_unit, student_should_have_access, expected_error_type=None):
""" Verify the expected result from _has_access_descriptor """
@@ -93,6 +97,12 @@ def verify_access(self, mock_unit, student_should_have_access, expected_error_ty
self.assertTrue(
access._has_access_descriptor(self.course_staff, 'load', mock_unit, course_key=self.course.id)
)
+ self.assertTrue(
+ access._has_access_descriptor(self.ga_global_course_creator, 'load', mock_unit, course_key=self.course.id)
+ )
+ self.assertTrue(
+ access._has_access_descriptor(self.ga_course_scorer, 'load', mock_unit, course_key=self.course.id)
+ )
def test_has_staff_access_to_preview_mode(self):
"""
@@ -129,13 +139,17 @@ def test_has_staff_access_to_preview_mode(self):
self.assertTrue(
bool(access.has_staff_access_to_preview_mode(self.global_staff, obj=self.course, course_key=course_key))
)
+ self.assertTrue(
+ bool(access.has_staff_access_to_preview_mode(self.ga_course_scorer, obj=self.course, course_key=course_key))
+ )
- for user in [self.global_staff, self.course_staff, self.course_instructor]:
+ for user in [self.global_staff, self.course_staff, self.course_instructor, self.ga_course_scorer]:
for obj in modules:
self.assertTrue(bool(access.has_staff_access_to_preview_mode(user, obj=obj)))
self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.student, obj=obj)))
for obj in modules:
self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.ga_old_course_viewer, obj=obj)))
+ self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.ga_global_course_creator, obj=obj)))
def test_student_has_access(self):
"""
@@ -172,6 +186,7 @@ def test_string_has_staff_access_to_preview_mode(self):
self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.course_instructor, obj='global')))
self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.student, obj='global')))
self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.ga_old_course_viewer, obj='global')))
+ self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.ga_global_course_creator, obj='global')))
@patch('courseware.access.in_preview_mode', Mock(return_value=True))
def test_has_access_with_preview_mode(self):
@@ -187,6 +202,10 @@ def test_has_access_with_preview_mode(self):
self.assertFalse(bool(access.has_access(self.student, 'load', self.course, course_key=self.course.id)))
self.assertFalse(bool(access.has_access(self.ga_old_course_viewer, 'staff', self.course, course_key=self.course.id)))
self.assertFalse(bool(access.has_access(self.ga_old_course_viewer, 'load', self.course, course_key=self.course.id)))
+ self.assertFalse(bool(access.has_access(self.ga_global_course_creator, 'staff', self.course, course_key=self.course.id)))
+ self.assertFalse(bool(access.has_access(self.ga_global_course_creator, 'load', self.course, course_key=self.course.id)))
+ self.assertTrue(bool(access.has_access(self.ga_course_scorer, 'staff', self.course, course_key=self.course.id)))
+ self.assertTrue(bool(access.has_access(self.ga_course_scorer, 'load', self.course, course_key=self.course.id)))
# User should be able to preview when masquerade.
with patch('courseware.access.is_masquerading_as_student') as mock_masquerade:
@@ -200,6 +219,12 @@ def test_has_access_with_preview_mode(self):
self.assertFalse(
bool(access.has_access(self.ga_old_course_viewer, 'staff', self.course, course_key=self.course.id))
)
+ self.assertFalse(
+ bool(access.has_access(self.ga_global_course_creator, 'staff', self.course, course_key=self.course.id))
+ )
+ self.assertTrue(
+ bool(access.has_access(self.ga_course_scorer, 'staff', self.course, course_key=self.course.id))
+ )
def test_has_access_to_course(self):
self.assertFalse(access._has_access_to_course(
@@ -250,6 +275,18 @@ def test_has_access_to_course(self):
self.assertFalse(access._has_access_to_course(
self.ga_old_course_viewer, 'instructor', self.course.id
))
+ self.assertFalse(access._has_access_to_course(
+ self.ga_global_course_creator, 'staff', self.course.id
+ ))
+ self.assertFalse(access._has_access_to_course(
+ self.ga_global_course_creator, 'instructor', self.course.id
+ ))
+ self.assertTrue(access._has_access_to_course(
+ self.ga_course_scorer, 'staff', self.course.id
+ ))
+ self.assertFalse(access._has_access_to_course(
+ self.ga_course_scorer, 'instructor', self.course.id
+ ))
self.assertFalse(access._has_access_to_course(
self.student, 'not_staff_or_instructor', self.course.id
@@ -257,6 +294,9 @@ def test_has_access_to_course(self):
self.assertFalse(access._has_access_to_course(
self.ga_old_course_viewer, 'not_staff_or_instructor', self.course.id
))
+ self.assertFalse(access._has_access_to_course(
+ self.ga_global_course_creator, 'not_staff_or_instructor', self.course.id
+ ))
def test__has_access_string(self):
user = Mock(is_staff=True)
@@ -268,17 +308,20 @@ def test__has_access_string(self):
self.assertRaises(ValueError, access._has_access_string, user, 'not_staff', 'global')
@ddt.data(
- ('staff', False, True, False),
- ('support', False, True, False),
- ('certificates', False, True, False),
- ('ga_old_course_view', False, True, True)
+ ('staff', False, True, False, False),
+ ('support', False, True, False, False),
+ ('certificates', False, True, False, False),
+ ('ga_old_course_view', False, True, True, False),
+ ('ga_global_course_creator', False, False, False, True),
)
@ddt.unpack
- def test__has_access_string_some_roles(self, action, expected_student, expected_staff, expected_ga_old_course_viewer):
+ def test__has_access_string_some_roles(self, action, expected_student, expected_staff, expected_ga_old_course_viewer, expected_ga_global_course_creator):
+ idx = 0
for (user, expected_response) in (
(self.student, expected_student),
(self.staff, expected_staff),
- (self.ga_old_course_viewer, expected_ga_old_course_viewer)
+ (self.ga_old_course_viewer, expected_ga_old_course_viewer),
+ (self.ga_global_course_creator, expected_ga_global_course_creator)
):
self.assertEquals(
bool(access._has_access_string(user, action, 'global')),
@@ -400,6 +443,14 @@ def test__has_access_course_can_enroll(self):
user = StaffFactory.create(course_key=course.id)
self.assertTrue(access._has_access_course(user, 'enroll', course))
+ # GaGlobalCourseCreator can always enroll even outside the open enrollment period
+ user = GaGlobalCourseCreatorFactory.create()
+ self.assertTrue(access._has_access_course(user, 'enroll', course))
+
+ # GaCourseScorer can always enroll even outside the open enrollment period
+ user = GaCourseScorerFactory.create(course_key=course.id)
+ self.assertTrue(access._has_access_course(user, 'enroll', course))
+
# Non-staff cannot enroll if it is between the start and end dates and invitation only
# and not specifically allowed
course = Mock(
@@ -437,6 +488,8 @@ def test__catalog_visibility(self):
user = UserFactory.create()
course_id = SlashSeparatedCourseKey('edX', 'test', '2012_Fall')
staff = StaffFactory.create(course_key=course_id)
+ ga_global_course_creator = GaGlobalCourseCreatorFactory.create()
+ ga_course_scorer = GaCourseScorerFactory.create(course_key=course_id)
course = Mock(
id=course_id,
@@ -446,6 +499,10 @@ def test__catalog_visibility(self):
self.assertTrue(access._has_access_course(user, 'see_about_page', course))
self.assertTrue(access._has_access_course(staff, 'see_in_catalog', course))
self.assertTrue(access._has_access_course(staff, 'see_about_page', course))
+ self.assertTrue(access._has_access_course(ga_global_course_creator, 'see_in_catalog', course))
+ self.assertTrue(access._has_access_course(ga_global_course_creator, 'see_about_page', course))
+ self.assertTrue(access._has_access_course(ga_course_scorer, 'see_in_catalog', course))
+ self.assertTrue(access._has_access_course(ga_course_scorer, 'see_about_page', course))
# Now set visibility to just about page
course = Mock(
@@ -456,6 +513,10 @@ def test__catalog_visibility(self):
self.assertTrue(access._has_access_course(user, 'see_about_page', course))
self.assertTrue(access._has_access_course(staff, 'see_in_catalog', course))
self.assertTrue(access._has_access_course(staff, 'see_about_page', course))
+ self.assertTrue(access._has_access_course(ga_global_course_creator, 'see_in_catalog', course))
+ self.assertTrue(access._has_access_course(ga_global_course_creator, 'see_about_page', course))
+ self.assertTrue(access._has_access_course(ga_course_scorer, 'see_in_catalog', course))
+ self.assertTrue(access._has_access_course(ga_course_scorer, 'see_about_page', course))
# Now set visibility to none, which means neither in catalog nor about pages
course = Mock(
@@ -466,6 +527,10 @@ def test__catalog_visibility(self):
self.assertFalse(access._has_access_course(user, 'see_about_page', course))
self.assertTrue(access._has_access_course(staff, 'see_in_catalog', course))
self.assertTrue(access._has_access_course(staff, 'see_about_page', course))
+ self.assertTrue(access._has_access_course(ga_global_course_creator, 'see_in_catalog', course))
+ self.assertTrue(access._has_access_course(ga_global_course_creator, 'see_about_page', course))
+ self.assertTrue(access._has_access_course(ga_course_scorer, 'see_in_catalog', course))
+ self.assertTrue(access._has_access_course(ga_course_scorer, 'see_about_page', course))
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_access_on_course_with_pre_requisites(self):
@@ -649,15 +714,17 @@ def setUp(self):
fulfill_course_milestone(self.user_completed_pre_requisite, self.course_started.id)
self.user_staff = UserFactory.create(is_staff=True)
self.user_anonymous = AnonymousUserFactory.create()
+ self.ga_global_course_creator = GaGlobalCourseCreatorFactory.create()
+ self.ga_course_scorer = GaCourseScorerFactory.create(course_key=self.course_not_started.id)
ENROLL_TEST_DATA = list(itertools.product(
- ['user_normal', 'user_staff', 'user_anonymous'],
+ ['user_normal', 'user_staff', 'user_anonymous', 'ga_global_course_creator', 'ga_course_scorer'],
['enroll'],
['course_default', 'course_started', 'course_not_started', 'course_staff_only'],
))
LOAD_TEST_DATA = list(itertools.product(
- ['user_normal', 'user_beta_tester', 'user_staff'],
+ ['user_normal', 'user_beta_tester', 'user_staff', 'ga_global_course_creator', 'ga_course_scorer'],
['load'],
['course_default', 'course_started', 'course_not_started', 'course_staff_only'],
))
@@ -669,7 +736,7 @@ def setUp(self):
))
PREREQUISITES_TEST_DATA = list(itertools.product(
- ['user_normal', 'user_completed_pre_requisite', 'user_staff', 'user_anonymous'],
+ ['user_normal', 'user_completed_pre_requisite', 'user_staff', 'user_anonymous', 'ga_global_course_creator', 'ga_course_scorer'],
['view_courseware_with_prerequisites'],
['course_default', 'course_with_pre_requisite', 'course_with_pre_requisites'],
))
@@ -693,6 +760,8 @@ def test_course_overview_access(self, user_attr_name, action, course_attr_name):
"""
user = getattr(self, user_attr_name)
course = getattr(self, course_attr_name)
+ if user_attr_name == 'ga_course_scorer':
+ user = GaCourseScorerFactory.create(course_key=course.id)
course_overview = CourseOverview.get_from_id(course.id)
self.assertEqual(
diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py
index 965c295460b7..a7e10c7f44d8 100644
--- a/lms/djangoapps/courseware/tests/test_date_summary.py
+++ b/lms/djangoapps/courseware/tests/test_date_summary.py
@@ -4,9 +4,9 @@
import ddt
from django.core.urlresolvers import reverse
-import freezegun
+from freezegun import freeze_time
from nose.plugins.attrib import attr
-import pytz
+from pytz import utc
from course_modes.tests.factories import CourseModeFactory
from course_modes.models import CourseMode
@@ -20,6 +20,7 @@
VerifiedUpgradeDeadlineDate,
)
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
+from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
@@ -46,7 +47,7 @@ def setup_course_and_user(
verification_status=None,
):
"""Set up the course and user for this test."""
- now = datetime.now(pytz.UTC)
+ now = datetime.now(utc)
self.course = CourseFactory.create( # pylint: disable=attribute-defined-outside-init
start=now + timedelta(days=days_till_start)
)
@@ -140,21 +141,49 @@ def test_date_summary(self):
## TodaysDate
- @freezegun.freeze_time('2015-01-02')
- def test_todays_date(self):
+ def _today_date_helper(self, expected_display_date):
+ """
+ Helper function to test that today's date block renders correctly
+ and displays the correct time, accounting for daylight savings
+ """
self.setup_course_and_user()
+ set_user_preference(self.user, "time_zone", "America/Los_Angeles")
block = TodaysDate(self.course, self.user)
self.assertTrue(block.is_enabled)
- self.assertEqual(block.date, datetime.now(pytz.UTC))
- self.assertEqual(block.title, 'Today is Jan 02, 2015 (00:00 UTC)')
+ self.assertEqual(block.date, datetime.now(utc))
+ self.assertEqual(block.title, 'Today is {date}'.format(date=expected_display_date))
self.assertNotIn('date-summary-date', block.render())
- @freezegun.freeze_time('2015-01-02')
+ @freeze_time('2015-11-01 08:59:00')
+ def test_todays_date_time_zone_daylight(self):
+ """
+ Test today's date block displays correctly during
+ daylight savings hours
+ """
+ self._today_date_helper('Nov 01, 2015 (01:59 PDT)')
+
+ @freeze_time('2015-11-01 09:00:00')
+ def test_todays_date_time_zone_normal(self):
+ """
+ Test today's date block displays correctly during
+ normal daylight hours
+ """
+ self._today_date_helper('Nov 01, 2015 (01:00 PST)')
+
+ @freeze_time('2015-01-02')
def test_todays_date_render(self):
self.setup_course_and_user()
block = TodaysDate(self.course, self.user)
self.assertIn('Jan 02, 2015', block.render())
+ @freeze_time('2015-01-02')
+ def test_todays_date_render_time_zone(self):
+ self.setup_course_and_user()
+ set_user_preference(self.user, "time_zone", "America/Los_Angeles")
+ block = TodaysDate(self.course, self.user)
+ # Today is 'Jan 01, 2015' because of time zone offset
+ self.assertIn('Jan 01, 2015', block.render())
+
## CourseStartDate
def test_course_start_date(self):
@@ -162,15 +191,23 @@ def test_course_start_date(self):
block = CourseStartDate(self.course, self.user)
self.assertEqual(block.date, self.course.start)
- @freezegun.freeze_time('2015-01-02')
+ @freeze_time('2015-01-02')
def test_start_date_render(self):
self.setup_course_and_user()
block = CourseStartDate(self.course, self.user)
self.assertIn('in 1 day - Jan 03, 2015', block.render())
+ @freeze_time('2015-01-02')
+ def test_start_date_render_time_zone(self):
+ self.setup_course_and_user()
+ set_user_preference(self.user, "time_zone", "America/Los_Angeles")
+ block = CourseStartDate(self.course, self.user)
+ # Jan 02 is in 1 day because of time zone offset
+ self.assertIn('in 1 day - Jan 02, 2015', block.render())
+
## CourseEndDate
- def test_course_end_date_during_course(self):
+ def test_course_end_date_for_certificate_eligible_mode(self):
self.setup_course_and_user(days_till_start=-1)
block = CourseEndDate(self.course, self.user)
self.assertEqual(
@@ -178,6 +215,14 @@ def test_course_end_date_during_course(self):
'To earn a certificate, you must complete all requirements before this date.'
)
+ def test_course_end_date_for_non_certificate_eligible_mode(self):
+ self.setup_course_and_user(days_till_start=-1, enrollment_mode=CourseMode.AUDIT)
+ block = CourseEndDate(self.course, self.user)
+ self.assertEqual(
+ block.description,
+ 'After this date, course content will be archived.'
+ )
+
def test_course_end_date_after_course(self):
self.setup_course_and_user(days_till_start=-2, days_till_end=-1)
block = CourseEndDate(self.course, self.user)
@@ -188,11 +233,11 @@ def test_course_end_date_after_course(self):
## VerifiedUpgradeDeadlineDate
- @freezegun.freeze_time('2015-01-02')
+ @freeze_time('2015-01-02')
def test_verified_upgrade_deadline_date(self):
self.setup_course_and_user(days_till_upgrade_deadline=1)
block = VerifiedUpgradeDeadlineDate(self.course, self.user)
- self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=1))
+ self.assertEqual(block.date, datetime.now(utc) + timedelta(days=1))
self.assertEqual(block.link, reverse('verify_student_upgrade_and_verify', args=(self.course.id,)))
def test_without_upgrade_deadline(self):
@@ -212,13 +257,13 @@ def test_no_verified_enrollment(self):
block = VerificationDeadlineDate(self.course, self.user)
self.assertFalse(block.is_enabled)
- @freezegun.freeze_time('2015-01-02')
+ @freeze_time('2015-01-02')
def test_verification_deadline_date_upcoming(self):
self.setup_course_and_user(days_till_start=-1)
block = VerificationDeadlineDate(self.course, self.user)
self.assertEqual(block.css_class, 'verification-deadline-upcoming')
self.assertEqual(block.title, 'Verification Deadline')
- self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=14))
+ self.assertEqual(block.date, datetime.now(utc) + timedelta(days=14))
self.assertEqual(
block.description,
'You must successfully complete verification before this date to qualify for a Verified Certificate.'
@@ -226,13 +271,13 @@ def test_verification_deadline_date_upcoming(self):
self.assertEqual(block.link_text, 'Verify My Identity')
self.assertEqual(block.link, reverse('verify_student_verify_now', args=(self.course.id,)))
- @freezegun.freeze_time('2015-01-02')
+ @freeze_time('2015-01-02')
def test_verification_deadline_date_retry(self):
self.setup_course_and_user(days_till_start=-1, verification_status='denied')
block = VerificationDeadlineDate(self.course, self.user)
self.assertEqual(block.css_class, 'verification-deadline-retry')
self.assertEqual(block.title, 'Verification Deadline')
- self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=14))
+ self.assertEqual(block.date, datetime.now(utc) + timedelta(days=14))
self.assertEqual(
block.description,
'You must successfully complete verification before this date to qualify for a Verified Certificate.'
@@ -240,7 +285,7 @@ def test_verification_deadline_date_retry(self):
self.assertEqual(block.link_text, 'Retry Verification')
self.assertEqual(block.link, reverse('verify_student_reverify'))
- @freezegun.freeze_time('2015-01-02')
+ @freeze_time('2015-01-02')
def test_verification_deadline_date_denied(self):
self.setup_course_and_user(
days_till_start=-10,
@@ -250,10 +295,42 @@ def test_verification_deadline_date_denied(self):
block = VerificationDeadlineDate(self.course, self.user)
self.assertEqual(block.css_class, 'verification-deadline-passed')
self.assertEqual(block.title, 'Missed Verification Deadline')
- self.assertEqual(block.date, datetime.now(pytz.UTC) + timedelta(days=-1))
+ self.assertEqual(block.date, datetime.now(utc) + timedelta(days=-1))
self.assertEqual(
block.description,
"Unfortunately you missed this course's deadline for a successful verification."
)
self.assertEqual(block.link_text, 'Learn More')
self.assertEqual(block.link, '')
+
+ @freeze_time('2015-01-02')
+ @ddt.data(
+ (-1, '1 day ago - Jan 01, 2015'),
+ (1, 'in 1 day - Jan 03, 2015')
+ )
+ @ddt.unpack
+ def test_render_date_string_past(self, delta, expected_date_string):
+ self.setup_course_and_user(
+ days_till_start=-10,
+ verification_status='denied',
+ days_till_verification_deadline=delta,
+ )
+ block = VerificationDeadlineDate(self.course, self.user)
+ self.assertEqual(block.get_context()['date'], expected_date_string)
+
+ @freeze_time('2015-01-02')
+ @ddt.data(
+ # dates reflected from Jan 01, 2015 because of time zone offset
+ (-1, '1 day ago - Dec 31, 2014'),
+ (1, 'in 1 day - Jan 02, 2015')
+ )
+ @ddt.unpack
+ def test_render_date_string_time_zone(self, delta, expected_date_string):
+ self.setup_course_and_user(
+ days_till_start=-10,
+ verification_status='denied',
+ days_till_verification_deadline=delta,
+ )
+ set_user_preference(self.user, "time_zone", "America/Los_Angeles")
+ block = VerificationDeadlineDate(self.course, self.user)
+ self.assertEqual(block.get_context()['date'], expected_date_string)
diff --git a/lms/djangoapps/courseware/tests/test_ga_access.py b/lms/djangoapps/courseware/tests/test_ga_access.py
index 3abc2a9c7e4d..528e072b04bd 100644
--- a/lms/djangoapps/courseware/tests/test_ga_access.py
+++ b/lms/djangoapps/courseware/tests/test_ga_access.py
@@ -8,6 +8,8 @@
from courseware.ga_access import is_terminated
from courseware.tests.factories import (
BetaTesterFactory,
+ GaCourseScorerFactory,
+ GaGlobalCourseCreatorFactory,
GaOldCourseViewerStaffFactory,
StaffFactory,
UserFactory,
@@ -28,6 +30,8 @@ def _create_enrolled_users(self, course, enroll_days_ago=0):
course_staff = StaffFactory(course_key=course.id)
course_beta_tester = BetaTesterFactory(course_key=course.id)
student = UserFactory()
+ global_course_creator = GaGlobalCourseCreatorFactory()
+ course_scorer = GaCourseScorerFactory(course_key=course.id)
# enroll to course
enroll_global_staff = CourseEnrollment.enroll(global_staff, course.id)
@@ -45,8 +49,14 @@ def _create_enrolled_users(self, course, enroll_days_ago=0):
enroll_student = CourseEnrollment.enroll(student, course.id)
enroll_student.created = datetime.now() - timedelta(days=enroll_days_ago)
enroll_student.save()
+ enroll_global_course_creator = CourseEnrollment.enroll(global_course_creator, course.id)
+ enroll_global_course_creator.created = datetime.now() - timedelta(days=enroll_days_ago)
+ enroll_global_course_creator.save()
+ enroll_course_scorer = CourseEnrollment.enroll(course_scorer, course.id)
+ enroll_course_scorer.created = datetime.now() - timedelta(days=enroll_days_ago)
+ enroll_course_scorer.save()
- return global_staff, old_course_viewer, course_staff, course_beta_tester, student
+ return global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer
def setUp(self):
super(IsTerminatedTestCase, self).setUp()
@@ -71,7 +81,7 @@ def test_self_paced_course_after_course_registration_date(self):
course_overview = CourseOverview.get_from_id(course.id)
# create users and enroll to course
- global_staff, old_course_viewer, course_staff, course_beta_tester, student = self._create_enrolled_users(course)
+ global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer = self._create_enrolled_users(course)
# assert is_terminated
self.assertFalse(is_terminated(course_overview, global_staff))
@@ -79,6 +89,8 @@ def test_self_paced_course_after_course_registration_date(self):
self.assertFalse(is_terminated(course_overview, course_staff))
self.assertFalse(is_terminated(course_overview, course_beta_tester))
self.assertFalse(is_terminated(course_overview, student))
+ self.assertFalse(is_terminated(course_overview, global_course_creator))
+ self.assertFalse(is_terminated(course_overview, course_scorer))
def test_self_paced_course_after_self_pace_course_finished(self):
"""
@@ -100,7 +112,7 @@ def test_self_paced_course_after_self_pace_course_finished(self):
course_overview = CourseOverview.get_from_id(course.id)
# create users and enroll to course
- global_staff, old_course_viewer, course_staff, course_beta_tester, student = self._create_enrolled_users(
+ global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer = self._create_enrolled_users(
course=course, enroll_days_ago=20)
# assert is_terminated
@@ -109,6 +121,8 @@ def test_self_paced_course_after_self_pace_course_finished(self):
self.assertFalse(is_terminated(course_overview, course_staff))
self.assertFalse(is_terminated(course_overview, course_beta_tester))
self.assertTrue(is_terminated(course_overview, student))
+ self.assertFalse(is_terminated(course_overview, global_course_creator))
+ self.assertFalse(is_terminated(course_overview, course_scorer))
def test_self_paced_course_after_course_closed(self):
"""
@@ -130,7 +144,7 @@ def test_self_paced_course_after_course_closed(self):
course_overview = CourseOverview.get_from_id(course.id)
# create users and enroll to course
- global_staff, old_course_viewer, course_staff, course_beta_tester, student = self._create_enrolled_users(course)
+ global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer = self._create_enrolled_users(course)
# assert is_terminated
self.assertFalse(is_terminated(course_overview, global_staff))
@@ -138,6 +152,8 @@ def test_self_paced_course_after_course_closed(self):
self.assertTrue(is_terminated(course_overview, course_staff))
self.assertTrue(is_terminated(course_overview, course_beta_tester))
self.assertTrue(is_terminated(course_overview, student))
+ self.assertFalse(is_terminated(course_overview, global_course_creator))
+ self.assertTrue(is_terminated(course_overview, course_scorer))
def test_self_paced_course_after_course_start(self):
"""
@@ -159,7 +175,7 @@ def test_self_paced_course_after_course_start(self):
course_overview = CourseOverview.get_from_id(course.id)
# create users and enroll to course
- global_staff, old_course_viewer, course_staff, course_beta_tester, student = self._create_enrolled_users(course)
+ global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer = self._create_enrolled_users(course)
# assert is_terminated
self.assertFalse(is_terminated(course_overview, global_staff))
@@ -167,6 +183,8 @@ def test_self_paced_course_after_course_start(self):
self.assertFalse(is_terminated(course_overview, course_staff))
self.assertFalse(is_terminated(course_overview, course_beta_tester))
self.assertFalse(is_terminated(course_overview, student))
+ self.assertFalse(is_terminated(course_overview, global_course_creator))
+ self.assertFalse(is_terminated(course_overview, course_scorer))
def test_self_paced_course_before_course_start(self):
"""
@@ -188,7 +206,7 @@ def test_self_paced_course_before_course_start(self):
course_overview = CourseOverview.get_from_id(course.id)
# create users and enroll to course
- global_staff, old_course_viewer, course_staff, course_beta_tester, student = self._create_enrolled_users(course)
+ global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer = self._create_enrolled_users(course)
# assert is_terminated
self.assertFalse(is_terminated(course_overview, global_staff))
@@ -196,6 +214,8 @@ def test_self_paced_course_before_course_start(self):
self.assertFalse(is_terminated(course_overview, course_staff))
self.assertFalse(is_terminated(course_overview, course_beta_tester))
self.assertFalse(is_terminated(course_overview, student)) # note
+ self.assertFalse(is_terminated(course_overview, global_course_creator))
+ self.assertFalse(is_terminated(course_overview, course_scorer))
def test_instructor_paced_course_after_registration_close(self):
"""
@@ -216,7 +236,7 @@ def test_instructor_paced_course_after_registration_close(self):
course_overview = CourseOverview.get_from_id(course.id)
# create users and enroll to course
- global_staff, old_course_viewer, course_staff, course_beta_tester, student = self._create_enrolled_users(course)
+ global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer = self._create_enrolled_users(course)
# assert is_terminated
self.assertFalse(is_terminated(course_overview, global_staff))
@@ -224,6 +244,8 @@ def test_instructor_paced_course_after_registration_close(self):
self.assertFalse(is_terminated(course_overview, course_staff))
self.assertFalse(is_terminated(course_overview, course_beta_tester))
self.assertFalse(is_terminated(course_overview, student))
+ self.assertFalse(is_terminated(course_overview, global_course_creator))
+ self.assertFalse(is_terminated(course_overview, course_scorer))
def test_instructor_paced_course_after_course_closed(self):
"""
@@ -244,7 +266,7 @@ def test_instructor_paced_course_after_course_closed(self):
course_overview = CourseOverview.get_from_id(course.id)
# create users and enroll to course
- global_staff, old_course_viewer, course_staff, course_beta_tester, student = self._create_enrolled_users(course)
+ global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer = self._create_enrolled_users(course)
# assert is_terminated
self.assertFalse(is_terminated(course_overview, global_staff))
@@ -252,6 +274,8 @@ def test_instructor_paced_course_after_course_closed(self):
self.assertTrue(is_terminated(course_overview, course_staff))
self.assertTrue(is_terminated(course_overview, course_beta_tester))
self.assertTrue(is_terminated(course_overview, student))
+ self.assertFalse(is_terminated(course_overview, global_course_creator))
+ self.assertTrue(is_terminated(course_overview, course_scorer))
def test_instructor_paced_course_after_course_finished(self):
"""
@@ -272,7 +296,7 @@ def test_instructor_paced_course_after_course_finished(self):
course_overview = CourseOverview.get_from_id(course.id)
# create users and enroll to course
- global_staff, old_course_viewer, course_staff, course_beta_tester, student = self._create_enrolled_users(course)
+ global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer = self._create_enrolled_users(course)
# assert is_terminated
self.assertFalse(is_terminated(course_overview, global_staff))
@@ -280,6 +304,8 @@ def test_instructor_paced_course_after_course_finished(self):
self.assertFalse(is_terminated(course_overview, course_staff))
self.assertFalse(is_terminated(course_overview, course_beta_tester))
self.assertFalse(is_terminated(course_overview, student))
+ self.assertFalse(is_terminated(course_overview, global_course_creator))
+ self.assertFalse(is_terminated(course_overview, course_scorer))
def test_instructor_paced_course_after_course_start(self):
"""
@@ -300,7 +326,7 @@ def test_instructor_paced_course_after_course_start(self):
course_overview = CourseOverview.get_from_id(course.id)
# create users and enroll to course
- global_staff, old_course_viewer, course_staff, course_beta_tester, student = self._create_enrolled_users(course)
+ global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer = self._create_enrolled_users(course)
# assert is_terminated
self.assertFalse(is_terminated(course_overview, global_staff))
@@ -308,6 +334,8 @@ def test_instructor_paced_course_after_course_start(self):
self.assertFalse(is_terminated(course_overview, course_staff))
self.assertFalse(is_terminated(course_overview, course_beta_tester))
self.assertFalse(is_terminated(course_overview, student))
+ self.assertFalse(is_terminated(course_overview, global_course_creator))
+ self.assertFalse(is_terminated(course_overview, course_scorer))
def test_instructor_paced_course_before_course_start(self):
"""
@@ -328,7 +356,7 @@ def test_instructor_paced_course_before_course_start(self):
course_overview = CourseOverview.get_from_id(course.id)
# create users and enroll to course
- global_staff, old_course_viewer, course_staff, course_beta_tester, student = self._create_enrolled_users(course)
+ global_staff, old_course_viewer, course_staff, course_beta_tester, student, global_course_creator, course_scorer = self._create_enrolled_users(course)
# assert is_terminated
self.assertFalse(is_terminated(course_overview, global_staff))
@@ -336,3 +364,5 @@ def test_instructor_paced_course_before_course_start(self):
self.assertFalse(is_terminated(course_overview, course_staff))
self.assertFalse(is_terminated(course_overview, course_beta_tester))
self.assertFalse(is_terminated(course_overview, student))
+ self.assertFalse(is_terminated(course_overview, global_course_creator))
+ self.assertFalse(is_terminated(course_overview, course_scorer))
diff --git a/lms/djangoapps/courseware/tests/test_ga_middleware.py b/lms/djangoapps/courseware/tests/test_ga_middleware.py
index 50b8c56d02ad..54c2ba4d3fac 100644
--- a/lms/djangoapps/courseware/tests/test_ga_middleware.py
+++ b/lms/djangoapps/courseware/tests/test_ga_middleware.py
@@ -9,6 +9,7 @@
from courseware.ga_middleware import CourseTerminatedCheckMiddleware, CustomLogoMiddleware
from courseware.tests.helpers import get_request_for_user, LoginEnrollmentTestCase
from openedx.core.djangoapps.ga_optional.models import CourseOptionalConfiguration
+from student.roles import GaCourseScorerRole, GaGlobalCourseCreatorRole
from student.tests.factories import CourseAccessRoleFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -135,6 +136,104 @@ def test_self_paced_course_closed(self, is_blocked, is_staff, is_old_course_view
self.assertEqual(mock_is_course_closed.call_count, 1)
+class CourseTerminatedCheckMiddlewareTestWithGaGlobalCourseCreator(LoginEnrollmentTestCase, ModuleStoreTestCase):
+ """
+ Tests for CourseTerminatedCheckMiddleware.
+ """
+
+ def setUp(self):
+ super(CourseTerminatedCheckMiddlewareTestWithGaGlobalCourseCreator, self).setUp()
+ self.course = CourseFactory.create(start=timezone.now() - timedelta(days=10))
+
+ def _create_request(self, path):
+ self.setup_user()
+ GaGlobalCourseCreatorRole().add_users(self.user)
+ self.enroll(self.course)
+ request = get_request_for_user(self.user)
+ request.path = path.format(unicode(self.course.id))
+ return request
+
+ def test_course_opened(self):
+ """
+ Tests that the opened course always does not block the access.
+ """
+ request = self._create_request('/courses/{}/courseware/')
+ response = CourseTerminatedCheckMiddleware().process_request(request)
+ self.assertIsNone(response)
+
+ def test_course_terminated(self):
+ """
+ Tests that the terminated course the access by GaGlobalCourseCreator.
+ """
+ self.course.terminate_start = timezone.now() - timedelta(days=1)
+ self.update_course(self.course, self.user.id)
+
+ request = self._create_request('/courses/{}/courseware/')
+ response = CourseTerminatedCheckMiddleware().process_request(request)
+ self.assertIsNone(response)
+
+ @patch('openedx.core.djangoapps.ga_self_paced.api.is_course_closed', return_value=True)
+ def test_self_paced_course_closed(self, mock_is_course_closed):
+ """
+ Tests that the self-paced closed course the access by GaGlobalCourseCreator.
+ """
+ self.course.self_paced = True
+ self.update_course(self.course, self.user.id)
+
+ request = self._create_request('/courses/{}/courseware/')
+ response = CourseTerminatedCheckMiddleware().process_request(request)
+ self.assertIsNone(response)
+
+
+class CourseTerminatedCheckMiddlewareTestWithGaCourseScorer(LoginEnrollmentTestCase, ModuleStoreTestCase):
+ """
+ Tests for CourseTerminatedCheckMiddleware.
+ """
+
+ def setUp(self):
+ super(CourseTerminatedCheckMiddlewareTestWithGaCourseScorer, self).setUp()
+ self.course = CourseFactory.create(start=timezone.now() - timedelta(days=10))
+
+ def _create_request(self, path):
+ self.setup_user()
+ GaCourseScorerRole(self.course.id).add_users(self.user)
+ self.enroll(self.course)
+ request = get_request_for_user(self.user)
+ request.path = path.format(unicode(self.course.id))
+ return request
+
+ def test_course_opened(self):
+ """
+ Tests that the opened course always does not block the access.
+ """
+ request = self._create_request('/courses/{}/courseware/')
+ response = CourseTerminatedCheckMiddleware().process_request(request)
+ self.assertIsNone(response)
+
+ def test_course_terminated(self):
+ """
+ Tests that the terminated course block the access by GaCourseScorer.
+ """
+ self.course.terminate_start = timezone.now() - timedelta(days=1)
+ self.update_course(self.course, self.user.id)
+
+ request = self._create_request('/courses/{}/courseware/')
+ response = CourseTerminatedCheckMiddleware().process_request(request)
+ self.assertEquals(response.status_code, 302)
+
+ @patch('openedx.core.djangoapps.ga_self_paced.api.is_course_closed', return_value=True)
+ def test_self_paced_course_closed(self, mock_is_course_closed):
+ """
+ Tests that the self-paced closed course the access by GaCourseScorer.
+ """
+ self.course.self_paced = True
+ self.update_course(self.course, self.user.id)
+
+ request = self._create_request('/courses/{}/courseware/')
+ response = CourseTerminatedCheckMiddleware().process_request(request)
+ self.assertIsNone(response)
+
+
@ddt.ddt
class CourseTerminatedCheckViewTest(LoginEnrollmentTestCase, ModuleStoreTestCase):
"""
@@ -217,6 +316,80 @@ def test_self_paced_course_closed_not_logged_in(self):
self._assert_redirect_login(response)
+class CourseTerminatedCheckViewTestWithGaGlobalCourseCreator(LoginEnrollmentTestCase, ModuleStoreTestCase):
+ """
+ Tests for the courseware view to confirm the execute CourseTerminatedCheckMiddleware.
+ """
+
+ def setUp(self):
+ super(CourseTerminatedCheckViewTestWithGaGlobalCourseCreator, self).setUp()
+
+ self.course = CourseFactory.create(start=timezone.now() - timedelta(days=10))
+
+ def _access_page(self):
+ self.setup_user()
+ GaGlobalCourseCreatorRole().add_users(self.user)
+ self.enroll(self.course)
+ path = '/courses/{}/progress'.format(unicode(self.course.id))
+ return self.client.get(path)
+
+ def test_course_opened(self):
+ response = self._access_page()
+ self.assertEqual(response.status_code, 200)
+
+ def test_course_terminated(self):
+ self.course.terminate_start = timezone.now() - timedelta(days=1)
+ self.update_course(self.course, self.user.id)
+
+ response = self._access_page()
+ self.assertEqual(response.status_code, 200)
+
+ @patch('openedx.core.djangoapps.ga_self_paced.api.is_course_closed', return_value=True)
+ def test_self_paced_course_closed(self, mock_is_course_closed):
+ self.course.self_paced = True
+ self.update_course(self.course, self.user.id)
+
+ response = self._access_page()
+ self.assertEqual(response.status_code, 200)
+
+
+class CourseTerminatedCheckViewTestWithGaCourseScorer(LoginEnrollmentTestCase, ModuleStoreTestCase):
+ """
+ Tests for the courseware view to confirm the execute CourseTerminatedCheckMiddleware.
+ """
+
+ def setUp(self):
+ super(CourseTerminatedCheckViewTestWithGaCourseScorer, self).setUp()
+
+ self.course = CourseFactory.create(start=timezone.now() - timedelta(days=10))
+
+ def _access_page(self):
+ self.setup_user()
+ GaCourseScorerRole(self.course.id).add_users(self.user)
+ self.enroll(self.course)
+ path = '/courses/{}/progress'.format(unicode(self.course.id))
+ return self.client.get(path)
+
+ def test_course_opened(self):
+ response = self._access_page()
+ self.assertEqual(response.status_code, 200)
+
+ def test_course_terminated(self):
+ self.course.terminate_start = timezone.now() - timedelta(days=1)
+ self.update_course(self.course, self.user.id)
+
+ response = self._access_page()
+ self.assertEqual(response.status_code, 302)
+
+ @patch('openedx.core.djangoapps.ga_self_paced.api.is_course_closed', return_value=True)
+ def test_self_paced_course_closed(self, mock_is_course_closed):
+ self.course.self_paced = True
+ self.update_course(self.course, self.user.id)
+
+ response = self._access_page()
+ self.assertEqual(response.status_code, 200)
+
+
class CustomLogoMiddlewareTest(LoginEnrollmentTestCase, ModuleStoreTestCase):
def setUp(self):
diff --git a/lms/djangoapps/courseware/tests/test_ga_module_render.py b/lms/djangoapps/courseware/tests/test_ga_module_render.py
new file mode 100644
index 000000000000..271b06965c5d
--- /dev/null
+++ b/lms/djangoapps/courseware/tests/test_ga_module_render.py
@@ -0,0 +1,451 @@
+# -*- coding: utf-8 -*-
+"""
+Test for lms courseware app, module render unit
+"""
+import ddt
+import json
+from nose.plugins.attrib import attr
+from mock import Mock, patch
+
+from django.test.client import RequestFactory
+from capa.tests.response_xml_factory import OptionResponseXMLFactory
+from courseware import module_render as render
+from courseware.model_data import FieldDataCache
+from courseware.tests.factories import (
+ GaCourseScorerFactory,
+ GaGlobalCourseCreatorFactory,
+ GlobalStaffFactory,
+ StudentModuleFactory,
+ UserFactory,
+)
+from courseware.tests.test_ga_progress_restriction import ProgressRestrictionTestBase
+from lms.djangoapps.lms_xblock.runtime import quote_slashes
+from openedx.core.djangoapps.ga_optional.models import CourseOptionalConfiguration
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
+from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW, CombinedSystem
+
+
+class LMSXBlockServiceBindingTestWithGaGlobalCourseCreator(ModuleStoreTestCase):
+ """
+ Tests that the LMS Module System (XBlock Runtime) provides an expected set of services.
+ """
+
+ def setUp(self):
+ """
+ Set up the user and other fields that will be used to instantiate the runtime.
+ """
+ super(LMSXBlockServiceBindingTestWithGaGlobalCourseCreator, self).setUp()
+ self.course = CourseFactory.create()
+ self._set_up_user()
+ self.student_data = Mock()
+ self.track_function = Mock()
+ self.xqueue_callback_url_prefix = Mock()
+ self.request_token = Mock()
+
+ def _set_up_user(self):
+ self.user = GaGlobalCourseCreatorFactory()
+
+ def _assert_runtime(self, runtime):
+ # pylint: disable=no-member
+ self.assertFalse(runtime.user_is_staff)
+ self.assertFalse(runtime.user_is_admin)
+ self.assertFalse(runtime.user_is_beta_tester)
+ self.assertEqual(runtime.days_early_for_beta, 5)
+
+ def test_is_staff(self):
+ """
+ Tests that the beta tester fields are set on LMS runtime.
+ """
+ descriptor = ItemFactory(category="pure", parent=self.course)
+ descriptor.days_early_for_beta = 5
+ runtime, _ = render.get_module_system_for_user(
+ self.user,
+ self.student_data,
+ descriptor,
+ self.course.id,
+ self.track_function,
+ self.xqueue_callback_url_prefix,
+ self.request_token,
+ course=self.course
+ )
+
+ self._assert_runtime(runtime)
+
+
+class LMSXBlockServiceBindingTestWithGaCourseScorer(LMSXBlockServiceBindingTestWithGaGlobalCourseCreator):
+ """
+ Tests that the LMS Module System (XBlock Runtime) provides an expected set of services.
+ """
+
+ def _set_up_user(self):
+ self.user = GaCourseScorerFactory(course_key=self.course.id)
+
+ def _assert_runtime(self, runtime):
+ self.assertTrue(runtime.user_is_staff)
+ self.assertFalse(runtime.user_is_admin)
+ self.assertFalse(runtime.user_is_beta_tester)
+ self.assertEqual(runtime.days_early_for_beta, 5)
+
+
+class MongoViewInStudioWithRoleMixIn(ModuleStoreTestCase):
+ """Test the 'View in Studio' link visibility in a mongo backed course."""
+
+ def _set_up_user(self):
+ """ Set up the user and request that will be used. """
+ raise NotImplementedError
+
+ def _get_module(self, course_id, descriptor, location):
+ """
+ Get the module from the course from which to pattern match (or not) the 'View in Studio' buttons
+ """
+ field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
+ course_id,
+ self.user,
+ descriptor
+ )
+
+ return render.get_module(
+ self.user,
+ self.request,
+ location,
+ field_data_cache,
+ )
+
+ def setup_mongo_course(self, course_edit_method='Studio'):
+ """ Create a mongo backed course. """
+ self.course = CourseFactory.create(
+ course_edit_method=course_edit_method
+ )
+
+ descriptor = ItemFactory.create(
+ category='vertical',
+ parent_location=self.course.location,
+ )
+
+ child_descriptor = ItemFactory.create(
+ category='vertical',
+ parent_location=descriptor.location
+ )
+
+ self._set_up_user()
+ self.request = RequestFactory().get('/')
+ self.request.user = self.user
+ self.request.session = {}
+
+ self.module = self._get_module(self.course.id, descriptor, descriptor.location)
+
+ # pylint: disable=attribute-defined-outside-init
+ self.child_module = self._get_module(self.course.id, child_descriptor, child_descriptor.location)
+
+
+class MongoViewInStudioTestWithGaGlobalCourseCreator(MongoViewInStudioWithRoleMixIn):
+ """Test the 'View in Studio' link visibility in a mongo backed course."""
+
+ def _set_up_user(self):
+ """ Set up the user and request that will be used. """
+ self.user = GaGlobalCourseCreatorFactory()
+
+ def test_view_in_studio_link_studio_course(self):
+ """Regular Studio courses should see 'View in Studio' links."""
+ self.setup_mongo_course()
+ result_fragment = self.module.render(STUDENT_VIEW)
+ self.assertNotIn('View Unit in Studio', result_fragment.content)
+
+ def test_view_in_studio_link_only_in_top_level_vertical(self):
+ """Regular Studio courses should not see 'View in Studio' for child verticals of verticals."""
+ self.setup_mongo_course()
+ # Render the parent vertical, then check that there is only a single "View Unit in Studio" link.
+ result_fragment = self.module.render(STUDENT_VIEW)
+ # The single "View Unit in Studio" link should appear before the first xmodule vertical definition.
+ parts = result_fragment.content.split('data-block-type="vertical"')
+ self.assertEqual(3, len(parts), "Did not find two vertical blocks")
+ self.assertNotIn('View Unit in Studio', parts[0])
+ self.assertNotIn('View Unit in Studio', parts[1])
+ self.assertNotIn('View Unit in Studio', parts[2])
+
+ def test_view_in_studio_link_xml_authored(self):
+ """Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
+ self.setup_mongo_course(course_edit_method='XML')
+ result_fragment = self.module.render(STUDENT_VIEW)
+ self.assertNotIn('View Unit in Studio', result_fragment.content)
+
+
+class MongoViewInStudioTestWithGaCourseScorer(MongoViewInStudioWithRoleMixIn):
+ """Test the 'View in Studio' link visibility in a mongo backed course."""
+
+ def _set_up_user(self):
+ """ Set up the user and request that will be used. """
+ self.user = GaCourseScorerFactory(course_key=self.course.id)
+
+ def test_view_in_studio_link_studio_course(self):
+ """Regular Studio courses should see 'View in Studio' links."""
+ self.setup_mongo_course()
+ result_fragment = self.module.render(STUDENT_VIEW)
+ self.assertNotIn('View Unit in Studio', result_fragment.content)
+
+ def test_view_in_studio_link_only_in_top_level_vertical(self):
+ """Regular Studio courses should not see 'View in Studio' for child verticals of verticals."""
+ self.setup_mongo_course()
+ # Render the parent vertical, then check that there is only a single "View Unit in Studio" link.
+ result_fragment = self.module.render(STUDENT_VIEW)
+ # The single "View Unit in Studio" link should appear before the first xmodule vertical definition.
+ parts = result_fragment.content.split('data-block-type="vertical"')
+ self.assertEqual(3, len(parts), "Did not find two vertical blocks")
+ self.assertNotIn('View Unit in Studio', parts[0])
+ self.assertNotIn('View Unit in Studio', parts[1])
+ self.assertNotIn('View Unit in Studio', parts[2])
+
+ def test_view_in_studio_link_xml_authored(self):
+ """Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
+ self.setup_mongo_course(course_edit_method='XML')
+ result_fragment = self.module.render(STUDENT_VIEW)
+ self.assertNotIn('View Unit in Studio', result_fragment.content)
+
+
+class TestStaffDebugInfoWithRoleMixIn(ModuleStoreTestCase):
+ def _set_up_user(self):
+ raise NotImplementedError
+
+ def setUp(self):
+ super(TestStaffDebugInfoWithRoleMixIn, self).setUp()
+ self.course = CourseFactory.create()
+ self._set_up_user()
+ self.request = RequestFactory().get('/')
+ self.request.user = self.user
+ self.request.session = {}
+
+ problem_xml = OptionResponseXMLFactory().build_xml(
+ question_text='The correct answer is Correct',
+ num_inputs=2,
+ weight=2,
+ options=['Correct', 'Incorrect'],
+ correct_option='Correct'
+ )
+ self.descriptor = ItemFactory.create(
+ category='problem',
+ data=problem_xml,
+ display_name='Option Response Problem'
+ )
+
+ self.location = self.descriptor.location
+ self.field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
+ self.course.id,
+ self.user,
+ self.descriptor
+ )
+
+
+@patch.dict('django.conf.settings.FEATURES', {'DISPLAY_DEBUG_INFO_TO_STAFF': True, 'DISPLAY_HISTOGRAMS_TO_STAFF': True})
+@patch('courseware.module_render.has_access', Mock(return_value=True, autospec=True))
+class TestStaffDebugInfoWithGaGlobalCourseCreator(TestStaffDebugInfoWithRoleMixIn):
+ """Tests to verify that Staff Debug Info panel and histograms are displayed to staff."""
+
+ def _set_up_user(self):
+ self.user = GaGlobalCourseCreatorFactory.create()
+
+ @patch.dict('django.conf.settings.FEATURES', {'DISPLAY_DEBUG_INFO_TO_STAFF': False})
+ def test_staff_debug_info_disabled(self):
+ module = render.get_module(
+ self.user,
+ self.request,
+ self.location,
+ self.field_data_cache,
+ )
+ result_fragment = module.render(STUDENT_VIEW)
+ self.assertNotIn('Staff Debug', result_fragment.content)
+
+ def test_staff_debug_info_enabled(self):
+ module = render.get_module(
+ self.user,
+ self.request,
+ self.location,
+ self.field_data_cache,
+ )
+ result_fragment = module.render(STUDENT_VIEW)
+ self.assertNotIn('Staff Debug', result_fragment.content)
+
+ @patch.dict('django.conf.settings.FEATURES', {'DISPLAY_HISTOGRAMS_TO_STAFF': False})
+ def test_histogram_disabled(self):
+ module = render.get_module(
+ self.user,
+ self.request,
+ self.location,
+ self.field_data_cache,
+ )
+ result_fragment = module.render(STUDENT_VIEW)
+ self.assertNotIn('histogram', result_fragment.content)
+ self.assertNotIn('Staff Debug', result_fragment.content)
+
+ def test_histogram_enabled_for_unscored_xmodules(self):
+ """Histograms should not display for xmodules which are not scored."""
+
+ html_descriptor = ItemFactory.create(
+ category='html',
+ data='Here are some course details.'
+ )
+ field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
+ self.course.id,
+ self.user,
+ self.descriptor
+ )
+ with patch('openedx.core.lib.xblock_utils.grade_histogram') as mock_grade_histogram:
+ mock_grade_histogram.return_value = []
+ module = render.get_module(
+ self.user,
+ self.request,
+ html_descriptor.location,
+ field_data_cache,
+ )
+ module.render(STUDENT_VIEW)
+ self.assertFalse(mock_grade_histogram.called)
+
+ def test_histogram_enabled_for_scored_xmodules(self):
+ """Histograms should display for xmodules which are scored."""
+
+ StudentModuleFactory.create(
+ course_id=self.course.id,
+ module_state_key=self.location,
+ student=UserFactory(),
+ grade=1,
+ max_grade=1,
+ state="{}",
+ )
+ with patch('openedx.core.lib.xblock_utils.grade_histogram') as mock_grade_histogram:
+ mock_grade_histogram.return_value = []
+ module = render.get_module(
+ self.user,
+ self.request,
+ self.location,
+ self.field_data_cache,
+ )
+ result_fragment = module.render(STUDENT_VIEW)
+ self.assertFalse(mock_grade_histogram.called)
+ self.assertNotIn('Staff Debug', result_fragment.content)
+
+
+@patch.dict('django.conf.settings.FEATURES', {'DISPLAY_DEBUG_INFO_TO_STAFF': True, 'DISPLAY_HISTOGRAMS_TO_STAFF': True})
+@patch('courseware.module_render.has_access', Mock(return_value=True, autospec=True))
+class TestStaffDebugInfoWithGaCourseScorer(TestStaffDebugInfoWithRoleMixIn):
+ """Tests to verify that Staff Debug Info panel and histograms are displayed to staff."""
+
+ def _set_up_user(self):
+ self.user = GaCourseScorerFactory.create(course_key=self.course.id)
+
+ @patch.dict('django.conf.settings.FEATURES', {'DISPLAY_DEBUG_INFO_TO_STAFF': False})
+ def test_staff_debug_info_disabled(self):
+ module = render.get_module(
+ self.user,
+ self.request,
+ self.location,
+ self.field_data_cache,
+ )
+ result_fragment = module.render(STUDENT_VIEW)
+ self.assertNotIn('Staff Debug', result_fragment.content)
+
+ def test_staff_debug_info_enabled(self):
+ module = render.get_module(
+ self.user,
+ self.request,
+ self.location,
+ self.field_data_cache,
+ )
+ result_fragment = module.render(STUDENT_VIEW)
+ self.assertIn('Staff Debug', result_fragment.content)
+
+ @patch.dict('django.conf.settings.FEATURES', {'DISPLAY_HISTOGRAMS_TO_STAFF': False})
+ def test_histogram_disabled(self):
+ module = render.get_module(
+ self.user,
+ self.request,
+ self.location,
+ self.field_data_cache,
+ )
+ result_fragment = module.render(STUDENT_VIEW)
+ self.assertNotIn('histogram', result_fragment.content)
+ self.assertIn('Staff Debug', result_fragment.content)
+
+ def test_histogram_enabled_for_unscored_xmodules(self):
+ """Histograms should not display for xmodules which are not scored."""
+
+ html_descriptor = ItemFactory.create(
+ category='html',
+ data='Here are some course details.'
+ )
+ field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
+ self.course.id,
+ self.user,
+ self.descriptor
+ )
+ with patch('openedx.core.lib.xblock_utils.grade_histogram') as mock_grade_histogram:
+ mock_grade_histogram.return_value = []
+ module = render.get_module(
+ self.user,
+ self.request,
+ html_descriptor.location,
+ field_data_cache,
+ )
+ module.render(STUDENT_VIEW)
+ self.assertFalse(mock_grade_histogram.called)
+
+ def test_histogram_enabled_for_scored_xmodules(self):
+ """Histograms should display for xmodules which are scored."""
+
+ StudentModuleFactory.create(
+ course_id=self.course.id,
+ module_state_key=self.location,
+ student=UserFactory(),
+ grade=1,
+ max_grade=1,
+ state="{}",
+ )
+ with patch('openedx.core.lib.xblock_utils.grade_histogram') as mock_grade_histogram:
+ mock_grade_histogram.return_value = []
+ module = render.get_module(
+ self.user,
+ self.request,
+ self.location,
+ self.field_data_cache,
+ )
+ result_fragment = module.render(STUDENT_VIEW)
+ self.assertTrue(mock_grade_histogram.called)
+ self.assertIn('Staff Debug', result_fragment.content)
+
+
+@attr('shard_1')
+class TestModuleTrackingContextWithProgressRestriction(ProgressRestrictionTestBase):
+ """
+ Ensure correct tracking information is included in events emitted during XBlock callback handling.
+ """
+ def setUp(self):
+ super(TestModuleTrackingContextWithProgressRestriction, self).setUp()
+
+ self.request = RequestFactory().get('/')
+ self.request.user = self.user
+ self.request.session = {}
+
+ def test_progress_restriction_info(self):
+ self.set_course_optional_setting()
+ content = self.submit_problem(self.problem1, {'2_1': 'Correct'})
+ self.assertIn('restricted_list', content)
+ self.assertIn('restricted_chapters', content)
+ self.assertIn('restricted_sections', content)
+
+ def test_progress_restriction_info_before_answer_correctly(self):
+ self.set_course_optional_setting()
+ content = self.submit_problem(self.problem1, {'2_2': 'Incorrect'})
+ content_json = json.loads(content)
+ self.assertEqual(content_json['restricted_list'], [3])
+ self.assertEqual(content_json['restricted_chapters'], [1, 2])
+ self.assertEqual(content_json['restricted_sections'], {u'0': [2], u'1': [1, 2], u'2': [1, 2]})
+
+ def test_progress_restriction_info_after_answer_correctly(self):
+ self.set_course_optional_setting()
+ self.submit_problem(self.problem1, {'2_1': 'Correct'})
+ self.submit_problem(self.problem3, {'2_1': 'Correct'})
+ self.submit_problem(self.problem4, {'2_1': 'Correct'})
+ content = self.submit_problem(self.problem1, {'2_1': 'Correct'})
+ content_json = json.loads(content)
+ self.assertEqual(content_json['restricted_list'], [])
+ self.assertEqual(content_json['restricted_chapters'], [])
+ self.assertEqual(content_json['restricted_sections'], {u'0': [], u'1': [], u'2': []})
diff --git a/lms/djangoapps/courseware/tests/test_ga_progress_restriction.py b/lms/djangoapps/courseware/tests/test_ga_progress_restriction.py
new file mode 100644
index 000000000000..f68264160b4d
--- /dev/null
+++ b/lms/djangoapps/courseware/tests/test_ga_progress_restriction.py
@@ -0,0 +1,341 @@
+import ddt
+from capa.tests.response_xml_factory import OptionResponseXMLFactory
+from courseware.ga_progress_restriction import ProgressRestriction
+from courseware.model_data import FieldDataCache
+from courseware.module_render import get_module_for_descriptor
+from courseware.tests.helpers import LoginEnrollmentTestCase
+from django.core.urlresolvers import reverse
+from django.test.client import RequestFactory
+from lms.djangoapps.lms_xblock.runtime import quote_slashes
+from nose.plugins.attrib import attr
+from openedx.core.djangoapps.ga_optional.models import CourseOptionalConfiguration
+from xmodule.modulestore import ModuleStoreEnum
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
+
+
+@attr('shard_1')
+@ddt.ddt
+class ProgressRestrictionTestBase(ModuleStoreTestCase, LoginEnrollmentTestCase):
+ """Test methods related to progress restriction."""
+
+ def setUp(self):
+ super(ProgressRestrictionTestBase, self).setUp()
+
+ # self.course = CourseFactory.create(start=timezone.now() - timedelta(days=10))
+ self.course = CourseFactory.create()
+ self.setup_user()
+ self.enroll(self.course)
+ self.factory = RequestFactory()
+
+ self.vertical_number = dict()
+ self.chapters = []
+ self.chapter_ids = []
+ self.chapter_location_names = []
+ self.sections = []
+ self.section_location_names = []
+ self.verticals = []
+
+ self.setup_course()
+
+ def set_course_optional_setting(self):
+ self.course_optional_configuration = CourseOptionalConfiguration(
+ id=1,
+ change_date="2017-11-06 16:02:13",
+ enabled=True,
+ key='progress-restriction-settings',
+ course_key=self.course.id,
+ changed_by_id=self.user.id
+ )
+ self.course_optional_configuration.save()
+
+ def setup_course(self):
+ vertical_metadata_with_restriction = {
+ 'graded': True,
+ 'format': 'Homework',
+ 'due': '2030-12-25T00:00',
+ 'progress_restriction': {
+ 'type': 'Correct Answer Rate',
+ 'passing_mark': 50,
+ }
+ }
+ vertical_metadata_without_restriction_01 = {
+ 'graded': True,
+ 'format': 'Homework',
+ 'due': '2030-12-25T00:00',
+ 'progress_restriction': {
+ 'type': 'No Restriction',
+ }
+ }
+ vertical_metadata_without_restriction_02 = {
+ 'graded': True,
+ 'format': 'Homework',
+ 'due': '2030-12-25T00:00',
+ }
+
+ with modulestore().default_store(ModuleStoreEnum.Type.mongo):
+ for ch_idx in range(3):
+ self.chapters.append(ItemFactory(parent=self.course, category='chapter', graded=True))
+ self.chapter_ids.append(ch_idx)
+ self.chapter_location_names.append(self.chapters[ch_idx].location.name)
+
+ self.sections.append([])
+ for sc_idx in range(2):
+ self.sections[ch_idx].append(ItemFactory(parent=self.chapters[ch_idx], category='sequential'))
+ self.section_location_names.append(self.sections[ch_idx][sc_idx].location.name)
+
+ self.verticals.append(
+ ItemFactory(parent=self.sections[ch_idx][sc_idx],
+ category='vertical',
+ metadata=vertical_metadata_with_restriction)
+ )
+ self.verticals.append(
+ ItemFactory(parent=self.sections[ch_idx][sc_idx],
+ category='vertical',
+ metadata=vertical_metadata_without_restriction_01)
+ )
+ self.verticals.append(
+ ItemFactory(parent=self.sections[ch_idx][sc_idx],
+ category='vertical',
+ metadata=vertical_metadata_with_restriction)
+ )
+ self.verticals.append(
+ ItemFactory(parent=self.sections[ch_idx][sc_idx],
+ category='vertical',
+ metadata=vertical_metadata_without_restriction_02)
+ )
+
+ self.prob_xml = OptionResponseXMLFactory().build_xml(
+ question_text='The correct answer is Correct',
+ num_inputs=1,
+ weight=1,
+ options=['Correct', 'Incorrect'],
+ correct_option='Correct'
+ )
+
+ # chapter 1 / section 1 / vertical 3
+ self.problem1 = ItemFactory.create(
+ parent_location=self.verticals[2].location,
+ category='problem',
+ data=self.prob_xml,
+ display_name='p1'
+ )
+
+ self.problem2 = ItemFactory.create(
+ parent_location=self.verticals[2].location,
+ category='problem',
+ data=self.prob_xml,
+ display_name='p2'
+ )
+
+ # chapter 1 / section 2 / vertical 1
+ self.problem3 = ItemFactory.create(
+ parent_location=self.verticals[4].location,
+ category='problem',
+ data=self.prob_xml,
+ display_name='p3'
+ )
+
+ # chapter 2 / section 1 / vertical 1
+ self.problem4 = ItemFactory.create(
+ parent_location=self.verticals[8].location,
+ category='problem',
+ data=self.prob_xml,
+ display_name='p4'
+ )
+
+ self.course = modulestore().get_course(self.course.id, depth=5)
+
+ def submit_problem(self, problem, responses):
+ answer_key_prefix = 'input_{}_'.format(problem.location.html_id())
+
+ # format the response dictionary to be sent in the post request by adding the above prefix to each key
+ response_dict = {(answer_key_prefix + k): v for k, v in responses.items()}
+
+ resp = self.client.post(
+ reverse(
+ 'xblock_handler',
+ kwargs={
+ 'course_id': self.course.id.to_deprecated_string(),
+ 'usage_id': quote_slashes(problem.location.to_deprecated_string()),
+ 'handler': 'xmodule_handler',
+ 'suffix': 'problem_check',
+ }
+ ),
+ response_dict
+ )
+
+ self.assertEqual(resp.status_code, 200)
+
+ return resp.content
+
+ def get_progress_restriction_obj(self):
+ fake_request = self.factory.get(
+ reverse('courseware', kwargs={'course_id': unicode(self.course.id)})
+ )
+
+ with modulestore().bulk_operations(self.course.id):
+ field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
+ self.course.id, self.user, self.course, depth=5
+ )
+ course_module = get_module_for_descriptor(
+ self.user,
+ fake_request,
+ self.course,
+ field_data_cache,
+ self.course.id,
+ course=self.course
+ )
+ progress_restriction = ProgressRestriction(self.course.id, self.user, course_module)
+
+ return progress_restriction
+
+
+@attr('shard_1')
+@ddt.ddt
+class ProgressRestrictionTest(ProgressRestrictionTestBase):
+ """Test methods related to progress restriction."""
+
+ def setUp(self):
+ super(ProgressRestrictionTest, self).setUp()
+
+ def test_get_restricted_list_in_section_before_answer_correctly(self):
+ self.set_course_optional_setting()
+
+ self.submit_problem(self.problem1, {'2_1': 'Correct'})
+
+ progress_restriction = self.get_progress_restriction_obj()
+
+ self.assertEqual(
+ progress_restriction.get_restricted_list_in_section(self.section_location_names[1]),
+ [1, 2, 3]
+ )
+
+ def test_get_restricted_list_in_section_after_answer_correctly(self):
+ self.set_course_optional_setting()
+
+ self.submit_problem(self.problem1, {'2_1': 'Correct'})
+ self.submit_problem(self.problem3, {'2_1': 'Correct'})
+
+ progress_restriction = self.get_progress_restriction_obj()
+
+ self.assertEqual(
+ progress_restriction.get_restricted_list_in_section(self.section_location_names[1]),
+ []
+ )
+
+ def test_get_restricted_list_in_same_section_before_answer_correctly(self):
+ self.set_course_optional_setting()
+
+ self.submit_problem(self.problem1, {'2_1': 'Correct'})
+
+ progress_restriction = self.get_progress_restriction_obj()
+
+ self.assertEqual(
+ progress_restriction.get_restricted_list_in_same_section(unicode(self.verticals[4].location.name)),
+ [1, 2, 3]
+ )
+
+ def test_get_restricted_list_in_same_section_after_answer_correctly(self):
+ self.set_course_optional_setting()
+
+ self.submit_problem(self.problem1, {'2_1': 'Correct'})
+ self.submit_problem(self.problem3, {'2_1': 'Correct'})
+
+ progress_restriction = self.get_progress_restriction_obj()
+
+ self.assertEqual(
+ progress_restriction.get_restricted_list_in_same_section(unicode(self.verticals[4].location.name)),
+ []
+ )
+
+ def test_get_restricted_chapters_before_answer(self):
+ self.set_course_optional_setting()
+
+ progress_restriction = self.get_progress_restriction_obj()
+
+ self.assertEqual(progress_restriction.get_restricted_chapters(), [self.chapter_ids[1],
+ self.chapter_ids[2]])
+
+ def test_get_restricted_chapters_after_answer(self):
+ self.set_course_optional_setting()
+
+ self.submit_problem(self.problem1, {'2_1': 'Correct'})
+ self.submit_problem(self.problem3, {'2_1': 'Correct'})
+
+ progress_restriction = self.get_progress_restriction_obj()
+
+ self.assertEqual(progress_restriction.get_restricted_chapters(), [self.chapter_ids[2]])
+
+ def test_get_restricted_sections_before_answer(self):
+ self.set_course_optional_setting()
+
+ progress_restriction = self.get_progress_restriction_obj()
+
+ restricted_sections = progress_restriction.get_restricted_sections()
+
+ self.assertEqual(
+ restricted_sections[self.chapter_ids[0]],
+ [2]
+ )
+ self.assertEqual(
+ restricted_sections[self.chapter_ids[1]],
+ [1, 2]
+ )
+ self.assertEqual(
+ restricted_sections[self.chapter_ids[2]],
+ [1, 2]
+ )
+
+ def test_get_restricted_sections_after_answer(self):
+ self.set_course_optional_setting()
+
+ self.submit_problem(self.problem1, {'2_1': 'Correct'})
+ self.submit_problem(self.problem3, {'2_1': 'Correct'})
+ self.submit_problem(self.problem4, {'2_1': 'Correct'})
+
+ progress_restriction = self.get_progress_restriction_obj()
+ restricted_sections = progress_restriction.get_restricted_sections()
+
+ self.assertEqual(
+ restricted_sections[self.chapter_ids[0]],
+ []
+ )
+ self.assertEqual(
+ restricted_sections[self.chapter_ids[1]],
+ []
+ )
+ self.assertEqual(
+ restricted_sections[self.chapter_ids[2]],
+ []
+ )
+
+ @ddt.data(
+ (0, False),
+ (1, True),
+ (2, True),
+ )
+ @ddt.unpack
+ def test_is_restricted_chapter(self, chapter_idx, is_restricted):
+ self.set_course_optional_setting()
+
+ chapter = self.chapter_location_names[chapter_idx]
+ progress_restriction = self.get_progress_restriction_obj()
+ self.assertEqual(progress_restriction.is_restricted_chapter(chapter), is_restricted)
+
+ @ddt.data(
+ (0, False),
+ (1, True),
+ (2, True),
+ (3, True),
+ (4, True),
+ (5, True),
+ )
+ @ddt.unpack
+ def test_is_restricted_section(self, section_idx, is_restricted):
+ self.set_course_optional_setting()
+
+ section = self.section_location_names[section_idx]
+ progress_restriction = self.get_progress_restriction_obj()
+ self.assertEqual(progress_restriction.is_restricted_section(section), is_restricted)
diff --git a/lms/djangoapps/courseware/tests/test_ga_tabs.py b/lms/djangoapps/courseware/tests/test_ga_tabs.py
index 078264f392a8..cb7b958b8c4c 100644
--- a/lms/djangoapps/courseware/tests/test_ga_tabs.py
+++ b/lms/djangoapps/courseware/tests/test_ga_tabs.py
@@ -9,6 +9,7 @@
from courseware.tabs import get_course_tab_list
from courseware.tests.helpers import get_request_for_user, LoginEnrollmentTestCase
from courseware.tests.test_tabs import TabTestCase
+from student.roles import GaCourseScorerRole, GaGlobalCourseCreatorRole
from student.tests.factories import CourseAccessRoleFactory
@@ -76,3 +77,74 @@ def test_self_paced_course_closed(self, is_blocked, is_staff, is_old_course_view
if is_blocked and not is_staff:
self.assertEqual(mock_is_course_closed.call_count, 4)
+
+
+@ddt.ddt
+class CourseTerminatedCheckTabTestWithGaGlobalCourseCreator(LoginEnrollmentTestCase, TabTestCase):
+
+ def _create_request(self):
+ self.setup_user()
+ GaGlobalCourseCreatorRole().add_users(self.user)
+ self.enroll(self.course)
+ return get_request_for_user(self.user)
+
+ def _assert_tabs(self, tabs):
+ tab_types = [tab.type for tab in tabs]
+ self.assertItemsEqual(tab_types, ['courseware', 'course_info', 'wiki', 'progress'])
+
+ def test_course_opened(self):
+ request = self._create_request()
+ tab_list = get_course_tab_list(request, self.course)
+ self._assert_tabs(tab_list)
+
+ def test_course_terminated(self):
+ self.course.terminate_start = timezone.now() - timedelta(days=1)
+ self.update_course(self.course, self.user.id)
+
+ request = self._create_request()
+ tab_list = get_course_tab_list(request, self.course)
+ self._assert_tabs(tab_list)
+
+ @patch('openedx.core.djangoapps.ga_self_paced.api.is_course_closed', return_value=True)
+ def test_self_paced_course_closed(self, mock_is_course_closed):
+ self.course.self_paced = True
+ self.update_course(self.course, self.user.id)
+
+ request = self._create_request()
+ tab_list = get_course_tab_list(request, self.course)
+ self._assert_tabs(tab_list)
+
+
+class CourseTerminatedCheckTabTestWithGaCourseScorer(LoginEnrollmentTestCase, TabTestCase):
+
+ def _create_request(self):
+ self.setup_user()
+ GaCourseScorerRole(self.course.id).add_users(self.user)
+ self.enroll(self.course)
+ return get_request_for_user(self.user)
+
+ def _assert_tabs(self, tabs):
+ tab_types = [tab.type for tab in tabs]
+ self.assertItemsEqual(tab_types, ['courseware', 'course_info', 'wiki', 'progress', 'instructor'])
+
+ def test_course_opened(self):
+ request = self._create_request()
+ tab_list = get_course_tab_list(request, self.course)
+ self._assert_tabs(tab_list)
+
+ def test_course_terminated(self):
+ self.course.terminate_start = timezone.now() - timedelta(days=1)
+ self.update_course(self.course, self.user.id)
+
+ request = self._create_request()
+ tab_list = get_course_tab_list(request, self.course)
+ self.assertItemsEqual(tab_list, [])
+
+ @patch('openedx.core.djangoapps.ga_self_paced.api.is_course_closed', return_value=True)
+ def test_self_paced_course_closed(self, mock_is_course_closed):
+ self.course.self_paced = True
+ self.update_course(self.course, self.user.id)
+
+ request = self._create_request()
+ tab_list = get_course_tab_list(request, self.course)
+ self._assert_tabs(tab_list)
diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py
index e35f14cb5454..cfce4d181abf 100644
--- a/lms/djangoapps/courseware/tests/test_views.py
+++ b/lms/djangoapps/courseware/tests/test_views.py
@@ -35,9 +35,10 @@
from certificates.tests.factories import GeneratedCertificateFactory
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
+from courseware.ga_progress_restriction import ProgressRestriction
from courseware.model_data import set_score
from courseware.testutils import RenderXBlockTestMixin
-from courseware.tests.factories import StudentModuleFactory
+from courseware.tests.factories import GaCourseScorerFactory, GaGlobalCourseCreatorFactory, StudentModuleFactory
from courseware.user_state_client import DjangoXBlockUserStateClient
from edxmako.tests import mako_middleware_process_request
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
@@ -450,6 +451,51 @@ def test_submission_history_contents(self):
self.assertIn("Score: 3.0 / 3.0", response_content)
self.assertIn('#4', response_content)
+ def test_submission_history_contents_with_ga_course_scorer(self):
+ # log into a GaCourseScorer
+ ga_course_scorer = GaCourseScorerFactory(course_key=self.course.id)
+
+ self.client.login(username=ga_course_scorer.username, password='test')
+
+ usage_key = self.course_key.make_usage_key('problem', 'test-history')
+ state_client = DjangoXBlockUserStateClient(ga_course_scorer)
+
+ # store state via the UserStateClient
+ state_client.set(
+ username=ga_course_scorer.username,
+ block_key=usage_key,
+ state={'field_a': 'x', 'field_b': 'y'}
+ )
+
+ set_score(ga_course_scorer.id, usage_key, 0, 3)
+
+ state_client.set(
+ username=ga_course_scorer.username,
+ block_key=usage_key,
+ state={'field_a': 'a', 'field_b': 'b'}
+ )
+ set_score(ga_course_scorer.id, usage_key, 3, 3)
+
+ url = reverse('submission_history', kwargs={
+ 'course_id': unicode(self.course_key),
+ 'student_username': ga_course_scorer.username,
+ 'location': unicode(usage_key),
+ })
+ response = self.client.get(url)
+ response_content = HTMLParser().unescape(response.content.decode('utf-8'))
+
+ # We have update the state 4 times: twice to change content, and twice
+ # to set the scores. We'll check that the identifying content from each is
+ # displayed (but not the order), and also the indexes assigned in the output
+ # #1 - #4
+
+ self.assertIn('#1', response_content)
+ self.assertIn(json.dumps({'field_a': 'a', 'field_b': 'b'}, sort_keys=True, indent=2), response_content)
+ self.assertIn("Score: 0.0 / 3.0", response_content)
+ self.assertIn(json.dumps({'field_a': 'x', 'field_b': 'y'}, sort_keys=True, indent=2), response_content)
+ self.assertIn("Score: 3.0 / 3.0", response_content)
+ self.assertIn('#4', response_content)
+
@ddt.data(('America/New_York', -5), # UTC - 5
('Asia/Pyongyang', 9), # UTC + 9
('Europe/London', 0), # UTC
@@ -741,9 +787,11 @@ class TestAccordionDueDate(BaseDueDateTests):
def get_text(self, course):
""" Returns the HTML for the accordion """
+ progress_restriction = ProgressRestriction(course.id, self.user, None)
+
return views.render_accordion(
self.request.user, self.request, course,
- unicode(course.get_children()[0].scope_ids.usage_id), None, None
+ unicode(course.get_children()[0].scope_ids.usage_id), None, None, progress_restriction
)
@@ -1351,3 +1399,24 @@ def setUp(self):
def course_options(self):
return {'self_paced': True}
+
+
+class TestVisibleStudioUrl(ModuleStoreTestCase):
+ """
+ Tests for the view studio url
+ """
+ def setUp(self):
+ super(TestVisibleStudioUrl, self).setUp()
+ self.course = CourseFactory.create()
+
+ def test_get_studio_url(self):
+ # Global staff
+ self.assertIsNotNone(views._get_studio_url(self.user, self.course, 'course'))
+
+ # GaGlobalCourseCreatorFactory
+ ga_global_course_creator = GaGlobalCourseCreatorFactory()
+ self.assertIsNone(views._get_studio_url(ga_global_course_creator, self.course, 'course'))
+
+ # GaCourseScorer
+ ga_course_scorer = GaCourseScorerFactory(course_key=self.course.id)
+ self.assertIsNone(views._get_studio_url(ga_course_scorer, self.course, 'course'))
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 3f119785f0c4..bf8333ddf1c6 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -48,6 +48,7 @@
sort_by_start_date,
UserNotEnrolled
)
+from courseware.ga_progress_restriction import ProgressRestriction
from courseware.masquerade import setup_masquerade
from openedx.core.djangoapps.credit.api import (
get_credit_requirement_status,
@@ -55,7 +56,7 @@
is_credit_course
)
from openedx.core.djangoapps.ga_optional.api import is_available
-from openedx.core.djangoapps.ga_optional.models import DISCCUSION_IMAGE_UPLOAD_KEY
+from openedx.core.djangoapps.ga_optional.models import DISCCUSION_IMAGE_UPLOAD_KEY, PROGRESS_RESTRICTION_OPTION_KEY
from courseware.models import StudentModuleHistory
from courseware.model_data import FieldDataCache, ScoresClient
from .module_render import toc_for_course, get_module_for_descriptor, get_module, get_module_by_usage_id
@@ -71,6 +72,7 @@
from pdfgen import api as pdfgen_api
from student.models import UserTestGroup, CourseEnrollment
+from student.roles import GaCourseScorerRole, GaGlobalCourseCreatorRole
from student.views import is_course_blocked
from util.cache import cache, cache_if_anonymous
from util.date_utils import strftime_localized
@@ -153,7 +155,7 @@ def courses(request):
)
-def render_accordion(user, request, course, chapter, section, field_data_cache, is_modal=False):
+def render_accordion(user, request, course, chapter, section, field_data_cache, progress_restriction, is_modal=False):
"""
Draws navigation bar. Takes current position in accordion as
parameter.
@@ -167,6 +169,18 @@ def render_accordion(user, request, course, chapter, section, field_data_cache,
# grab the table of contents
toc = toc_for_course(user, request, course, chapter, section, field_data_cache)
+ # to gray out chapters and sections restricted by progress restriction
+ if progress_restriction:
+ for ch_toc in toc:
+ ch_toc['restricted'] = progress_restriction.is_restricted_chapter(ch_toc['url_name'])
+ for sc_toc in ch_toc['sections']:
+ sc_toc['restricted'] = progress_restriction.is_restricted_section(sc_toc['url_name'])
+ else:
+ for ch_toc in toc:
+ ch_toc['restricted'] = False
+ for sc_toc in ch_toc['sections']:
+ sc_toc['restricted'] = False
+
context = dict([
('toc', toc),
('course_id', course.id.to_deprecated_string()),
@@ -415,12 +429,17 @@ def _index_bulk_op(request, course_key, chapter, section, position):
u' far, should have gotten a course module for this user')
return redirect(reverse('about_course', args=[course_key.to_deprecated_string()]))
- studio_url = get_studio_url(course, 'course')
+ studio_url = _get_studio_url(user, course, 'course')
+
+ progress_restriction = ProgressRestriction(course.id, user, course_module) \
+ if is_available(PROGRESS_RESTRICTION_OPTION_KEY, course_key) and not staff_access else None
context = {
'csrf': csrf(request)['csrf_token'],
- 'accordion': render_accordion(user, request, course, chapter, section, field_data_cache),
- 'accordion_modal': render_accordion(user, request, course, chapter, section, field_data_cache, is_modal=True),
+ 'accordion': render_accordion(user, request, course, chapter, section, field_data_cache,
+ progress_restriction),
+ 'accordion_modal': render_accordion(user, request, course, chapter, section, field_data_cache,
+ progress_restriction, is_modal=True),
'COURSE_TITLE': course.display_name_with_default,
'course': course,
'init': '',
@@ -430,6 +449,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
'masquerade': masquerade,
'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"),
'is_image_upload': is_available(DISCCUSION_IMAGE_UPLOAD_KEY, course_key),
+ 'restricted_list': [],
}
now = datetime.now(UTC())
@@ -528,6 +548,10 @@ def _index_bulk_op(request, course_key, chapter, section, position):
# they don't have access to.
raise Http404
+ # Add list of subsections(sequence nubmers) restricted by progress restriction
+ if progress_restriction is not None:
+ context['restricted_list'] = progress_restriction.get_restricted_list_in_section(section)
+
# Save where we are in the chapter.
save_child_position(chapter_module, section)
section_render_context = {'activate_block_id': request.GET.get('activate_block_id')}
@@ -535,7 +559,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
context['section_title'] = section_descriptor.display_name_with_default
else:
# section is none, so display a message
- studio_url = get_studio_url(course, 'course')
+ studio_url = _get_studio_url(user, course, 'course')
prev_section = get_current_child(chapter_module)
if prev_section is None:
# Something went wrong -- perhaps this chapter has no sections visible to the user.
@@ -696,7 +720,7 @@ def course_info(request, course_id):
if request.user.is_authenticated() and survey.utils.must_answer_survey(course, user):
return redirect(reverse('course_survey', args=[unicode(course.id)]))
- studio_url = get_studio_url(course, 'course_info')
+ studio_url = _get_studio_url(user, course, 'course_info')
# link to where the student should go to enroll in the course:
# about page if there is not marketing site, SITE_NAME if there is
@@ -839,7 +863,7 @@ def course_about(request, course_id):
registered = registered_for_course(course, request.user)
staff_access = bool(has_access(request.user, 'staff', course))
- studio_url = get_studio_url(course, 'settings/details')
+ studio_url = _get_studio_url(request.user, course, 'settings/details')
if has_access(request.user, 'load', course):
course_target = reverse('info', args=[course.id.to_deprecated_string()])
@@ -981,7 +1005,7 @@ def _progress(request, course_key, student_id):
grade_summary = grades.grade(
student, request, course, field_data_cache=field_data_cache, scores_client=scores_client
)
- studio_url = get_studio_url(course, 'settings/grading')
+ studio_url = _get_studio_url(request.user, course, 'settings/grading')
if courseware_summary is None:
#This means the student didn't have access to the course (which the instructor requested)
@@ -1641,3 +1665,10 @@ def financial_assistance_form(request):
}
],
})
+
+
+def _get_studio_url(user, course, page):
+ # Note: GaGlobalCourseCreator and GaCourseScorer cannot see the studio link (#2150)
+ if GaGlobalCourseCreatorRole().has_user(user) or GaCourseScorerRole(course.id).has_user(user):
+ return None
+ return get_studio_url(course, page)
diff --git a/lms/djangoapps/ga_advanced_course/models.py b/lms/djangoapps/ga_advanced_course/models.py
index c279147af07d..6e3e5390d5ed 100644
--- a/lms/djangoapps/ga_advanced_course/models.py
+++ b/lms/djangoapps/ga_advanced_course/models.py
@@ -42,11 +42,11 @@ class AdvancedCourse(models.Model):
display_name = models.CharField(max_length=255, verbose_name=_("Display Name"))
- start_date = models.DateField(verbose_name=_("Start Date"))
+ start_date = models.DateField(verbose_name=_("Start Date (JST)"))
- start_time = models.TimeField(verbose_name=_("Start Time"))
+ start_time = models.TimeField(verbose_name=_("Start Time (JST)"))
- end_time = models.TimeField(verbose_name=_("End Time"))
+ end_time = models.TimeField(verbose_name=_("End Time (JST)"))
capacity = models.IntegerField(default=0, verbose_name=_("Capacity"))
diff --git a/lms/djangoapps/ga_operation/management/commands/count_initial_registration_course.py b/lms/djangoapps/ga_operation/management/commands/count_initial_registration_course.py
new file mode 100644
index 000000000000..ff9021d0029f
--- /dev/null
+++ b/lms/djangoapps/ga_operation/management/commands/count_initial_registration_course.py
@@ -0,0 +1,53 @@
+import logging
+import pytz
+import traceback
+from datetime import datetime
+
+from django.conf import settings
+from django.core.mail import send_mail
+from django.core.management.base import BaseCommand
+from django.db import connection
+
+log = logging.getLogger(__name__)
+query_text = """
+SELECT a.course_id, COUNT(a.course_id)
+FROM student_courseenrollment a INNER JOIN (
+ SELECT user_id, MIN(created) AS created
+ FROM student_courseenrollment
+ WHERE course_id NOT IN (SELECT course_id FROM course_global_courseglobalsetting WHERE global_enabled = 1) AND course_id LIKE '%gacco%'
+ GROUP BY user_id
+) b
+ON a.user_id = b.user_id AND a.created = b.created
+GROUP BY a.course_id;
+"""
+
+
+class Command(BaseCommand):
+ """
+ This command allows you to aggregate user's initial registration course.
+
+ Usage: python manage.py lms --settings=aws count_initial_registration_course
+ """
+ help = 'Usage: count_initial_registration_course'
+
+ def handle(self, *args, **options):
+ body = None
+
+ try:
+ with connection.cursor() as cursor:
+ cursor.execute(query_text)
+ body = "\n".join(['"{0}",{1}'.format(*c) for c in cursor.fetchall()])
+ log.info(body)
+ except Exception as e:
+ log.exception('Caught the exception: ' + type(e).__name__)
+ body = traceback.format_exc()
+ finally:
+ send_mail(
+ 'Initial registration course daily report ({0:%Y/%m/%d})'.format(
+ pytz.timezone(settings.TIME_ZONE).localize(datetime.now()),
+ ),
+ body,
+ settings.GA_OPERATION_EMAIL_SENDER_REGISTRATION_COURSE_DAILY_REPORT,
+ settings.GA_OPERATION_CALLBACK_EMAIL_REGISTRATION_COURSE_DAILY_REPORT,
+ fail_silently=False
+ )
diff --git a/lms/djangoapps/ga_operation/management/commands/tests/test_count_initial_registration_course.py b/lms/djangoapps/ga_operation/management/commands/tests/test_count_initial_registration_course.py
new file mode 100644
index 000000000000..d22a365f4bd3
--- /dev/null
+++ b/lms/djangoapps/ga_operation/management/commands/tests/test_count_initial_registration_course.py
@@ -0,0 +1,81 @@
+import ddt
+from datetime import datetime
+from mock import patch
+
+from django.conf import settings
+from django.core.management import call_command
+from django.test.utils import override_settings
+
+from opaque_keys.edx.keys import CourseKey
+from openedx.core.djangoapps.course_global.tests.factories import CourseGlobalSettingFactory
+from student.tests.factories import CourseEnrollmentFactory, UserFactory
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from xmodule.modulestore.tests.factories import CourseFactory
+
+
+@ddt.ddt
+@override_settings(
+ TIME_ZONE='Asia/Tokyo',
+ GA_OPERATION_EMAIL_SENDER_REGISTRATION_COURSE_DAILY_REPORT='sender@example.com',
+ GA_OPERATION_CALLBACK_EMAIL_REGISTRATION_COURSE_DAILY_REPORT=['receiver@example.com'],
+)
+class InitialRegistrationCourseCountTest(ModuleStoreTestCase):
+
+ def setUp(self):
+ super(InitialRegistrationCourseCountTest, self).setUp()
+
+ @staticmethod
+ def _setup_course_enrollment(course_id_dict, user_count):
+ course_list = []
+ for course_id, global_enabled in course_id_dict.iteritems():
+ course_key = CourseKey.from_string(course_id)
+ CourseGlobalSettingFactory.create(course_id=course_key, global_enabled=global_enabled)
+ course_list.append(CourseFactory.create(org=course_key.org, course=course_key.course, run=course_key.run))
+ for _ in range(user_count):
+ for course in course_list:
+ CourseEnrollmentFactory.create(user=UserFactory.create(), course_id=course.id)
+
+ @patch('ga_operation.management.commands.count_initial_registration_course.datetime')
+ @patch('ga_operation.management.commands.count_initial_registration_course.traceback.format_exc', return_value='dummy_traceback')
+ @patch('ga_operation.management.commands.count_initial_registration_course.send_mail')
+ @ddt.data(
+ ({'gacco/ht001/2015_00': True}, 3, 0),
+ ({'gacco/ht001/2015_00': True, 'gacco/course1/run': False}, 3, 3),
+ ({'gacco/ht001/2015_00': True, 'other/course/run': False, 'gacco/course1/run': False}, 3, 3),
+ )
+ @ddt.unpack
+ def test_handle(self, course_id_dict, user_count, expect_count, mock_send_mail, mock_traceback, mock_datetime):
+ expect_datetime = datetime(2020, 7, 24)
+ mock_datetime.now.return_value = expect_datetime
+ self._setup_course_enrollment(course_id_dict, user_count)
+
+ call_command('count_initial_registration_course')
+
+ mock_traceback.assert_not_called_once()
+ mock_send_mail.assert_called_once_with(
+ 'Initial registration course daily report ({0:%Y/%m/%d})'.format(expect_datetime),
+ '"gacco/course1/run",{}'.format(expect_count) if expect_count else '',
+ settings.GA_OPERATION_EMAIL_SENDER_REGISTRATION_COURSE_DAILY_REPORT,
+ settings.GA_OPERATION_CALLBACK_EMAIL_REGISTRATION_COURSE_DAILY_REPORT,
+ fail_silently=False,
+ )
+
+ @patch('ga_operation.management.commands.count_initial_registration_course.datetime')
+ @patch('ga_operation.management.commands.count_initial_registration_course.traceback.format_exc', return_value='dummy_traceback')
+ @patch('ga_operation.management.commands.count_initial_registration_course.send_mail')
+ @patch('ga_operation.management.commands.count_initial_registration_course.connection')
+ def test_handle_caught_exception(self, mock_connection, mock_send_mail, mock_traceback, mock_datetime):
+ expect_datetime = datetime(2020, 7, 24)
+ mock_datetime.now.return_value = expect_datetime
+ mock_connection.cursor().__enter__().execute.side_effect = Exception()
+
+ call_command('count_initial_registration_course')
+
+ mock_traceback.assert_called_once()
+ mock_send_mail.assert_called_once_with(
+ 'Initial registration course daily report ({0:%Y/%m/%d})'.format(expect_datetime),
+ 'dummy_traceback',
+ settings.GA_OPERATION_EMAIL_SENDER_REGISTRATION_COURSE_DAILY_REPORT,
+ settings.GA_OPERATION_CALLBACK_EMAIL_REGISTRATION_COURSE_DAILY_REPORT,
+ fail_silently=False,
+ )
diff --git a/lms/djangoapps/instructor/access.py b/lms/djangoapps/instructor/access.py
index a62a5a6dd5ef..047edc0f17cb 100644
--- a/lms/djangoapps/instructor/access.py
+++ b/lms/djangoapps/instructor/access.py
@@ -17,6 +17,7 @@
CourseInstructorRole,
CourseCcxCoachRole,
CourseStaffRole,
+ GaCourseScorerRole,
)
from instructor.enrollment import (
@@ -31,6 +32,8 @@
'instructor': CourseInstructorRole,
'staff': CourseStaffRole,
'ccx_coach': CourseCcxCoachRole,
+ # Note: GaCourseScorer is registered by instructor from membership of instructor tab (#2150)
+ 'ga_course_scorer': GaCourseScorerRole,
}
diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index cec1935634a5..8d72bd683511 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -35,7 +35,8 @@
from course_modes.models import CourseMode
from courseware.models import StudentModule
from courseware.tests.factories import (
- BetaTesterFactory, GlobalStaffFactory, InstructorFactory, StaffFactory, UserProfileFactory
+ BetaTesterFactory, GlobalStaffFactory, InstructorFactory, StaffFactory, UserProfileFactory,
+ GaCourseScorerFactory
)
from courseware.tests.helpers import LoginEnrollmentTestCase
from django_comment_common.models import FORUM_ROLE_COMMUNITY_TA
@@ -74,6 +75,7 @@
from certificates.models import CertificateStatuses
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohort_settings
+from openedx.core.lib.ga_datetime_utils import format_for_csv
from openedx.core.lib.xblock_utils import grade_histogram
from .test_tools import msk_from_problem_urlname
@@ -1943,6 +1945,7 @@ def setUp(self):
self.other_instructor = InstructorFactory(course_key=self.course.id)
self.other_staff = StaffFactory(course_key=self.course.id)
self.other_user = UserFactory()
+ self.other_ga_course_scorer = GaCourseScorerFactory(course_key=self.course.id)
def test_modify_access_noparams(self):
""" Test missing all query parameters. """
@@ -1979,6 +1982,15 @@ def test_modify_access_allow(self):
})
self.assertEqual(response.status_code, 200)
+ def test_modify_access_allow_ga_course_scorer(self):
+ url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
+ response = self.client.post(url, {
+ 'unique_student_identifier': self.other_user.email,
+ 'rolename': 'ga_course_scorer',
+ 'action': 'allow',
+ })
+ self.assertEqual(response.status_code, 200)
+
def test_modify_access_allow_with_uname(self):
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.post(url, {
@@ -1997,6 +2009,15 @@ def test_modify_access_revoke(self):
})
self.assertEqual(response.status_code, 200)
+ def test_modify_access_revoke_ga_course_scorer(self):
+ url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
+ response = self.client.post(url, {
+ 'unique_student_identifier': self.other_ga_course_scorer.email,
+ 'rolename': 'ga_course_scorer',
+ 'action': 'revoke',
+ })
+ self.assertEqual(response.status_code, 200)
+
def test_modify_access_revoke_with_username(self):
url = reverse('modify_access', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.post(url, {
@@ -2105,6 +2126,28 @@ def test_list_course_role_members_staff(self):
res_json = json.loads(response.content)
self.assertEqual(res_json, expected)
+ def test_list_course_role_members_ga_course_scorer(self):
+ url = reverse('list_course_role_members', kwargs={'course_id': self.course.id.to_deprecated_string()})
+ response = self.client.post(url, {
+ 'rolename': 'ga_course_scorer',
+ })
+ self.assertEqual(response.status_code, 200)
+
+ # check response content
+ expected = {
+ 'course_id': self.course.id.to_deprecated_string(),
+ 'ga_course_scorer': [
+ {
+ 'username': self.other_ga_course_scorer.username,
+ 'email': self.other_ga_course_scorer.email,
+ 'first_name': self.other_ga_course_scorer.first_name,
+ 'last_name': self.other_ga_course_scorer.last_name,
+ }
+ ]
+ }
+ res_json = json.loads(response.content)
+ self.assertEqual(res_json, expected)
+
def test_list_course_role_members_beta(self):
url = reverse('list_course_role_members', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.post(url, {
@@ -3452,6 +3495,14 @@ def test_send_email_as_logged_in_instructor(self):
response = self.client.post(url, self.full_test_message)
self.assertEqual(response.status_code, 200)
+ def test_send_email_as_logged_in_ga_course_scorer(self):
+ self.client.logout()
+ ga_course_scorer = GaCourseScorerFactory(course_key=self.course.id)
+ self.client.login(username=ga_course_scorer.username, password='test')
+ url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()})
+ response = self.client.post(url, self.full_test_message)
+ self.assertEqual(response.status_code, 200)
+
def test_send_email_but_not_logged_in(self):
self.client.logout()
url = reverse('send_email', kwargs={'course_id': self.course.id.to_deprecated_string()})
@@ -3606,6 +3657,28 @@ def test_list_instructor_tasks_running(self, act):
self.assertDictEqual(exp_task, act_task)
self.assertEqual(actual_tasks, expected_tasks)
+ @patch.object(instructor_task.api, 'get_running_instructor_tasks')
+ def test_list_instructor_tasks_running_with_ga_course_scorer(self, act):
+ """ Test list of all running tasks. """
+ self.client.logout()
+ self.instructor = GaCourseScorerFactory(course_key=self.course.id)
+ self.client.login(username=self.instructor.username, password='test')
+ act.return_value = self.tasks
+ url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
+ mock_factory = MockCompletionInfo()
+ with patch('instructor.views.instructor_task_helpers.get_task_completion_info') as mock_completion_info:
+ mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
+ response = self.client.post(url, {})
+ self.assertEqual(response.status_code, 200)
+
+ # check response
+ self.assertTrue(act.called)
+ expected_tasks = [ftask.to_dict() for ftask in self.tasks]
+ actual_tasks = json.loads(response.content)['tasks']
+ for exp_task, act_task in zip(expected_tasks, actual_tasks):
+ self.assertDictEqual(exp_task, act_task)
+ self.assertEqual(actual_tasks, expected_tasks)
+
@patch.object(instructor_task.api, 'get_instructor_task_history')
def test_list_background_email_tasks(self, act):
"""Test list of background email tasks."""
@@ -3625,6 +3698,27 @@ def test_list_background_email_tasks(self, act):
self.assertDictEqual(exp_task, act_task)
self.assertEqual(actual_tasks, expected_tasks)
+ @patch.object(instructor_task.api, 'get_instructor_task_history')
+ def test_list_background_email_tasks_with_ga_course_scorer(self, act):
+ """Test list of background email tasks."""
+ self.instructor = GaCourseScorerFactory(course_key=self.course.id)
+ self.client.login(username=self.instructor.username, password='test')
+ act.return_value = self.tasks
+ url = reverse('list_background_email_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
+ mock_factory = MockCompletionInfo()
+ with patch('instructor.views.instructor_task_helpers.get_task_completion_info') as mock_completion_info:
+ mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
+ response = self.client.post(url, {})
+ self.assertEqual(response.status_code, 200)
+
+ # check response
+ self.assertTrue(act.called)
+ expected_tasks = [ftask.to_dict() for ftask in self.tasks]
+ actual_tasks = json.loads(response.content)['tasks']
+ for exp_task, act_task in zip(expected_tasks, actual_tasks):
+ self.assertDictEqual(exp_task, act_task)
+ self.assertEqual(actual_tasks, expected_tasks)
+
@patch.object(instructor_task.api, 'get_instructor_task_history')
def test_list_instructor_tasks_problem(self, act):
""" Test list task history for problem. """
@@ -3804,6 +3898,18 @@ def test_list_email_with_no_successes(self, task_history_request):
self.assertDictEqual(expected_info, returned_info)
+@attr('shard_1')
+class TestInstructorEmailContentListWithGaCourseScorer(TestInstructorEmailContentList):
+ """
+ Test the instructor email content history endpoint.
+ """
+ def setUp(self):
+ super(TestInstructorEmailContentListWithGaCourseScorer, self).setUp()
+ self.client.logout()
+ self.instructor = GaCourseScorerFactory(course_key=self.course.id)
+ self.client.login(username=self.instructor.username, password='test')
+
+
@attr('shard_1')
class TestInstructorAPIHelpers(TestCase):
""" Test helpers for instructor.api """
@@ -4774,6 +4880,7 @@ def test_success_carriage_return_line_feed(self, mock_store_upload, mock_cohort_
'username,email,cohort\r\nfoo_username,bar_email,baz_cohort', mock_store_upload, mock_cohort_task
)
+
class InstructorAPISurveyDownloadTestMixin(object):
"""
Test instructor survey mix-in.
@@ -4876,17 +4983,17 @@ def test_get_survey(self):
self.assertEqual(
rows[1],
'"11111111111111111111111111111111","survey #1","%s","%s","","","1","1,2","submission #1","N/A"'
- % (submission1.created, submission1.user.username)
+ % (format_for_csv(submission1.created), submission1.user.username)
)
self.assertEqual(
rows[2],
'"11111111111111111111111111111111","survey #1","%s","%s","1","1","1","2","submission #2","N/A"'
- % (submission2.created, submission2.user.username)
+ % (format_for_csv(submission2.created), submission2.user.username)
)
self.assertEqual(
rows[3],
'"22222222222222222222222222222222","survey #2","%s","%s","","1","","","","extra"'
- % (submission3.created, submission3.user.username)
+ % (format_for_csv(submission3.created), submission3.user.username)
)
def test_get_survey_when_data_is_empty(self):
@@ -4910,12 +5017,12 @@ def test_get_survey_when_data_is_broken(self):
self.assertEqual(
rows[1],
'"11111111111111111111111111111111","survey #1","%s","%s","","","1","1,2","submission #1"'
- % (submission1.created, submission1.user.username)
+ % (format_for_csv(submission1.created), submission1.user.username)
)
self.assertEqual(
rows[2],
'"22222222222222222222222222222222","survey #5","%s","%s","","","N/A","N/A","N/A"'
- % (submission5.created, submission5.user.username)
+ % (format_for_csv(submission5.created), submission5.user.username)
)
diff --git a/lms/djangoapps/instructor/tests/test_ga_access.py b/lms/djangoapps/instructor/tests/test_ga_access.py
new file mode 100644
index 000000000000..6948cd3f7df5
--- /dev/null
+++ b/lms/djangoapps/instructor/tests/test_ga_access.py
@@ -0,0 +1,84 @@
+"""
+Test instructor.access
+"""
+
+from nose.plugins.attrib import attr
+from instructor.access import (
+ allow_access,
+ list_with_level,
+ revoke_access,
+ update_forum_role
+)
+from student.roles import GaCourseScorerRole
+from student.tests.factories import UserFactory
+from xmodule.modulestore.tests.factories import CourseFactory
+from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
+
+
+@attr('shard_1')
+class TestGaCourseScorerAccessList(SharedModuleStoreTestCase):
+ """ Test access listings. """
+ @classmethod
+ def setUpClass(cls):
+ super(TestGaCourseScorerAccessList, cls).setUpClass()
+ cls.course = CourseFactory.create()
+
+ def setUp(self):
+ super(TestGaCourseScorerAccessList, self).setUp()
+ self.ga_course_scorer = [UserFactory.create() for _ in xrange(4)]
+ for user in self.ga_course_scorer:
+ allow_access(self.course, user, 'ga_course_scorer')
+
+ def test_list(self):
+ ga_course_scorer = list_with_level(self.course, 'ga_course_scorer')
+ self.assertEqual(set(ga_course_scorer), set(self.ga_course_scorer))
+
+
+@attr('shard_1')
+class TestGaCourseScorerAccessAllow(SharedModuleStoreTestCase):
+ """ Test access allow. """
+ @classmethod
+ def setUpClass(cls):
+ super(TestGaCourseScorerAccessAllow, cls).setUpClass()
+ cls.course = CourseFactory.create()
+
+ def setUp(self):
+ super(TestGaCourseScorerAccessAllow, self).setUp()
+
+ self.course = CourseFactory.create()
+
+ def test_allow(self):
+ user = UserFactory()
+ allow_access(self.course, user, 'ga_course_scorer')
+ self.assertTrue(GaCourseScorerRole(self.course.id).has_user(user))
+
+ def test_allow_twice(self):
+ user = UserFactory()
+ allow_access(self.course, user, 'ga_course_scorer')
+ allow_access(self.course, user, 'ga_course_scorer')
+ self.assertTrue(GaCourseScorerRole(self.course.id).has_user(user))
+
+
+@attr('shard_1')
+class TestGaCourseScorerAccessRevoke(SharedModuleStoreTestCase):
+ """ Test access revoke. """
+ @classmethod
+ def setUpClass(cls):
+ super(TestGaCourseScorerAccessRevoke, cls).setUpClass()
+ cls.course = CourseFactory.create()
+
+ def setUp(self):
+ super(TestGaCourseScorerAccessRevoke, self).setUp()
+ self.ga_course_scorer = [UserFactory.create() for _ in xrange(4)]
+ for user in self.ga_course_scorer:
+ allow_access(self.course, user, 'ga_course_scorer')
+
+ def test_revoke(self):
+ user = self.ga_course_scorer[0]
+ revoke_access(self.course, user, 'ga_course_scorer')
+ self.assertFalse(GaCourseScorerRole(self.course.id).has_user(user))
+
+ def test_revoke_twice(self):
+ user = self.ga_course_scorer[0]
+ revoke_access(self.course, user, 'ga_course_scorer')
+ self.assertFalse(GaCourseScorerRole(self.course.id).has_user(user))
diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
index bf100fe10bb1..d46a5a41d7a9 100644
--- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
+++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
@@ -12,7 +12,7 @@
from ccx.tests.test_views import setup_students_and_grades
from courseware.tabs import get_course_tab_list
-from courseware.tests.factories import UserFactory
+from courseware.tests.factories import GaCourseScorerFactory, GaGlobalCourseCreatorFactory, UserFactory
from courseware.tests.helpers import LoginEnrollmentTestCase
from instructor.views.gradebook_api import calculate_page_info
@@ -100,6 +100,10 @@ def has_instructor_tab(user, course):
self.assertTrue(has_instructor_tab(self.instructor, self.course))
student = UserFactory.create()
self.assertFalse(has_instructor_tab(student, self.course))
+ ga_global_course_creator = GaGlobalCourseCreatorFactory.create()
+ self.assertFalse(has_instructor_tab(ga_global_course_creator, self.course))
+ ga_course_scorer = GaCourseScorerFactory.create(course_key=self.course.id)
+ self.assertTrue(has_instructor_tab(ga_course_scorer, self.course))
def test_default_currency_in_the_html_response(self):
"""
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 8d3dfdda30eb..b8019c46d1a4 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -117,6 +117,7 @@
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys import InvalidKeyError
from openedx.core.djangoapps.course_groups.cohorts import is_course_cohorted
+from openedx.core.lib.ga_datetime_utils import format_for_csv
log = logging.getLogger(__name__)
@@ -812,11 +813,12 @@ def modify_access(request, course_id):
return JsonResponse(response_payload)
+# Note: GaCourseScorer is registered by instructor from membership of instructor tab (#2150)
@require_POST
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('instructor')
-@require_post_params(rolename="'instructor', 'staff', or 'beta'")
+@require_post_params(rolename="'instructor', 'staff', or 'beta', 'ga_course_scorer'")
def list_course_role_members(request, course_id):
"""
List instructors and staff.
@@ -2720,7 +2722,7 @@ def csv_response(filename, header, rows):
msg = "Couldn't parse JSON in survey_answer, so treat each item as 'N/A'. course_id={0}, unit_id={1}, username={2}".format(
course_id, s.unit_id, s.username)
log.warning(msg)
- row = [s.unit_id, s.survey_name, s.created, s.username]
+ row = [s.unit_id, s.survey_name, format_for_csv(s.created), s.username]
row.append('1' if s.account_status == UserStanding.ACCOUNT_DISABLED else '')
row.append('1' if not s.is_active else '')
for key in keys:
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index e410af433d5d..01755d5ab7c4 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -37,7 +37,7 @@
from student.models import CourseEnrollment
from shoppingcart.models import Coupon, PaidCourseRegistration, CourseRegCodeItem
from course_modes.models import CourseMode, CourseModesArchive
-from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole
+from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole, GaCourseScorerRole, GaGlobalCourseCreatorRole
from certificates.models import (
CertificateGenerationConfiguration,
CertificateWhitelist,
@@ -73,6 +73,11 @@ def is_enabled(cls, course, user=None):
"""
Returns true if the specified user has staff access.
"""
+ if user and (GaGlobalCourseCreatorRole().has_user(user) and not user.is_staff):
+ # Note: GaGlobalCourseCreator cannot see the instructor tab (#2150)
+ # GaGlobalCourseCreatorRole is not a staff at LMS,
+ # but because courses made by oneself will become course instructor.
+ return False
return bool(user and has_access(user, 'staff', course, course.id))
@@ -191,7 +196,8 @@ def instructor_dashboard_2(request, course_id):
context = {
'course': course,
- 'studio_url': get_studio_url(course, 'course'),
+ # Note: GaCourseScorer cannot see the studio link (#2150)
+ 'studio_url': None if GaCourseScorerRole(course_key).has_user(request.user) else get_studio_url(course, 'course'),
'sections': [s for s in sections if s is not None],
'disable_buttons': disable_buttons,
'analytics_dashboard_message': analytics_dashboard_message,
diff --git a/lms/djangoapps/student_profile/views.py b/lms/djangoapps/student_profile/views.py
index b57f2fe6ab7d..438e36c58ff8 100644
--- a/lms/djangoapps/student_profile/views.py
+++ b/lms/djangoapps/student_profile/views.py
@@ -21,7 +21,7 @@
from openedx.core.djangoapps.user_api.accounts.serializers import PROFILE_IMAGE_KEY_PREFIX
from openedx.core.djangoapps.user_api.errors import UserNotFound, UserNotAuthorized
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences
-from student.models import User, CourseEnrollment
+from student.models import User, UserStanding, CourseEnrollment
from microsite_configuration import microsite
from xmodule.modulestore.django import modulestore
@@ -46,10 +46,17 @@ def learner_profile(request, username):
GET /account/profile
"""
try:
- return render_to_response(
- 'student_profile/learner_profile.html',
- learner_profile_context(request, username, request.user.is_staff)
- )
+ is_disabled = UserStanding.objects.filter(
+ user__username=username,
+ account_status=UserStanding.ACCOUNT_DISABLED
+ ).exists()
+ if is_disabled:
+ return render_to_response('disabled_account.html')
+ else:
+ return render_to_response(
+ 'student_profile/learner_profile.html',
+ learner_profile_context(request, username, request.user.is_staff)
+ )
except (UserNotAuthorized, UserNotFound, ObjectDoesNotExist):
raise Http404
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index a09cc2c9c3a0..f820fd3a8705 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -836,6 +836,8 @@
GA_OPERATION_TARGET_BUCKETS_OF_AGGREGATE_UPLOAD_S3_OBJECTS = ENV_TOKENS.get('GA_OPERATION_TARGET_BUCKETS_OF_AGGREGATE_UPLOAD_S3_OBJECTS', None)
GA_OPERATION_EMAIL_SENDER_FSECURE_REPORT = ENV_TOKENS.get('GA_OPERATION_EMAIL_SENDER_FSECURE_REPORT', None)
GA_OPERATION_CALLBACK_EMAIL_SERVICE_SUPPORT = ENV_TOKENS.get('GA_OPERATION_CALLBACK_EMAIL_SERVICE_SUPPORT', [])
+GA_OPERATION_EMAIL_SENDER_REGISTRATION_COURSE_DAILY_REPORT = ENV_TOKENS.get('GA_OPERATION_EMAIL_SENDER_REGISTRATION_COURSE_DAILY_REPORT', None)
+GA_OPERATION_CALLBACK_EMAIL_REGISTRATION_COURSE_DAILY_REPORT = ENV_TOKENS.get('GA_OPERATION_CALLBACK_EMAIL_REGISTRATION_COURSE_DAILY_REPORT', [])
##### Settings for Gacco.org's app #####
from ga_app.envs.aws import *
diff --git a/lms/static/css/gacco-course.css b/lms/static/css/gacco-course.css
index 7087d5704323..c1b078f73290 100644
--- a/lms/static/css/gacco-course.css
+++ b/lms/static/css/gacco-course.css
@@ -82,6 +82,9 @@ div.info-wrapper section.handouts ol li a:hover{
background-image: none;
box-shadow: 0 1px 0 #fff inset, 0 -1px 0 rgba(0, 0, 0, 0.3) inset;
}
+.course-index .accordion .course-navigation .button-chapter.restricted-chapter {
+ background-color: #aaa;
+}
.course-index .accordion .course-navigation .chapter-content-container.is-open {
border-bottom: 1px solid rgba(0,0,0,0.3);
}
@@ -196,6 +199,9 @@ div.course-wrapper > a[href="#course-accordion-modal"] span i {
#modal-accordion .chapter-wrapper:focus {
background-color: #fff;
}
+#modal-accordion .chapter-wrapper.restricted-chapter {
+ background-color: #aaa;
+}
#modal-accordion .chapter {
position: relative;
display: block;
@@ -215,6 +221,9 @@ div.course-wrapper > a[href="#course-accordion-modal"] span i {
#modal-accordion .chapter:focus {
color: #666;
}
+#modal-accordion .chapter.restricted-chapter {
+ background-color: #aaa;
+}
#modal-accordion .chapter .icon {
position: absolute;
left: 20px;
@@ -233,6 +242,9 @@ div.course-wrapper > a[href="#course-accordion-modal"] span i {
#modal-accordion .chapter-content-container .chapter-menu {
display: none;
}
+#modal-accordion .chapter-content-container .chapter-menu.restricted-chapter {
+ background-color: #aaa;
+}
#modal-accordion .chapter-content-container .menu-item {
border-bottom: 0;
border-radius: 0;
@@ -278,6 +290,10 @@ div.course-wrapper > a[href="#course-accordion-modal"] span i {
opacity: 1.0;
right: 15px;
}
+#modal-accordion .chapter-content-container .menu-item.restricted-section,
+#modal-accordion .chapter-content-container .menu-item.restricted-section a {
+ background-color: #aaa;
+}
@media screen and (max-width: 800px) {
div.course-wrapper section.course-content {
diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss
index fda4857eac3b..8da002c5bef3 100644
--- a/lms/static/sass/course/courseware/_sidebar.scss
+++ b/lms/static/sass/course/courseware/_sidebar.scss
@@ -129,6 +129,10 @@
color: $alert-color;
}
}
+
+ .subtitle-name {
+ margin-right: 5px;
+ }
}
&:hover,
@@ -164,6 +168,15 @@
padding-bottom: ($baseline/2);
}
}
+ .menu-item.restricted-section {
+ background-color: #aaa;
+ a {
+ background-color: #aaa;
+ }
+ }
+ }
+ .chapter-menu.restricted-chapter {
+ background-color: #aaa;
}
}
}
diff --git a/lms/templates/courseware/accordion.html b/lms/templates/courseware/accordion.html
index 39263f59e00f..85919b9d7ea9 100644
--- a/lms/templates/courseware/accordion.html
+++ b/lms/templates/courseware/accordion.html
@@ -15,18 +15,18 @@
active_class = ''
%>
% if is_modal:
-