18
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
### Please consider the following when opening a pull request:
|
||||
|
||||
- Link to the relevant JIRA ticket(s) and tag any relevant team(s).
|
||||
- Squash your changes down into one or more discrete commits.
|
||||
In each commit, include description that could help a developer
|
||||
several months from now.
|
||||
- If running `make upgrade`, run _as close to the time of merging as possible_
|
||||
to avoid accidentally downgrading someone else's package.
|
||||
Put the output of `make upgrade` in its own separate commit,
|
||||
decoupled from other code changes.
|
||||
- Aim for comprehensive test coverage, but remember that
|
||||
automated testing isn't a substitute for manual verification.
|
||||
- Carefully consider naming, code organization, dependencies when adding new code.
|
||||
Code that is amenable to refactoring and improvement benefits all platform developers,
|
||||
especially given the size and scope of edx-platform.
|
||||
Consult existing Architectural Decision Records (ADRs),
|
||||
including those concerning the app(s) you are changing and
|
||||
[those concerning edx-platform as a whole](https://github.com/edx/edx-platform/tree/master/docs/decisions).
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -85,6 +85,7 @@ test_root/uploads/
|
||||
django-pyfs
|
||||
.tox/
|
||||
common/test/db_cache/bok_choy_*.yaml
|
||||
common/test/data/badges/*.png
|
||||
|
||||
### Installation artifacts
|
||||
*.egg-info
|
||||
|
||||
17
Makefile
17
Makefile
@@ -1,5 +1,6 @@
|
||||
# Do things in edx-platform
|
||||
.PHONY: clean docs extract_translations help pull pull_translations push_translations requirements shell upgrade
|
||||
.PHONY: clean extract_translations help pull pull_translations push_translations requirements shell upgrade
|
||||
.PHONY: api-docs docs guides swagger
|
||||
|
||||
# Careful with mktemp syntax: it has to work on Mac and Ubuntu, which have differences.
|
||||
PRIVATE_FILES := $(shell mktemp -u /tmp/private_files.XXXXXX)
|
||||
@@ -18,7 +19,19 @@ clean: ## archive and delete most git-ignored files
|
||||
tar xf $(PRIVATE_FILES)
|
||||
rm $(PRIVATE_FILES)
|
||||
|
||||
docs: ## build the developer documentation for this repository
|
||||
SWAGGER = docs/swagger.yaml
|
||||
|
||||
docs: api-docs guides ## build all the developer documentation for this repository
|
||||
|
||||
swagger: ## generate the swagger.yaml file
|
||||
DJANGO_SETTINGS_MODULE=docs.docs_settings python manage.py lms generate_swagger --generator-class=openedx.core.openapi.ApiSchemaGenerator -o $(SWAGGER)
|
||||
|
||||
api-docs: swagger ## build the REST api docs
|
||||
rm -f docs/api/gen/*
|
||||
python docs/sw2md.py $(SWAGGER) docs/api/gen
|
||||
cd docs/api; make html
|
||||
|
||||
guides: ## build the developer guide docs
|
||||
cd docs/guides; make clean html
|
||||
|
||||
extract_translations: ## extract localizable strings from sources
|
||||
|
||||
@@ -128,7 +128,9 @@ class CourseImportView(CourseImportExportViewMixin, GenericAPIView):
|
||||
developer_message='Parameter in the wrong format',
|
||||
error_code='internal_error',
|
||||
)
|
||||
course_dir = path(settings.GITHUB_REPO_ROOT) / base64.urlsafe_b64encode(repr(course_key))
|
||||
course_dir = path(settings.GITHUB_REPO_ROOT) / base64.urlsafe_b64encode(
|
||||
repr(course_key).encode('utf-8')
|
||||
).decode('utf-8')
|
||||
temp_filepath = course_dir / filename
|
||||
if not course_dir.isdir():
|
||||
os.mkdir(course_dir)
|
||||
|
||||
@@ -108,7 +108,7 @@ def export_to_git(course_id, repo, user='', rdir=None):
|
||||
# Get current branch
|
||||
cmd = ['git', 'symbolic-ref', '--short', 'HEAD']
|
||||
try:
|
||||
branch = cmd_log(cmd, cwd).strip('\n')
|
||||
branch = cmd_log(cmd, cwd).decode('utf-8').strip('\n')
|
||||
except subprocess.CalledProcessError as ex:
|
||||
log.exception(u'Failed to get branch: %r', ex.output)
|
||||
raise GitExportError(GitExportError.DETACHED_HEAD)
|
||||
@@ -146,7 +146,7 @@ def export_to_git(course_id, repo, user='', rdir=None):
|
||||
if not branch:
|
||||
cmd = ['git', 'symbolic-ref', '--short', 'HEAD']
|
||||
try:
|
||||
branch = cmd_log(cmd, os.path.abspath(rdirp)).strip('\n')
|
||||
branch = cmd_log(cmd, os.path.abspath(rdirp)).decode('utf-8').strip('\n')
|
||||
except subprocess.CalledProcessError as ex:
|
||||
log.exception(u'Failed to get branch from freshly cloned repo: %r',
|
||||
ex.output)
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from contentstore.views import tabs
|
||||
from courseware.courses import get_course_by_id
|
||||
from lms.djangoapps.courseware.courses import get_course_by_id
|
||||
|
||||
from .prompt import query_yes_no
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class DeleteCourseTests(ModuleStoreTestCase):
|
||||
|
||||
store = contentstore()
|
||||
asset_key = course_run.id.make_asset_key('asset', 'test.txt')
|
||||
content = StaticContent(asset_key, 'test.txt', 'plain/text', 'test data')
|
||||
content = StaticContent(asset_key, 'test.txt', 'plain/text', b'test data')
|
||||
store.save(content)
|
||||
__, asset_count = store.get_all_content_for_course(course_run.id)
|
||||
assert asset_count == 1
|
||||
@@ -69,7 +69,7 @@ class DeleteCourseTests(ModuleStoreTestCase):
|
||||
store = contentstore()
|
||||
|
||||
asset_key = course_run.id.make_asset_key('asset', 'test.txt')
|
||||
content = StaticContent(asset_key, 'test.txt', 'plain/text', 'test data')
|
||||
content = StaticContent(asset_key, 'test.txt', 'plain/text', b'test data')
|
||||
store.save(content)
|
||||
__, asset_count = store.get_all_content_for_course(course_run.id)
|
||||
assert asset_count == 1
|
||||
|
||||
@@ -29,7 +29,10 @@ class TestArgParsingCourseExportOlx(unittest.TestCase):
|
||||
"""
|
||||
Test export command with no arguments
|
||||
"""
|
||||
errstring = "Error: too few arguments"
|
||||
if six.PY2:
|
||||
errstring = "Error: too few arguments"
|
||||
else:
|
||||
errstring = "Error: the following arguments are required: course_id"
|
||||
with self.assertRaisesRegexp(CommandError, errstring):
|
||||
call_command('export_olx')
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ class TestGitExport(CourseTestCase):
|
||||
)
|
||||
cwd = os.path.abspath(git_export_utils.GIT_REPO_EXPORT_DIR / 'test_bare')
|
||||
git_log = subprocess.check_output(['git', 'log', '-1',
|
||||
'--format=%an|%ae'], cwd=cwd)
|
||||
'--format=%an|%ae'], cwd=cwd).decode('utf-8')
|
||||
self.assertEqual(expect_string, git_log)
|
||||
|
||||
# Make changes to course so there is something to commit
|
||||
@@ -177,7 +177,7 @@ class TestGitExport(CourseTestCase):
|
||||
self.user.email,
|
||||
)
|
||||
git_log = subprocess.check_output(
|
||||
['git', 'log', '-1', '--format=%an|%ae'], cwd=cwd)
|
||||
['git', 'log', '-1', '--format=%an|%ae'], cwd=cwd).decode('utf-8')
|
||||
self.assertEqual(expect_string, git_log)
|
||||
|
||||
def test_no_change(self):
|
||||
|
||||
@@ -811,7 +811,7 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan
|
||||
try:
|
||||
tar_file = tarfile.open(temp_filepath)
|
||||
try:
|
||||
safetar_extractall(tar_file, (course_dir + u'/').encode(u'utf-8'))
|
||||
safetar_extractall(tar_file, (course_dir + u'/'))
|
||||
except SuspiciousOperation as exc:
|
||||
LOGGER.info(u'Course import %s: Unsafe tar file - %s', courselike_key, exc.args[0])
|
||||
with respect_language(language):
|
||||
|
||||
@@ -149,7 +149,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
|
||||
# asset in the course. (i.e. _invalid_displayname_subs-esLhHcdKGWvKs.srt)
|
||||
asset_key = course.id.make_asset_key('asset', 'sample_asset.srt')
|
||||
content = StaticContent(
|
||||
asset_key, expected_displayname, 'application/text', 'test',
|
||||
asset_key, expected_displayname, 'application/text', b'test',
|
||||
)
|
||||
content_store.save(content)
|
||||
|
||||
@@ -159,7 +159,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
|
||||
|
||||
# Verify both assets have similar `displayname` after saving.
|
||||
for asset in assets:
|
||||
self.assertEquals(asset['displayname'], expected_displayname)
|
||||
self.assertEqual(asset['displayname'], expected_displayname)
|
||||
|
||||
# Test course export does not fail
|
||||
root_dir = path(mkdtemp_clean())
|
||||
@@ -500,7 +500,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
|
||||
# Create a module, and ensure that its `data` field is empty
|
||||
word_cloud = ItemFactory.create(parent_location=parent.location, category="word_cloud", display_name="untitled")
|
||||
del word_cloud.data
|
||||
self.assertEquals(word_cloud.data, '')
|
||||
self.assertEqual(word_cloud.data, '')
|
||||
|
||||
# Export the course
|
||||
root_dir = path(mkdtemp_clean())
|
||||
@@ -511,7 +511,7 @@ class ImportRequiredTestCases(ContentStoreTestCase):
|
||||
imported_word_cloud = self.store.get_item(course_id.make_usage_key('word_cloud', 'untitled'))
|
||||
|
||||
# It should now contain empty data
|
||||
self.assertEquals(imported_word_cloud.data, '')
|
||||
self.assertEqual(imported_word_cloud.data, '')
|
||||
|
||||
def test_html_export_roundtrip(self):
|
||||
"""
|
||||
@@ -689,7 +689,7 @@ class MiscCourseTests(ContentStoreTestCase):
|
||||
resp = self.client.get_html(get_url('container_handler', self.vert_loc) + '?action=' + malicious_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# Test that malicious code does not appear in html
|
||||
self.assertNotIn(malicious_code, resp.content)
|
||||
self.assertNotIn(malicious_code, resp.content.decode('utf-8'))
|
||||
|
||||
def test_advanced_components_in_edit_unit(self):
|
||||
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the
|
||||
@@ -708,7 +708,7 @@ class MiscCourseTests(ContentStoreTestCase):
|
||||
# Create an asset with slash `invalid_displayname` '
|
||||
asset_key = self.course.id.make_asset_key('asset', "fake_asset.txt")
|
||||
content = StaticContent(
|
||||
asset_key, invalid_displayname, 'application/text', 'test',
|
||||
asset_key, invalid_displayname, 'application/text', b'test',
|
||||
)
|
||||
content_store.save(content)
|
||||
|
||||
@@ -766,7 +766,7 @@ class MiscCourseTests(ContentStoreTestCase):
|
||||
asset_path = 'sample_asset_{}.txt'.format(i)
|
||||
asset_key = self.course.id.make_asset_key('asset', asset_path)
|
||||
content = StaticContent(
|
||||
asset_key, asset_displayname, 'application/text', 'test',
|
||||
asset_key, asset_displayname, 'application/text', b'test',
|
||||
)
|
||||
content_store.save(content)
|
||||
|
||||
@@ -776,7 +776,7 @@ class MiscCourseTests(ContentStoreTestCase):
|
||||
|
||||
# Verify both assets have similar 'displayname' after saving.
|
||||
for asset in assets:
|
||||
self.assertEquals(asset['displayname'], asset_displayname)
|
||||
self.assertEqual(asset['displayname'], asset_displayname)
|
||||
|
||||
# Now export the course to a tempdir and test that it contains assets.
|
||||
root_dir = path(mkdtemp_clean())
|
||||
@@ -1000,7 +1000,7 @@ class MiscCourseTests(ContentStoreTestCase):
|
||||
"""
|
||||
asset_key = self.course.id.make_asset_key('asset', 'sample_static.html')
|
||||
content = StaticContent(
|
||||
asset_key, "Fake asset", "application/text", "test",
|
||||
asset_key, "Fake asset", "application/text", b"test",
|
||||
)
|
||||
contentstore().save(content)
|
||||
|
||||
@@ -1082,7 +1082,7 @@ class MiscCourseTests(ContentStoreTestCase):
|
||||
# add an asset
|
||||
asset_key = self.course.id.make_asset_key('asset', 'sample_static.html')
|
||||
content = StaticContent(
|
||||
asset_key, "Fake asset", "application/text", "test",
|
||||
asset_key, "Fake asset", "application/text", b"test",
|
||||
)
|
||||
contentstore().save(content)
|
||||
assets, count = contentstore().get_all_content_for_course(self.course.id)
|
||||
@@ -1207,7 +1207,7 @@ class ContentStoreTest(ContentStoreTestCase):
|
||||
test_course_data = self.assert_created_course()
|
||||
course_id = _get_course_id(self.store, test_course_data)
|
||||
course_module = self.store.get_course(course_id)
|
||||
self.assertEquals(course_module.language, 'hr')
|
||||
self.assertEqual(course_module.language, 'hr')
|
||||
|
||||
def test_create_course_with_dots(self):
|
||||
"""Test new course creation with dots in the name"""
|
||||
@@ -1640,7 +1640,7 @@ class ContentStoreTest(ContentStoreTestCase):
|
||||
# Import a course with wiki_slug == location.course
|
||||
import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], target_id=target_id)
|
||||
course_module = self.store.get_course(target_id)
|
||||
self.assertEquals(course_module.wiki_slug, 'toy')
|
||||
self.assertEqual(course_module.wiki_slug, 'toy')
|
||||
|
||||
# But change the wiki_slug if it is a different course.
|
||||
target_id = self.store.make_course_key('MITx', '111', '2013_Spring')
|
||||
@@ -1655,12 +1655,12 @@ class ContentStoreTest(ContentStoreTestCase):
|
||||
# Import a course with wiki_slug == location.course
|
||||
import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], target_id=target_id)
|
||||
course_module = self.store.get_course(target_id)
|
||||
self.assertEquals(course_module.wiki_slug, 'MITx.111.2013_Spring')
|
||||
self.assertEqual(course_module.wiki_slug, 'MITx.111.2013_Spring')
|
||||
|
||||
# Now try importing a course with wiki_slug == '{0}.{1}.{2}'.format(location.org, location.course, location.run)
|
||||
import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['two_toys'], target_id=target_id)
|
||||
course_module = self.store.get_course(target_id)
|
||||
self.assertEquals(course_module.wiki_slug, 'MITx.111.2013_Spring')
|
||||
self.assertEqual(course_module.wiki_slug, 'MITx.111.2013_Spring')
|
||||
|
||||
def test_import_metadata_with_attempts_empty_string(self):
|
||||
import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['simple'], create_if_not_present=True)
|
||||
@@ -1797,13 +1797,13 @@ class ContentStoreTest(ContentStoreTestCase):
|
||||
course_key = _get_course_id(self.store, self.course_data)
|
||||
_create_course(self, course_key, self.course_data)
|
||||
course_module = self.store.get_course(course_key)
|
||||
self.assertEquals(course_module.wiki_slug, 'MITx.111.2013_Spring')
|
||||
self.assertEqual(course_module.wiki_slug, 'MITx.111.2013_Spring')
|
||||
|
||||
def test_course_handler_with_invalid_course_key_string(self):
|
||||
"""Test viewing the course overview page with invalid course id"""
|
||||
|
||||
response = self.client.get_html('/course/edX/test')
|
||||
self.assertEquals(response.status_code, 404)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
class MetadataSaveTestCase(ContentStoreTestCase):
|
||||
@@ -1933,7 +1933,7 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
'should_display': True,
|
||||
}
|
||||
for field_name, expected_value in six.iteritems(expected_states):
|
||||
self.assertEquals(getattr(rerun_state, field_name), expected_value)
|
||||
self.assertEqual(getattr(rerun_state, field_name), expected_value)
|
||||
|
||||
# Verify that the creator is now enrolled in the course.
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, destination_course_key))
|
||||
@@ -2023,7 +2023,7 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
|
||||
# Verify that the course rerun action is marked failed
|
||||
rerun_state = CourseRerunState.objects.find_first(course_key=destination_course_key)
|
||||
self.assertEquals(rerun_state.state, CourseRerunUIStateManager.State.FAILED)
|
||||
self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.FAILED)
|
||||
self.assertIn("Cannot find a course at", rerun_state.message)
|
||||
|
||||
# Verify that the creator is not enrolled in the course.
|
||||
@@ -2071,7 +2071,7 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
source_course = CourseFactory.create()
|
||||
destination_course_key = self.post_rerun_request(source_course.id)
|
||||
rerun_state = CourseRerunState.objects.find_first(course_key=destination_course_key)
|
||||
self.assertEquals(rerun_state.state, CourseRerunUIStateManager.State.FAILED)
|
||||
self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.FAILED)
|
||||
self.assertIn(error_message, rerun_state.message)
|
||||
|
||||
def test_rerun_error_trunc_message(self):
|
||||
@@ -2090,7 +2090,7 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
with mock.patch('traceback.format_exc', return_value=message_too_long):
|
||||
destination_course_key = self.post_rerun_request(source_course.id)
|
||||
rerun_state = CourseRerunState.objects.find_first(course_key=destination_course_key)
|
||||
self.assertEquals(rerun_state.state, CourseRerunUIStateManager.State.FAILED)
|
||||
self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.FAILED)
|
||||
self.assertTrue(rerun_state.message.endswith("traceback"))
|
||||
self.assertEqual(len(rerun_state.message), CourseRerunState.MAX_MESSAGE_LENGTH)
|
||||
|
||||
@@ -2112,7 +2112,7 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
source_course = self.store.get_course(source_course_key)
|
||||
|
||||
# Verify created course's wiki_slug.
|
||||
self.assertEquals(source_course.wiki_slug, source_wiki_slug)
|
||||
self.assertEqual(source_course.wiki_slug, source_wiki_slug)
|
||||
|
||||
destination_course_data = course_data
|
||||
destination_course_data['run'] = '2013_Rerun'
|
||||
@@ -2128,7 +2128,7 @@ class RerunCourseTest(ContentStoreTestCase):
|
||||
)
|
||||
|
||||
# Verify rerun course's wiki_slug.
|
||||
self.assertEquals(destination_course.wiki_slug, destination_wiki_slug)
|
||||
self.assertEqual(destination_course.wiki_slug, destination_wiki_slug)
|
||||
|
||||
|
||||
class ContentLicenseTest(ContentStoreTestCase):
|
||||
@@ -2144,8 +2144,9 @@ class ContentLicenseTest(ContentStoreTestCase):
|
||||
export_course_to_xml(self.store, content_store, self.course.id, root_dir, u'test_license')
|
||||
fname = "{block}.xml".format(block=self.course.scope_ids.usage_id.block_id)
|
||||
run_file_path = root_dir / "test_license" / "course" / fname
|
||||
run_xml = etree.parse(run_file_path.open())
|
||||
self.assertEqual(run_xml.getroot().get("license"), "creative-commons: BY SA")
|
||||
with run_file_path.open() as f:
|
||||
run_xml = etree.parse(f)
|
||||
self.assertEqual(run_xml.getroot().get("license"), "creative-commons: BY SA")
|
||||
|
||||
def test_video_license_export(self):
|
||||
content_store = contentstore()
|
||||
@@ -2157,8 +2158,9 @@ class ContentLicenseTest(ContentStoreTestCase):
|
||||
export_course_to_xml(self.store, content_store, self.course.id, root_dir, u'test_license')
|
||||
fname = "{block}.xml".format(block=video_descriptor.scope_ids.usage_id.block_id)
|
||||
video_file_path = root_dir / "test_license" / "video" / fname
|
||||
video_xml = etree.parse(video_file_path.open())
|
||||
self.assertEqual(video_xml.getroot().get("license"), "all-rights-reserved")
|
||||
with video_file_path.open() as f:
|
||||
video_xml = etree.parse(f)
|
||||
self.assertEqual(video_xml.getroot().get("license"), "all-rights-reserved")
|
||||
|
||||
def test_license_import(self):
|
||||
course_items = import_course_from_xml(
|
||||
|
||||
@@ -22,6 +22,7 @@ from pytz import UTC
|
||||
|
||||
from contentstore.config.waffle import ENABLE_PROCTORING_PROVIDER_OVERRIDES
|
||||
from contentstore.utils import reverse_course_url, reverse_usage_url
|
||||
from course_modes.models import CourseMode
|
||||
from models.settings.course_grading import GRADING_POLICY_CHANGED_EVENT_TYPE, CourseGradingModel, hash_grading_policy
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
from models.settings.encoder import CourseSettingsEncoder
|
||||
@@ -179,6 +180,29 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
elif field in encoded and encoded[field] is not None:
|
||||
self.fail(field + " included in encoding but missing from details at " + context)
|
||||
|
||||
@ddt.data(
|
||||
(False, False),
|
||||
(True, False),
|
||||
(True, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_upgrade_deadline(self, has_verified_mode, has_expiration_date):
|
||||
if has_verified_mode:
|
||||
deadline = None
|
||||
if has_expiration_date:
|
||||
deadline = self.course.start + datetime.timedelta(days=2)
|
||||
CourseMode.objects.get_or_create(
|
||||
course_id=self.course.id,
|
||||
mode_display_name="Verified",
|
||||
mode_slug="verified",
|
||||
min_price=1,
|
||||
_expiration_datetime=deadline,
|
||||
)
|
||||
|
||||
settings_details_url = get_url(self.course.id)
|
||||
response = self.client.get_html(settings_details_url)
|
||||
self.assertEqual("Upgrade Deadline Date" in response.content, has_expiration_date and has_verified_mode)
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True})
|
||||
def test_pre_requisite_course_list_present(self):
|
||||
settings_details_url = get_url(self.course.id)
|
||||
@@ -240,15 +264,14 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
(False, True, False),
|
||||
(True, True, True),
|
||||
)
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
def test_visibility_of_entrance_exam_section(self, feature_flags):
|
||||
"""
|
||||
Tests entrance exam section is available if ENTRANCE_EXAMS feature is enabled no matter any other
|
||||
feature is enabled or disabled i.e ENABLE_MKTG_SITE.
|
||||
feature is enabled or disabled i.e ENABLE_PUBLISHER.
|
||||
"""
|
||||
with patch.dict("django.conf.settings.FEATURES", {
|
||||
'ENTRANCE_EXAMS': feature_flags[0],
|
||||
'ENABLE_MKTG_SITE': feature_flags[1]
|
||||
'ENABLE_PUBLISHER': feature_flags[1]
|
||||
}):
|
||||
course_details_url = get_url(self.course.id)
|
||||
resp = self.client.get_html(course_details_url)
|
||||
@@ -257,11 +280,11 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
b'<h3 id="heading-entrance-exam">' in resp.content
|
||||
)
|
||||
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
def test_marketing_site_fetch(self):
|
||||
settings_details_url = get_url(self.course.id)
|
||||
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {
|
||||
'ENABLE_PUBLISHER': True,
|
||||
'ENABLE_MKTG_SITE': True,
|
||||
'ENTRANCE_EXAMS': False,
|
||||
'ENABLE_PREREQUISITE_COURSES': False
|
||||
@@ -276,8 +299,6 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
self.assertContains(response, "Enrollment Start Date")
|
||||
self.assertContains(response, "Enrollment End Date")
|
||||
|
||||
self.assertContains(response, "Introducing Your Course")
|
||||
self.assertContains(response, "Course Card Image")
|
||||
self.assertContains(response, "Course Short Description")
|
||||
self.assertNotContains(response, "Course About Sidebar HTML")
|
||||
self.assertNotContains(response, "Course Title")
|
||||
@@ -416,7 +437,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
|
||||
def test_regular_site_fetch(self):
|
||||
settings_details_url = get_url(self.course.id)
|
||||
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False,
|
||||
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_PUBLISHER': False,
|
||||
'ENABLE_EXTENDED_COURSE_DETAILS': True}):
|
||||
response = self.client.get_html(settings_details_url)
|
||||
self.assertContains(response, "Course Summary Page")
|
||||
@@ -797,7 +818,6 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
self.fullcourse = CourseFactory.create()
|
||||
self.course_setting_url = get_url(self.course.id, 'advanced_settings_handler')
|
||||
self.fullcourse_setting_url = get_url(self.fullcourse.id, 'advanced_settings_handler')
|
||||
self.notes_tab = {"type": "notes", "name": "My Notes"}
|
||||
|
||||
self.request = RequestFactory().request()
|
||||
self.user = UserFactory()
|
||||
@@ -1127,60 +1147,6 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
|
||||
self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value")
|
||||
|
||||
def test_advanced_components_munge_tabs(self):
|
||||
"""
|
||||
Test that adding and removing specific advanced components adds and removes tabs.
|
||||
"""
|
||||
# First ensure that none of the tabs are visible
|
||||
self.assertNotIn(self.notes_tab, self.course.tabs)
|
||||
|
||||
# Now enable student notes and verify that the "My Notes" tab has been added
|
||||
self.client.ajax_post(self.course_setting_url, {
|
||||
'advanced_modules': {"value": ["notes"]}
|
||||
})
|
||||
course = modulestore().get_course(self.course.id)
|
||||
self.assertIn(self.notes_tab, course.tabs)
|
||||
|
||||
# Disable student notes and verify that the "My Notes" tab is gone
|
||||
self.client.ajax_post(self.course_setting_url, {
|
||||
'advanced_modules': {"value": [""]}
|
||||
})
|
||||
course = modulestore().get_course(self.course.id)
|
||||
self.assertNotIn(self.notes_tab, course.tabs)
|
||||
|
||||
def test_advanced_components_munge_tabs_validation_failure(self):
|
||||
with patch('contentstore.views.course._refresh_course_tabs', side_effect=InvalidTabsException):
|
||||
resp = self.client.ajax_post(self.course_setting_url, {
|
||||
'advanced_modules': {"value": ["notes"]}
|
||||
})
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
error_msg = [
|
||||
{
|
||||
'message': 'An error occurred while trying to save your tabs',
|
||||
'model': {'display_name': 'Tabs Exception'}
|
||||
}
|
||||
]
|
||||
self.assertEqual(json.loads(resp.content.decode('utf-8')), error_msg)
|
||||
|
||||
# verify that the course wasn't saved into the modulestore
|
||||
course = modulestore().get_course(self.course.id)
|
||||
self.assertNotIn("notes", course.advanced_modules)
|
||||
|
||||
@ddt.data(
|
||||
[{'type': 'course_info'}, {'type': 'courseware'}, {'type': 'wiki', 'is_hidden': True}],
|
||||
[{'type': 'course_info', 'name': 'Home'}, {'type': 'courseware', 'name': 'Course'}],
|
||||
)
|
||||
def test_course_tab_configurations(self, tab_list):
|
||||
self.course.tabs = tab_list
|
||||
modulestore().update_item(self.course, self.user.id)
|
||||
self.client.ajax_post(self.course_setting_url, {
|
||||
'advanced_modules': {"value": ["notes"]}
|
||||
})
|
||||
course = modulestore().get_course(self.course.id)
|
||||
tab_list.append(self.notes_tab)
|
||||
self.assertEqual(tab_list, course.tabs)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': True})
|
||||
@patch('xmodule.util.xmodule_django.get_current_request')
|
||||
def test_post_settings_with_staff_not_enrolled(self, mock_request):
|
||||
@@ -1534,44 +1500,42 @@ id=\"course-enrollment-end-time\" value=\"\" placeholder=\"HH:MM\" autocomplete=
|
||||
for element in self.EDITABLE_ELEMENTS:
|
||||
self.assertNotContains(response, element)
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': False})
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': False})
|
||||
def test_course_details_with_disabled_setting_global_staff(self):
|
||||
"""
|
||||
Test that user enrollment end date is editable in response.
|
||||
|
||||
Feature flag 'ENABLE_MKTG_SITE' is not enabled.
|
||||
Feature flag 'ENABLE_PUBLISHER' is not enabled.
|
||||
User is global staff.
|
||||
"""
|
||||
self._verify_editable(self._get_course_details_response(True))
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': False})
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': False})
|
||||
def test_course_details_with_disabled_setting_non_global_staff(self):
|
||||
"""
|
||||
Test that user enrollment end date is editable in response.
|
||||
|
||||
Feature flag 'ENABLE_MKTG_SITE' is not enabled.
|
||||
Feature flag 'ENABLE_PUBLISHER' is not enabled.
|
||||
User is non-global staff.
|
||||
"""
|
||||
self._verify_editable(self._get_course_details_response(False))
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': True})
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': True})
|
||||
def test_course_details_with_enabled_setting_global_staff(self):
|
||||
"""
|
||||
Test that user enrollment end date is editable in response.
|
||||
|
||||
Feature flag 'ENABLE_MKTG_SITE' is enabled.
|
||||
Feature flag 'ENABLE_PUBLISHER' is enabled.
|
||||
User is global staff.
|
||||
"""
|
||||
self._verify_editable(self._get_course_details_response(True))
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': True})
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': True})
|
||||
def test_course_details_with_enabled_setting_non_global_staff(self):
|
||||
"""
|
||||
Test that user enrollment end date is not editable in response.
|
||||
|
||||
Feature flag 'ENABLE_MKTG_SITE' is enabled.
|
||||
Feature flag 'ENABLE_PUBLISHER' is enabled.
|
||||
User is non-global staff.
|
||||
"""
|
||||
self._verify_not_editable(self._get_course_details_response(False))
|
||||
|
||||
@@ -69,7 +69,7 @@ class TestExportGit(CourseTestCase):
|
||||
self.assertIn(
|
||||
('giturl must be defined in your '
|
||||
'course settings before you can export to git.'),
|
||||
response.content
|
||||
response.content.decode('utf-8')
|
||||
)
|
||||
|
||||
response = self.client.get('{}?action=push'.format(self.test_url))
|
||||
@@ -77,7 +77,7 @@ class TestExportGit(CourseTestCase):
|
||||
self.assertIn(
|
||||
('giturl must be defined in your '
|
||||
'course settings before you can export to git.'),
|
||||
response.content
|
||||
response.content.decode('utf-8')
|
||||
)
|
||||
|
||||
def test_course_export_failures(self):
|
||||
@@ -88,7 +88,7 @@ class TestExportGit(CourseTestCase):
|
||||
modulestore().update_item(self.course_module, self.user.id)
|
||||
|
||||
response = self.client.get('{}?action=push'.format(self.test_url))
|
||||
self.assertIn('Export Failed:', response.content)
|
||||
self.assertIn('Export Failed:', response.content.decode('utf-8'))
|
||||
|
||||
def test_exception_translation(self):
|
||||
"""
|
||||
@@ -98,7 +98,7 @@ class TestExportGit(CourseTestCase):
|
||||
modulestore().update_item(self.course_module, self.user.id)
|
||||
|
||||
response = self.client.get('{}?action=push'.format(self.test_url))
|
||||
self.assertNotIn('django.utils.functional.__proxy__', response.content)
|
||||
self.assertNotIn('django.utils.functional.__proxy__', response.content.decode('utf-8'))
|
||||
|
||||
def test_course_export_success(self):
|
||||
"""
|
||||
@@ -107,7 +107,7 @@ class TestExportGit(CourseTestCase):
|
||||
|
||||
self.make_bare_repo_with_course('test_repo')
|
||||
response = self.client.get('{}?action=push'.format(self.test_url))
|
||||
self.assertIn('Export Succeeded', response.content)
|
||||
self.assertIn('Export Succeeded', response.content.decode('utf-8'))
|
||||
|
||||
def test_repo_with_dots(self):
|
||||
"""
|
||||
@@ -115,7 +115,7 @@ class TestExportGit(CourseTestCase):
|
||||
"""
|
||||
self.make_bare_repo_with_course('test.repo')
|
||||
response = self.client.get('{}?action=push'.format(self.test_url))
|
||||
self.assertIn('Export Succeeded', response.content)
|
||||
self.assertIn('Export Succeeded', response.content.decode('utf-8'))
|
||||
|
||||
def test_dirty_repo(self):
|
||||
"""
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.utils.translation import get_language
|
||||
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from contentstore.views.preview import _preview_module_system
|
||||
from openedx.core.lib.edx_six import get_gettext
|
||||
from xmodule.modulestore.django import ModuleI18nService
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -94,7 +95,7 @@ class TestModuleI18nService(ModuleStoreTestCase):
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.old_ugettext = module.ugettext
|
||||
self.old_ugettext = get_gettext(module)
|
||||
|
||||
def __enter__(self):
|
||||
def new_ugettext(*args, **kwargs):
|
||||
@@ -102,9 +103,11 @@ class TestModuleI18nService(ModuleStoreTestCase):
|
||||
output = self.old_ugettext(*args, **kwargs)
|
||||
return "XYZ " + output
|
||||
self.module.ugettext = new_ugettext
|
||||
self.module.gettext = new_ugettext
|
||||
|
||||
def __exit__(self, _type, _value, _traceback):
|
||||
self.module.ugettext = self.old_ugettext
|
||||
self.module.gettext = self.old_ugettext
|
||||
|
||||
i18n_service = self.get_module_i18n_service(self.descriptor)
|
||||
|
||||
@@ -149,9 +152,9 @@ class TestModuleI18nService(ModuleStoreTestCase):
|
||||
with mock.patch('gettext.translation', return_value=_translator(domain='text', localedir=localedir,
|
||||
languages=[get_language()])):
|
||||
i18n_service = self.get_module_i18n_service(self.descriptor)
|
||||
self.assertEqual(i18n_service.ugettext('Hello'), 'Hello')
|
||||
self.assertNotEqual(i18n_service.ugettext('Hello'), 'fr-hello-world')
|
||||
self.assertNotEqual(i18n_service.ugettext('Hello'), 'es-hello-world')
|
||||
self.assertEqual(get_gettext(i18n_service)('Hello'), 'Hello')
|
||||
self.assertNotEqual(get_gettext(i18n_service)('Hello'), 'fr-hello-world')
|
||||
self.assertNotEqual(get_gettext(i18n_service)('Hello'), 'es-hello-world')
|
||||
|
||||
translation.activate("fr")
|
||||
with mock.patch('gettext.translation', return_value=_translator(domain='text', localedir=localedir,
|
||||
|
||||
@@ -93,7 +93,7 @@ class TestOrphan(TestOrphanBase):
|
||||
self.client.get(
|
||||
orphan_url,
|
||||
HTTP_ACCEPT='application/json'
|
||||
).content
|
||||
).content.decode('utf-8')
|
||||
)
|
||||
self.assertEqual(len(orphans), 3, u"Wrong # {}".format(orphans))
|
||||
location = course.location.replace(category='chapter', name='OrphanChapter')
|
||||
@@ -119,7 +119,7 @@ class TestOrphan(TestOrphanBase):
|
||||
self.client.delete(orphan_url)
|
||||
|
||||
orphans = json.loads(
|
||||
self.client.get(orphan_url, HTTP_ACCEPT='application/json').content
|
||||
self.client.get(orphan_url, HTTP_ACCEPT='application/json').content.decode('utf-8')
|
||||
)
|
||||
self.assertEqual(len(orphans), 0, u"Orphans not deleted {}".format(orphans))
|
||||
|
||||
|
||||
@@ -873,7 +873,7 @@ class TestGetTranscript(SharedModuleStoreTestCase):
|
||||
Verify that `get_transcript` function returns correct data when transcript is in content store.
|
||||
"""
|
||||
base_filename = 'video_101.srt'
|
||||
self.upload_file(self.create_srt_file(self.subs_srt), self.video.location, base_filename)
|
||||
self.upload_file(self.create_srt_file(self.subs_srt.encode('utf-8')), self.video.location, base_filename)
|
||||
self.create_transcript(subs_id, language, base_filename, youtube_id_1_0, html5_sources)
|
||||
content, file_name, mimetype = transcripts_utils.get_transcript(
|
||||
self.video,
|
||||
@@ -935,7 +935,7 @@ class TestGetTranscript(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Verify that `get_transcript` function returns correct exception when transcript content is empty.
|
||||
"""
|
||||
self.upload_file(self.create_srt_file(''), self.video.location, 'ur_video_101.srt')
|
||||
self.upload_file(self.create_srt_file(b''), self.video.location, 'ur_video_101.srt')
|
||||
self.create_transcript('', 'ur', 'ur_video_101.srt')
|
||||
|
||||
with self.assertRaises(NotFoundError) as no_content_exception:
|
||||
|
||||
@@ -6,7 +6,7 @@ from __future__ import absolute_import
|
||||
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from contentstore.utils import delete_course, reverse_url
|
||||
from courseware.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ CONTAINER_TEMPLATES = [
|
||||
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
|
||||
"add-xblock-component-support-legend", "add-xblock-component-support-level", "add-xblock-component-menu-problem",
|
||||
"xblock-string-field-editor", "xblock-access-editor", "publish-xblock", "publish-history",
|
||||
"unit-outline", "container-message", "container-access", "license-selector",
|
||||
"unit-outline", "container-message", "container-access", "license-selector", "copy-clipboard-button",
|
||||
"edit-title-button",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ from contentstore.views.entrance_exam import create_entrance_exam, delete_entran
|
||||
from course_action_state.managers import CourseActionStateItemNotFoundError
|
||||
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
|
||||
from course_creators.views import add_user_with_status_unrequested, get_course_creator_status
|
||||
from course_modes.models import CourseMode
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
@@ -1045,7 +1046,12 @@ def settings_handler(request, course_key_string):
|
||||
|
||||
# see if the ORG of this course can be attributed to a defined configuration . In that case, the
|
||||
# course about page should be editable in Studio
|
||||
marketing_site_enabled = configuration_helpers.get_value_for_org(
|
||||
publisher_enabled = configuration_helpers.get_value_for_org(
|
||||
course_module.location.org,
|
||||
'ENABLE_PUBLISHER',
|
||||
settings.FEATURES.get('ENABLE_PUBLISHER', False)
|
||||
)
|
||||
marketing_enabled = configuration_helpers.get_value_for_org(
|
||||
course_module.location.org,
|
||||
'ENABLE_MKTG_SITE',
|
||||
settings.FEATURES.get('ENABLE_MKTG_SITE', False)
|
||||
@@ -1056,8 +1062,8 @@ def settings_handler(request, course_key_string):
|
||||
settings.FEATURES.get('ENABLE_EXTENDED_COURSE_DETAILS', False)
|
||||
)
|
||||
|
||||
about_page_editable = not marketing_site_enabled
|
||||
enrollment_end_editable = GlobalStaff().has_user(request.user) or not marketing_site_enabled
|
||||
about_page_editable = not publisher_enabled
|
||||
enrollment_end_editable = GlobalStaff().has_user(request.user) or not publisher_enabled
|
||||
short_description_editable = configuration_helpers.get_value_for_org(
|
||||
course_module.location.org,
|
||||
'EDITABLE_SHORT_DESCRIPTION',
|
||||
@@ -1066,6 +1072,10 @@ def settings_handler(request, course_key_string):
|
||||
sidebar_html_enabled = course_experience_waffle().is_enabled(ENABLE_COURSE_ABOUT_SIDEBAR_HTML)
|
||||
# self_paced_enabled = SelfPacedConfiguration.current().enabled
|
||||
|
||||
verified_mode = CourseMode.verified_mode_for_course(course_key)
|
||||
upgrade_deadline = (verified_mode and verified_mode.expiration_datetime and
|
||||
verified_mode.expiration_datetime.isoformat())
|
||||
|
||||
settings_context = {
|
||||
'context_course': course_module,
|
||||
'course_locator': course_key,
|
||||
@@ -1075,6 +1085,7 @@ def settings_handler(request, course_key_string):
|
||||
'video_thumbnail_image_url': course_image_url(course_module, 'video_thumbnail_image'),
|
||||
'details_url': reverse_course_url('settings_handler', course_key),
|
||||
'about_page_editable': about_page_editable,
|
||||
'marketing_enabled': marketing_enabled,
|
||||
'short_description_editable': short_description_editable,
|
||||
'sidebar_html_enabled': sidebar_html_enabled,
|
||||
'upload_asset_url': upload_asset_url,
|
||||
@@ -1086,7 +1097,8 @@ def settings_handler(request, course_key_string):
|
||||
'enrollment_end_editable': enrollment_end_editable,
|
||||
'is_prerequisite_courses_enabled': is_prerequisite_courses_enabled(),
|
||||
'is_entrance_exams_enabled': is_entrance_exams_enabled(),
|
||||
'enable_extended_course_details': enable_extended_course_details
|
||||
'enable_extended_course_details': enable_extended_course_details,
|
||||
'upgrade_deadline': upgrade_deadline,
|
||||
}
|
||||
if is_prerequisite_courses_enabled():
|
||||
courses, in_process_course_actions = get_courses_accessible_to_user(request)
|
||||
@@ -1293,11 +1305,18 @@ def advanced_settings_handler(request, course_key_string):
|
||||
with modulestore().bulk_operations(course_key):
|
||||
course_module = get_course_and_check_access(course_key, request.user)
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
|
||||
publisher_enabled = configuration_helpers.get_value_for_org(
|
||||
course_module.location.org,
|
||||
'ENABLE_PUBLISHER',
|
||||
settings.FEATURES.get('ENABLE_PUBLISHER', False)
|
||||
)
|
||||
|
||||
return render_to_response('settings_advanced.html', {
|
||||
'context_course': course_module,
|
||||
'advanced_dict': CourseMetadata.fetch(course_module),
|
||||
'advanced_settings_url': reverse_course_url('advanced_settings_handler', course_key)
|
||||
'advanced_settings_url': reverse_course_url('advanced_settings_handler', course_key),
|
||||
'publisher_enabled': publisher_enabled,
|
||||
|
||||
})
|
||||
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
|
||||
if request.method == 'GET':
|
||||
@@ -1350,6 +1369,8 @@ def validate_textbooks_json(text):
|
||||
"""
|
||||
Validate the given text as representing a single PDF textbook
|
||||
"""
|
||||
if isinstance(text, (bytes, bytearray)): # data appears as bytes
|
||||
text = text.decode('utf-8')
|
||||
try:
|
||||
textbooks = json.loads(text)
|
||||
except ValueError:
|
||||
@@ -1370,6 +1391,8 @@ def validate_textbook_json(textbook):
|
||||
"""
|
||||
Validate the given text as representing a list of PDF textbooks
|
||||
"""
|
||||
if isinstance(textbook, (bytes, bytearray)): # data appears as bytes
|
||||
textbook = textbook.decode('utf-8')
|
||||
if isinstance(textbook, six.string_types):
|
||||
try:
|
||||
textbook = json.loads(textbook)
|
||||
|
||||
@@ -92,7 +92,7 @@ def hash_resource(resource):
|
||||
Hash a :class:`web_fragments.fragment.FragmentResource`.
|
||||
"""
|
||||
md5 = hashlib.md5()
|
||||
md5.update(repr(resource))
|
||||
md5.update(repr(resource).encode('utf-8'))
|
||||
return md5.hexdigest()
|
||||
|
||||
|
||||
@@ -416,7 +416,7 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
|
||||
return JsonResponse({
|
||||
'html': fragment.content,
|
||||
'resources': hashed_resources.items()
|
||||
'resources': list(hashed_resources.items())
|
||||
})
|
||||
|
||||
else:
|
||||
|
||||
@@ -181,7 +181,10 @@ class CertificatesBaseTestCase(object):
|
||||
with self.assertRaises(Exception) as context:
|
||||
CertificateManager.validate(json_data_1)
|
||||
|
||||
self.assertIn("Unsupported certificate schema version: 100. Expected version: 1.", context.exception)
|
||||
self.assertIn(
|
||||
"Unsupported certificate schema version: 100. Expected version: 1.",
|
||||
str(context.exception)
|
||||
)
|
||||
|
||||
#Test certificate name is missing
|
||||
json_data_2 = {
|
||||
@@ -192,7 +195,7 @@ class CertificatesBaseTestCase(object):
|
||||
with self.assertRaises(Exception) as context:
|
||||
CertificateManager.validate(json_data_2)
|
||||
|
||||
self.assertIn('must have name of the certificate', context.exception)
|
||||
self.assertIn('must have name of the certificate', str(context.exception))
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
|
||||
@@ -573,8 +573,11 @@ class ExportTestCase(CourseTestCase):
|
||||
output_url = result['ExportOutput']
|
||||
resp = self.client.get(output_url)
|
||||
self._verify_export_succeeded(resp)
|
||||
resp_content = b''
|
||||
for item in resp.streaming_content:
|
||||
resp_content += item
|
||||
|
||||
buff = StringIO("".join(resp.streaming_content))
|
||||
buff = six.BytesIO(resp_content)
|
||||
return tarfile.open(fileobj=buff)
|
||||
|
||||
def _verify_export_succeeded(self, resp):
|
||||
|
||||
@@ -328,7 +328,7 @@ class UnitTestLibraries(CourseTestCase):
|
||||
response = self.client.get(manage_users_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# extra_user has not been assigned to the library so should not show up in the list:
|
||||
self.assertNotIn(binary_type(extra_user.username), response.content)
|
||||
self.assertNotIn(extra_user.username, response.content.decode('utf-8'))
|
||||
|
||||
# Now add extra_user to the library:
|
||||
user_details_url = reverse_course_url(
|
||||
@@ -341,4 +341,4 @@ class UnitTestLibraries(CourseTestCase):
|
||||
# Now extra_user should apear in the list:
|
||||
response = self.client.get(manage_users_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(binary_type(extra_user.username), response.content)
|
||||
self.assertIn(extra_user.username, response.content.decode('utf-8'))
|
||||
|
||||
@@ -71,7 +71,7 @@ class TabsPageTests(CourseTestCase):
|
||||
|
||||
resp = self.client.get_html(self.url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn('course-nav-list', resp.content)
|
||||
self.assertIn('course-nav-list', resp.content.decode('utf-8'))
|
||||
|
||||
def test_reorder_tabs(self):
|
||||
"""Test re-ordering of tabs"""
|
||||
@@ -89,7 +89,7 @@ class TabsPageTests(CourseTestCase):
|
||||
|
||||
# remove the middle tab
|
||||
# (the code needs to handle the case where tabs requested for re-ordering is a subset of the tabs in the course)
|
||||
removed_tab = tab_ids.pop(num_orig_tabs / 2)
|
||||
removed_tab = tab_ids.pop(num_orig_tabs // 2)
|
||||
self.assertEqual(len(tab_ids), num_orig_tabs - 1)
|
||||
|
||||
# post the request
|
||||
@@ -214,16 +214,16 @@ class PrimitiveTabEdit(ModuleStoreTestCase):
|
||||
def test_insert(self):
|
||||
"""Test primitive tab insertion."""
|
||||
course = CourseFactory.create()
|
||||
tabs.primitive_insert(course, 2, 'notes', 'aname')
|
||||
self.assertEquals(course.tabs[2], {'type': 'notes', 'name': 'aname'})
|
||||
tabs.primitive_insert(course, 2, 'pdf_textbooks', 'aname')
|
||||
self.assertEquals(course.tabs[2], {'type': 'pdf_textbooks', 'name': 'aname'})
|
||||
with self.assertRaises(ValueError):
|
||||
tabs.primitive_insert(course, 0, 'notes', 'aname')
|
||||
tabs.primitive_insert(course, 0, 'pdf_textbooks', 'aname')
|
||||
with self.assertRaises(ValueError):
|
||||
tabs.primitive_insert(course, 3, 'static_tab', 'aname')
|
||||
|
||||
def test_save(self):
|
||||
"""Test course saving."""
|
||||
course = CourseFactory.create()
|
||||
tabs.primitive_insert(course, 3, 'notes', 'aname')
|
||||
tabs.primitive_insert(course, 3, 'pdf_textbooks', 'aname')
|
||||
course2 = modulestore().get_course(course.id)
|
||||
self.assertEquals(course2.tabs[3], {'type': 'notes', 'name': 'aname'})
|
||||
self.assertEquals(course2.tabs[3], {'type': 'pdf_textbooks', 'name': 'aname'})
|
||||
|
||||
@@ -258,7 +258,7 @@ class TextbookDetailTestCase(CourseTestCase):
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
resp2 = self.client.get(url)
|
||||
self.assertEqual(resp2.status_code, 200)
|
||||
compare = json.loads(resp2.content)
|
||||
compare = json.loads(resp2.content.decode('utf-8'))
|
||||
self.assertEqual(compare, textbook)
|
||||
self.reload_course()
|
||||
self.assertEqual(
|
||||
@@ -281,7 +281,7 @@ class TextbookDetailTestCase(CourseTestCase):
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
resp2 = self.client.get(self.url2)
|
||||
self.assertEqual(resp2.status_code, 200)
|
||||
compare = json.loads(resp2.content)
|
||||
compare = json.loads(resp2.content.decode('utf-8'))
|
||||
self.assertEqual(compare, replacement)
|
||||
course = self.store.get_item(self.course.location)
|
||||
self.assertEqual(
|
||||
|
||||
@@ -160,7 +160,7 @@ class TestUploadTranscripts(BaseTranscripts):
|
||||
super(TestUploadTranscripts, self).setUp()
|
||||
self.contents = {
|
||||
'good': SRT_TRANSCRIPT_CONTENT,
|
||||
'bad': 'Some BAD data',
|
||||
'bad': b'Some BAD data',
|
||||
}
|
||||
# Create temporary transcript files
|
||||
self.good_srt_file = self.create_transcript_file(content=self.contents['good'], suffix='.srt')
|
||||
@@ -186,13 +186,14 @@ class TestUploadTranscripts(BaseTranscripts):
|
||||
Setup a transcript file with suffix and content.
|
||||
"""
|
||||
transcript_file = tempfile.NamedTemporaryFile(suffix=suffix)
|
||||
wrapped_content = textwrap.dedent(content)
|
||||
wrapped_content = textwrap.dedent(content.decode('utf-8'))
|
||||
if include_bom:
|
||||
wrapped_content = wrapped_content.encode('utf-8-sig')
|
||||
# Verify that ufeff(BOM) character is in content.
|
||||
self.assertIn(BOM_UTF8, wrapped_content)
|
||||
|
||||
transcript_file.write(wrapped_content)
|
||||
transcript_file.write(wrapped_content)
|
||||
else:
|
||||
transcript_file.write(wrapped_content.encode('utf-8'))
|
||||
transcript_file.seek(0)
|
||||
|
||||
return transcript_file
|
||||
|
||||
@@ -325,11 +325,11 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertRegexpMatches(response["Content-Type"], "^text/html(;.*)?$")
|
||||
self.assertIn(_get_default_video_image_url(), response.content)
|
||||
self.assertIn(_get_default_video_image_url(), response.content.decode('utf-8'))
|
||||
# Crude check for presence of data in returned HTML
|
||||
for video in self.previous_uploads:
|
||||
self.assertIn(video["edx_video_id"], response.content)
|
||||
self.assertNotIn('video_upload_pagination', response.content)
|
||||
self.assertIn(video["edx_video_id"], response.content.decode('utf-8'))
|
||||
self.assertNotIn('video_upload_pagination', response.content.decode('utf-8'))
|
||||
|
||||
@override_waffle_flag(ENABLE_VIDEO_UPLOAD_PAGINATION, active=True)
|
||||
def test_get_html_paginated(self):
|
||||
@@ -338,7 +338,7 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
"""
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('video_upload_pagination', response.content)
|
||||
self.assertIn('video_upload_pagination', response.content.decode('utf-8'))
|
||||
|
||||
def test_post_non_json(self):
|
||||
response = self.client.post(self.url, {"files": []})
|
||||
@@ -782,7 +782,7 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
|
||||
# Verify that course video button is present in the response if videos transcript feature is enabled.
|
||||
self.assertEqual(
|
||||
'<button class="button course-video-settings-button">' in response.content,
|
||||
'<button class="button course-video-settings-button">' in response.content.decode('utf-8'),
|
||||
is_video_transcript_enabled
|
||||
)
|
||||
|
||||
@@ -810,7 +810,7 @@ class VideoImageTestCase(VideoUploadTestBase, CourseTestCase):
|
||||
uploaded image url
|
||||
"""
|
||||
self.assertEqual(upload_response.status_code, 200)
|
||||
response = json.loads(upload_response.content)
|
||||
response = json.loads(upload_response.content.decode('utf-8'))
|
||||
val_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=edx_video_id)
|
||||
self.assertEqual(response['image_url'], val_image_url)
|
||||
|
||||
@@ -1182,7 +1182,7 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
|
||||
'preferred_languages': ['es', 'ur']
|
||||
},
|
||||
True,
|
||||
u"Invalid languages [u'es', u'ur'].",
|
||||
"Invalid languages ['es', 'ur'].",
|
||||
400
|
||||
),
|
||||
(
|
||||
@@ -1211,7 +1211,7 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
|
||||
'preferred_languages': ['es', 'ur']
|
||||
},
|
||||
True,
|
||||
u"Invalid languages [u'es', u'ur'].",
|
||||
"Invalid languages ['es', 'ur'].",
|
||||
400
|
||||
),
|
||||
(
|
||||
@@ -1222,7 +1222,7 @@ class TranscriptPreferencesTestCase(VideoUploadTestBase, CourseTestCase):
|
||||
'preferred_languages': ['es', 'ur']
|
||||
},
|
||||
True,
|
||||
u"Invalid languages [u'es', u'ur'].",
|
||||
"Invalid languages ['es', 'ur'].",
|
||||
400
|
||||
),
|
||||
# Success
|
||||
@@ -1417,7 +1417,7 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
response["Content-Disposition"],
|
||||
u"attachment; filename={course}_video_urls.csv".format(course=self.course.id.course)
|
||||
)
|
||||
response_reader = StringIO(response.content)
|
||||
response_reader = StringIO(response.content.decode('utf-8') if six.PY3 else response.content)
|
||||
reader = csv.DictReader(response_reader, dialect=csv.excel)
|
||||
self.assertEqual(
|
||||
reader.fieldnames,
|
||||
@@ -1430,17 +1430,21 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
self.assertEqual(len(rows), len(self.previous_uploads))
|
||||
for i, row in enumerate(rows):
|
||||
response_video = {
|
||||
key.decode("utf-8"): value.decode("utf-8") for key, value in row.items()
|
||||
key.decode("utf-8") if six.PY2 else key: value.decode("utf-8") if six.PY2 else value
|
||||
for key, value in row.items()
|
||||
}
|
||||
# Videos should be returned by creation date descending
|
||||
original_video = self.previous_uploads[-(i + 1)]
|
||||
self.assertEqual(response_video["Name"], original_video["client_video_id"])
|
||||
client_video_id = original_video["client_video_id"].encode('utf-8') if six.PY2 \
|
||||
else original_video["client_video_id"]
|
||||
self.assertEqual(response_video["Name"].encode('utf-8') if six.PY2
|
||||
else response_video["Name"], client_video_id)
|
||||
self.assertEqual(response_video["Duration"], str(original_video["duration"]))
|
||||
dateutil.parser.parse(response_video["Date Added"])
|
||||
self.assertEqual(response_video["Video ID"], original_video["edx_video_id"])
|
||||
self.assertEqual(response_video["Status"], convert_video_status(original_video))
|
||||
for profile in expected_profiles:
|
||||
response_profile_url = response_video[u"{} URL".format(profile)]
|
||||
response_profile_url = response_video["{} URL".format(profile)] # pylint: disable=unicode-format-string
|
||||
original_encoded_for_profile = next(
|
||||
(
|
||||
original_encoded
|
||||
@@ -1450,7 +1454,10 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
None
|
||||
)
|
||||
if original_encoded_for_profile:
|
||||
self.assertEqual(response_profile_url, original_encoded_for_profile["url"])
|
||||
original_encoded_for_profile_url = original_encoded_for_profile["url"].encode('utf-8') if six.PY2 \
|
||||
else original_encoded_for_profile["url"]
|
||||
self.assertEqual(response_profile_url.encode('utf-8') if six.PY2 else response_profile_url,
|
||||
original_encoded_for_profile_url)
|
||||
else:
|
||||
self.assertEqual(response_profile_url, "")
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ def validate_transcript_preferences(provider, cielo24_fidelity, cielo24_turnarou
|
||||
return error, preferences
|
||||
|
||||
if not preferred_languages or not set(preferred_languages) <= set(supported_languages.keys()):
|
||||
error = u'Invalid languages {}.'.format(preferred_languages)
|
||||
error = 'Invalid languages {}.'.format(preferred_languages) # pylint: disable=unicode-format-string
|
||||
return error, preferences
|
||||
|
||||
# Validated Cielo24 preferences
|
||||
@@ -330,7 +330,6 @@ def transcript_preferences_handler(request, course_key_string):
|
||||
is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course_key)
|
||||
if not is_video_transcript_enabled:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if request.method == 'POST':
|
||||
data = request.json
|
||||
provider = data.get('provider')
|
||||
@@ -340,7 +339,7 @@ def transcript_preferences_handler(request, course_key_string):
|
||||
cielo24_turnaround=data.get('cielo24_turnaround', ''),
|
||||
three_play_turnaround=data.get('three_play_turnaround', ''),
|
||||
video_source_language=data.get('video_source_language'),
|
||||
preferred_languages=data.get('preferred_languages', [])
|
||||
preferred_languages=list(map(str, data.get('preferred_languages', [])))
|
||||
)
|
||||
if error:
|
||||
response = JsonResponse({'error': error}, status=400)
|
||||
@@ -413,7 +412,7 @@ def video_encodings_download(request, course_key_string):
|
||||
]
|
||||
)
|
||||
return {
|
||||
key.encode("utf-8"): value.encode("utf-8")
|
||||
key.encode("utf-8") if six.PY2 else key: value.encode("utf-8") if six.PY2 else value
|
||||
for key, value in ret.items()
|
||||
}
|
||||
|
||||
@@ -429,7 +428,7 @@ def video_encodings_download(request, course_key_string):
|
||||
writer = csv.DictWriter(
|
||||
response,
|
||||
[
|
||||
col_name.encode("utf-8")
|
||||
col_name.encode("utf-8") if six.PY2 else col_name
|
||||
for col_name
|
||||
in [name_col, duration_col, added_col, video_id_col, status_col] + profile_cols
|
||||
],
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.db import models
|
||||
from django.db.models.signals import post_init, post_save
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# A signal that will be sent when users should be added or removed from the creator group
|
||||
@@ -20,6 +21,7 @@ send_admin_notification = Signal(providing_args=["user"])
|
||||
send_user_notification = Signal(providing_args=["user", "state"])
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseCreator(models.Model):
|
||||
"""
|
||||
Creates the database table model.
|
||||
@@ -47,7 +49,7 @@ class CourseCreator(models.Model):
|
||||
note = models.CharField(max_length=512, blank=True, help_text=_("Optional notes about this user (for example, "
|
||||
"why course creation access was denied)"))
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return u"{0} | {1} [{2}]".format(self.user, self.state, self.state_changed)
|
||||
|
||||
|
||||
|
||||
@@ -289,7 +289,7 @@ class TestAnnouncementsViews(MaintenanceViewTestCase):
|
||||
"""
|
||||
url = reverse("maintenance:announcement_index")
|
||||
response = self.client.get(url)
|
||||
self.assertIn('<div class="announcement-container">', response.content)
|
||||
self.assertIn('<div class="announcement-container">', response.content.decode('utf-8'))
|
||||
|
||||
def test_create(self):
|
||||
"""
|
||||
@@ -308,7 +308,7 @@ class TestAnnouncementsViews(MaintenanceViewTestCase):
|
||||
announcement.save()
|
||||
url = reverse("maintenance:announcement_edit", kwargs={"pk": announcement.pk})
|
||||
response = self.client.get(url)
|
||||
self.assertIn('<div class="wrapper-form announcement-container">', response.content)
|
||||
self.assertIn('<div class="wrapper-form announcement-container">', response.content.decode('utf-8'))
|
||||
self.client.post(url, {"content": "Test Edit Announcement", "active": True})
|
||||
announcement = Announcement.objects.get(pk=announcement.pk)
|
||||
self.assertEquals(announcement.content, "Test Edit Announcement")
|
||||
|
||||
@@ -284,4 +284,4 @@ def hash_grading_policy(grading_policy):
|
||||
separators=(',', ':'), # Remove spaces from separators for more compact representation
|
||||
sort_keys=True,
|
||||
)
|
||||
return b64encode(sha1(ordered_policy.encode("utf-8")).digest())
|
||||
return b64encode(sha1(ordered_policy.encode("utf-8")).digest()).decode('utf-8')
|
||||
|
||||
@@ -178,7 +178,8 @@ class CourseMetadata(object):
|
||||
'value': field.read_json(descriptor),
|
||||
'display_name': _(field.display_name),
|
||||
'help': field_help,
|
||||
'deprecated': field.runtime_options.get('deprecated', False)
|
||||
'deprecated': field.runtime_options.get('deprecated', False),
|
||||
'hide_on_enabled_publisher': field.runtime_options.get('hide_on_enabled_publisher', False)
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from __future__ import absolute_import
|
||||
import six
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.db.models import TextField
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
@@ -37,6 +38,7 @@ class StudioConfig(ConfigurationModel):
|
||||
|
||||
# TODO: Move CourseEditLTIFieldsEnabledFlag to LTI XBlock as a part of EDUCATOR-121
|
||||
# reference: https://openedx.atlassian.net/browse/EDUCATOR-121
|
||||
@python_2_unicode_compatible
|
||||
class CourseEditLTIFieldsEnabledFlag(ConfigurationModel):
|
||||
"""
|
||||
Enables the editing of "request username" and "request email" fields
|
||||
@@ -77,7 +79,7 @@ class CourseEditLTIFieldsEnabledFlag(ConfigurationModel):
|
||||
|
||||
return course_specific_config.enabled if course_specific_config else False
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
en = "Not "
|
||||
if self.enabled:
|
||||
en = ""
|
||||
|
||||
@@ -220,10 +220,20 @@ FEATURES = {
|
||||
# in sync with the one in lms/envs/common.py
|
||||
'ENABLE_EDXNOTES': False,
|
||||
|
||||
# Toggle to enable coordination with the Publisher tool (keep in sync with lms/envs/common.py)
|
||||
'ENABLE_PUBLISHER': False,
|
||||
|
||||
# Show a new field in "Advanced settings" that can store custom data about a
|
||||
# course and that can be read from themes
|
||||
'ENABLE_OTHER_COURSE_SETTINGS': False,
|
||||
|
||||
# Write new CSM history to the extended table.
|
||||
# This will eventually default to True and may be
|
||||
# removed since all installs should have the separate
|
||||
# extended history table. This is needed in the LMS and CMS
|
||||
# for migration consistency.
|
||||
'ENABLE_CSMH_EXTENDED': True,
|
||||
|
||||
# Enable support for content libraries. Note that content libraries are
|
||||
# only supported in courses using split mongo.
|
||||
'ENABLE_CONTENT_LIBRARIES': True,
|
||||
@@ -302,9 +312,6 @@ FEATURES = {
|
||||
|
||||
# Prevent auto auth from creating superusers or modifying existing users
|
||||
'RESTRICT_AUTOMATIC_AUTH': True,
|
||||
|
||||
# Set this to true to make API docs available at /api-docs/.
|
||||
'ENABLE_API_DOCS': False,
|
||||
}
|
||||
|
||||
ENABLE_JASMINE = False
|
||||
@@ -1291,7 +1298,8 @@ INSTALLED_APPS = [
|
||||
# by installed apps.
|
||||
'openedx.core.djangoapps.oauth_dispatch.apps.OAuthDispatchAppConfig',
|
||||
'oauth_provider',
|
||||
'courseware',
|
||||
'lms.djangoapps.courseware',
|
||||
'coursewarehistoryextended',
|
||||
'survey.apps.SurveyConfig',
|
||||
'lms.djangoapps.verify_student.apps.VerifyStudentConfig',
|
||||
'completion',
|
||||
@@ -1349,6 +1357,8 @@ INSTALLED_APPS = [
|
||||
'openedx.features.discounts',
|
||||
'experiments',
|
||||
|
||||
# so sample_task is available to celery workers
|
||||
'openedx.core.djangoapps.heartbeat',
|
||||
]
|
||||
|
||||
|
||||
@@ -1799,6 +1809,10 @@ COURSE_ABOUT_VISIBILITY_PERMISSION = 'see_exists'
|
||||
DEFAULT_COURSE_VISIBILITY_IN_CATALOG = "both"
|
||||
DEFAULT_MOBILE_AVAILABLE = False
|
||||
|
||||
|
||||
# How long to cache OpenAPI schemas and UI, in seconds.
|
||||
OPENAPI_CACHE_TIMEOUT = 0
|
||||
|
||||
################# Mobile URLS ##########################
|
||||
|
||||
# These are URLs to the app store for mobile.
|
||||
@@ -1999,6 +2013,11 @@ FACEBOOK_APP_ID = 'FACEBOOK_APP_ID'
|
||||
FACEBOOK_APP_SECRET = 'FACEBOOK_APP_SECRET'
|
||||
FACEBOOK_API_VERSION = 'v2.1'
|
||||
|
||||
############### Settings for django-fernet-fields ##################
|
||||
FERNET_KEYS = [
|
||||
'DUMMY KEY CHANGE BEFORE GOING TO PRODUCTION',
|
||||
]
|
||||
|
||||
### Proctoring configuration (redirct URLs and keys shared between systems) ####
|
||||
PROCTORING_BACKENDS = {
|
||||
'DEFAULT': 'null',
|
||||
|
||||
@@ -71,7 +71,7 @@ CELERY_ALWAYS_EAGER = True
|
||||
|
||||
################################ DEBUG TOOLBAR ################################
|
||||
|
||||
INSTALLED_APPS += ['debug_toolbar', 'debug_toolbar_mongo']
|
||||
INSTALLED_APPS += ['debug_toolbar']
|
||||
|
||||
MIDDLEWARE_CLASSES.append('debug_toolbar.middleware.DebugToolbarMiddleware')
|
||||
INTERNAL_IPS = ('127.0.0.1',)
|
||||
@@ -105,15 +105,6 @@ def should_show_debug_toolbar(request):
|
||||
return True
|
||||
|
||||
|
||||
# To see stacktraces for MongoDB queries, set this to True.
|
||||
# Stacktraces slow down page loads drastically (for pages with lots of queries).
|
||||
DEBUG_TOOLBAR_MONGO_STACKTRACES = False
|
||||
|
||||
|
||||
########################### API DOCS #################################
|
||||
|
||||
FEATURES['ENABLE_API_DOCS'] = True
|
||||
|
||||
################################ MILESTONES ################################
|
||||
FEATURES['MILESTONES_APP'] = True
|
||||
|
||||
@@ -191,6 +182,9 @@ from openedx.core.djangoapps.plugins import constants as plugin_constants, plugi
|
||||
|
||||
plugin_settings.add_plugins(__name__, plugin_constants.ProjectType.CMS, plugin_constants.SettingsType.DEVSTACK)
|
||||
|
||||
|
||||
OPENAPI_CACHE_TIMEOUT = 0
|
||||
|
||||
###############################################################################
|
||||
# See if the developer has any local overrides.
|
||||
if os.path.isfile(join(dirname(abspath(__file__)), 'private.py')):
|
||||
|
||||
@@ -116,8 +116,7 @@ EDX_PLATFORM_REVISION = ENV_TOKENS.get('EDX_PLATFORM_REVISION', EDX_PLATFORM_REV
|
||||
# STATIC_URL_BASE specifies the base url to use for static files
|
||||
STATIC_URL_BASE = ENV_TOKENS.get('STATIC_URL_BASE', None)
|
||||
if STATIC_URL_BASE:
|
||||
# collectstatic will fail if STATIC_URL is a unicode string
|
||||
STATIC_URL = STATIC_URL_BASE.encode('ascii')
|
||||
STATIC_URL = STATIC_URL_BASE
|
||||
if not STATIC_URL.endswith("/"):
|
||||
STATIC_URL += "/"
|
||||
STATIC_URL += 'studio/'
|
||||
@@ -136,6 +135,9 @@ DEFAULT_MOBILE_AVAILABLE = ENV_TOKENS.get(
|
||||
DEFAULT_MOBILE_AVAILABLE
|
||||
)
|
||||
|
||||
# How long to cache OpenAPI schemas and UI, in seconds.
|
||||
OPENAPI_CACHE_TIMEOUT = ENV_TOKENS.get('OPENAPI_CACHE_TIMEOUT', 60 * 60)
|
||||
|
||||
# MEDIA_ROOT specifies the directory where user-uploaded files are stored.
|
||||
MEDIA_ROOT = ENV_TOKENS.get('MEDIA_ROOT', MEDIA_ROOT)
|
||||
MEDIA_URL = ENV_TOKENS.get('MEDIA_URL', MEDIA_URL)
|
||||
@@ -576,6 +578,9 @@ COMPLETION_VIDEO_COMPLETE_PERCENTAGE = ENV_TOKENS.get(
|
||||
COMPLETION_VIDEO_COMPLETE_PERCENTAGE,
|
||||
)
|
||||
|
||||
############### Settings for django-fernet-fields ##################
|
||||
FERNET_KEYS = AUTH_TOKENS.get('FERNET_KEYS', FERNET_KEYS)
|
||||
|
||||
####################### Enterprise Settings ######################
|
||||
|
||||
# A default dictionary to be used for filtering out enterprise customer catalog.
|
||||
|
||||
@@ -308,3 +308,5 @@ derive_settings(__name__)
|
||||
SYSTEM_WIDE_ROLE_CLASSES = os.environ.get("SYSTEM_WIDE_ROLE_CLASSES", [])
|
||||
|
||||
DEFAULT_MOBILE_AVAILABLE = True
|
||||
|
||||
PROCTORING_SETTINGS = {}
|
||||
|
||||
@@ -4,8 +4,10 @@ Django Model for tags
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class TagCategories(models.Model):
|
||||
"""
|
||||
This model represents tag categories.
|
||||
@@ -21,7 +23,7 @@ class TagCategories(models.Model):
|
||||
verbose_name = "tag category"
|
||||
verbose_name_plural = "tag categories"
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return "[TagCategories] {}: {}".format(self.name, self.title)
|
||||
|
||||
def get_values(self):
|
||||
@@ -31,6 +33,7 @@ class TagCategories(models.Model):
|
||||
return [t.value for t in TagAvailableValues.objects.filter(category=self)]
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class TagAvailableValues(models.Model):
|
||||
"""
|
||||
This model represents available values for tags.
|
||||
@@ -45,5 +48,5 @@ class TagAvailableValues(models.Model):
|
||||
ordering = ('id',)
|
||||
verbose_name = "available tag value"
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return "[TagAvailableValues] {}: {}".format(self.category, self.value)
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
'draggabilly': 'js/vendor/draggabilly',
|
||||
'hls': 'common/js/vendor/hls',
|
||||
'lang_edx': 'js/src/lang_edx',
|
||||
'jquery_extend_patch': 'js/src/jquery_extend_patch',
|
||||
|
||||
// externally hosted files
|
||||
mathjax: 'https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js?config=TeX-MML-AM_HTMLorMML&delayStartupUntil=configured', // eslint-disable-line max-len
|
||||
@@ -345,8 +346,13 @@
|
||||
'rangeslider', 'share-annotator', 'richText-annotator', 'reply-annotator',
|
||||
'tags-annotator', 'flagging-annotator', 'grouping-annotator', 'diacritic-annotator',
|
||||
'openseadragon', 'jquery-Watch', 'catch', 'handlebars', 'URI']
|
||||
}
|
||||
},
|
||||
// end of annotation tool files
|
||||
|
||||
// patch for jquery's extend
|
||||
'jquery_extend_patch': {
|
||||
deps: ['jquery']
|
||||
}
|
||||
}
|
||||
});
|
||||
}).call(this, require, define);
|
||||
|
||||
@@ -97,7 +97,10 @@ define([
|
||||
// general link management - new window/tab
|
||||
$('a[rel="external"]:not([title])')
|
||||
.attr('title', gettext('This link will open in a new browser window/tab'));
|
||||
$('a[rel="external"]').attr('target', '_blank');
|
||||
$('a[rel="external"]').attr({
|
||||
rel: 'noopener external',
|
||||
target: '_blank'
|
||||
});
|
||||
|
||||
// general link management - lean modal window
|
||||
$('a[rel="modal"]').attr('title', gettext('This link will open in a modal window')).leanModal({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// We can't convert this to an es6 module until all factories that use it have been converted out
|
||||
// of RequireJS
|
||||
define(['js/base', 'cms/js/main', 'js/src/logger', 'datepair', 'accessibility',
|
||||
'ieshim', 'tooltip_manager', 'lang_edx', 'js/models/course'],
|
||||
'ieshim', 'tooltip_manager', 'lang_edx', 'js/models/course', 'jquery_extend_patch'],
|
||||
function() {
|
||||
'use strict';
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ define([
|
||||
'jquery', 'js/models/settings/course_details', 'js/views/settings/main'
|
||||
], function($, CourseDetailsModel, MainView) {
|
||||
'use strict';
|
||||
return function(detailsUrl, showMinGradeWarning, showCertificateAvailableDate) {
|
||||
return function(detailsUrl, showMinGradeWarning, showCertificateAvailableDate, upgradeDeadline) {
|
||||
var model;
|
||||
// highlighting labels when fields are focused in
|
||||
$('form :input')
|
||||
@@ -16,6 +16,7 @@ define([
|
||||
model = new CourseDetailsModel();
|
||||
model.urlRoot = detailsUrl;
|
||||
model.showCertificateAvailableDate = showCertificateAvailableDate;
|
||||
model.set('upgrade_deadline', upgradeDeadline);
|
||||
model.fetch({
|
||||
success: function(model) {
|
||||
var editor = new MainView({
|
||||
|
||||
@@ -2,7 +2,7 @@ define([
|
||||
'jquery', 'gettext', 'js/models/settings/advanced', 'js/views/settings/advanced'
|
||||
], function($, gettext, AdvancedSettingsModel, AdvancedSettingsView) {
|
||||
'use strict';
|
||||
return function(advancedDict, advancedSettingsUrl) {
|
||||
return function(advancedDict, advancedSettingsUrl, publisherEnabled) {
|
||||
var advancedModel, editor;
|
||||
|
||||
$('form :input')
|
||||
@@ -17,6 +17,14 @@ define([
|
||||
advancedModel = new AdvancedSettingsModel(advancedDict, {parse: true});
|
||||
advancedModel.url = advancedSettingsUrl;
|
||||
|
||||
// set the hidden property to true on relevant fields if publisher is enabled
|
||||
if (publisherEnabled && advancedModel.attributes) {
|
||||
Object.keys(advancedModel.attributes).forEach(function(am) {
|
||||
var field = advancedModel.attributes[am];
|
||||
field.hidden = field.hide_on_enabled_publisher;
|
||||
});
|
||||
}
|
||||
|
||||
editor = new AdvancedSettingsView({
|
||||
el: $('.settings-advanced'),
|
||||
model: advancedModel
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('EditXBlockModal', function() {
|
||||
it('shows the correct title', function() {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
modal = showModal(requests, mockXBlockEditorHtml);
|
||||
expect(modal.$('.modal-window-title').text()).toBe('Editing: Component');
|
||||
expect(modal.$('.modal-window-title span.modal-button-title').text()).toBe('Editing: Component');
|
||||
});
|
||||
|
||||
it('does not show any editor mode buttons', function() {
|
||||
@@ -134,7 +134,7 @@ describe('EditXBlockModal', function() {
|
||||
it('shows the correct title', function() {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
modal = showModal(requests, mockXModuleEditorHtml);
|
||||
expect(modal.$('.modal-window-title').text()).toBe('Editing: Component');
|
||||
expect(modal.$('.modal-window-title span.modal-button-title').text()).toBe('Editing: Component');
|
||||
});
|
||||
|
||||
it('shows the correct default buttons', function() {
|
||||
|
||||
@@ -90,6 +90,7 @@ installEditTemplates = function(append) {
|
||||
// Add templates needed by the edit XBlock modal
|
||||
TemplateHelpers.installTemplate('edit-xblock-modal');
|
||||
TemplateHelpers.installTemplate('editor-mode-button');
|
||||
TemplateHelpers.installTemplate('edit-title-button');
|
||||
|
||||
// Add templates needed by the settings editor
|
||||
TemplateHelpers.installTemplate('metadata-editor');
|
||||
|
||||
@@ -29,7 +29,7 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'common/js/spec_hel
|
||||
|
||||
getModalTitle = function(modal) {
|
||||
var modalElement = getModalElement(modal);
|
||||
return modalElement.find('.modal-window-title').text();
|
||||
return modalElement.find('.modal-window-title span.modal-button-title').text();
|
||||
};
|
||||
|
||||
isShowingModal = function(modal) {
|
||||
|
||||
@@ -63,6 +63,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview'],
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// xss-lint: disable=javascript-jquery-html
|
||||
this.$el.html(this.modalTemplate({
|
||||
name: this.options.modalName,
|
||||
type: this.options.modalType,
|
||||
@@ -83,6 +84,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview'],
|
||||
|
||||
renderContents: function() {
|
||||
var contentHtml = this.getContentHtml();
|
||||
// xss-lint: disable=javascript-jquery-html
|
||||
this.$('.modal-content').html(contentHtml);
|
||||
},
|
||||
|
||||
@@ -146,6 +148,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview'],
|
||||
name: name,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
// xss-lint: disable=javascript-jquery-append
|
||||
this.getActionBar().find('ul').append(html);
|
||||
},
|
||||
|
||||
@@ -178,8 +181,8 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview'],
|
||||
modalWindow = this.$el.find(this.options.modalWindowClass);
|
||||
availableWidth = $(window).width();
|
||||
availableHeight = $(window).height();
|
||||
maxWidth = availableWidth * 0.80;
|
||||
maxHeight = availableHeight * 0.80;
|
||||
maxWidth = availableWidth * 0.98;
|
||||
maxHeight = availableHeight * 0.98;
|
||||
modalWidth = Math.min(modalWindow.outerWidth(), maxWidth);
|
||||
modalHeight = Math.min(modalWindow.outerHeight(), maxHeight);
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod
|
||||
var EditXBlockModal = BaseModal.extend({
|
||||
events: _.extend({}, BaseModal.prototype.events, {
|
||||
'click .action-save': 'save',
|
||||
'click .action-modes a': 'changeMode'
|
||||
'click .action-modes a': 'changeMode',
|
||||
'click .title-edit-button': 'clickTitleButton'
|
||||
}),
|
||||
|
||||
options: $.extend({}, BaseModal.prototype.options, {
|
||||
@@ -40,6 +41,7 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod
|
||||
this.xblockInfo = XBlockViewUtils.findXBlockInfo(xblockElement, rootXBlockInfo);
|
||||
this.options.modalType = this.xblockInfo.get('category');
|
||||
this.editOptions = options;
|
||||
|
||||
this.render();
|
||||
this.show();
|
||||
|
||||
@@ -68,6 +70,11 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod
|
||||
});
|
||||
},
|
||||
|
||||
createTitleEditor: function(title) {
|
||||
// xss-lint: disable=javascript-jquery-html
|
||||
this.$('.modal-window-title').html(this.loadTemplate('edit-title-button')({title: title}));
|
||||
},
|
||||
|
||||
onDisplayXBlock: function() {
|
||||
var editorView = this.editorView,
|
||||
title = this.getTitle(),
|
||||
@@ -84,7 +91,7 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod
|
||||
// Update the custom editor's title
|
||||
editorView.$('.component-name').text(title);
|
||||
} else {
|
||||
this.$('.modal-window-title').text(title);
|
||||
this.createTitleEditor(title);
|
||||
if (editorView.getDataEditor() && editorView.getMetadataEditor()) {
|
||||
this.addDefaultModes();
|
||||
// If the plugins content element exists, add a button to reveal it.
|
||||
@@ -103,8 +110,6 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod
|
||||
}
|
||||
this.getActionBar().show();
|
||||
}
|
||||
|
||||
// Resize the modal to fit the window
|
||||
this.resize();
|
||||
},
|
||||
|
||||
@@ -146,7 +151,6 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod
|
||||
},
|
||||
|
||||
changeMode: function(event) {
|
||||
this.removeCheatsheetVisibility();
|
||||
var $parent = $(event.target.parentElement),
|
||||
mode = $parent.data('mode');
|
||||
event.preventDefault();
|
||||
@@ -207,16 +211,30 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod
|
||||
}));
|
||||
},
|
||||
|
||||
removeCheatsheetVisibility: function() {
|
||||
var $cheatsheet = $('article.simple-editor-open-ended-cheatsheet');
|
||||
if ($cheatsheet.length === 0) {
|
||||
$cheatsheet = $('article.simple-editor-cheatsheet');
|
||||
}
|
||||
if ($cheatsheet.hasClass('shown')) {
|
||||
$cheatsheet.removeClass('shown');
|
||||
$('.modal-content').removeClass('cheatsheet-is-shown');
|
||||
}
|
||||
clickTitleButton: function(event) {
|
||||
var self = this,
|
||||
oldTitle = this.xblockInfo.get('display_name'),
|
||||
titleElt = this.$('.modal-window-title'),
|
||||
$input = $('<input type="text" size="40" />'),
|
||||
changeFunc = function(evt) {
|
||||
var newTitle = $(evt.target).val();
|
||||
if (oldTitle !== newTitle) {
|
||||
self.xblockInfo.set('display_name', newTitle);
|
||||
self.xblockInfo.save({metadata: {display_name: newTitle}});
|
||||
}
|
||||
self.createTitleEditor(self.getTitle());
|
||||
return true;
|
||||
};
|
||||
event.preventDefault();
|
||||
|
||||
$input.val(oldTitle);
|
||||
$input.change(changeFunc).blur(changeFunc);
|
||||
titleElt.html($input); // xss-lint: disable=javascript-jquery-html
|
||||
$input.focus().select();
|
||||
$(event.target).remove();
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return EditXBlockModal;
|
||||
|
||||
@@ -30,6 +30,7 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
|
||||
|
||||
view: 'container_preview',
|
||||
|
||||
|
||||
defaultViewClass: ContainerView,
|
||||
|
||||
// Overridable by subclasses-- determines whether the XBlock component
|
||||
|
||||
@@ -153,7 +153,7 @@ define(['js/views/validation',
|
||||
var newKeyId = _.uniqueId('policy_key_'),
|
||||
newEle = this.template({key: key, display_name: model.display_name, help: model.help,
|
||||
value: JSON.stringify(model.value, null, 4), deprecated: model.deprecated,
|
||||
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
|
||||
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_'), hidden: model.hidden});
|
||||
|
||||
this.fieldToSelectorMap[key] = newKeyId;
|
||||
this.selectorToField[newKeyId] = key;
|
||||
|
||||
@@ -83,6 +83,7 @@ define(['js/views/validation', 'codemirror', 'underscore', 'jquery', 'jquery.ui'
|
||||
DateUtils.setupDatePicker('certificate_available_date', this);
|
||||
DateUtils.setupDatePicker('enrollment_start', this);
|
||||
DateUtils.setupDatePicker('enrollment_end', this);
|
||||
DateUtils.setupDatePicker('upgrade_deadline', this);
|
||||
|
||||
this.$el.find('#' + this.fieldToSelectorMap.overview).val(this.model.get('overview'));
|
||||
this.codeMirrorize(null, $('#course-overview')[0]);
|
||||
@@ -162,6 +163,7 @@ define(['js/views/validation', 'codemirror', 'underscore', 'jquery', 'jquery.ui'
|
||||
end_date: 'course-end',
|
||||
enrollment_start: 'enrollment-start',
|
||||
enrollment_end: 'enrollment-end',
|
||||
upgrade_deadline: 'upgrade-deadline',
|
||||
certificate_available_date: 'certificate-available',
|
||||
overview: 'course-overview',
|
||||
title: 'course-title',
|
||||
|
||||
@@ -219,6 +219,11 @@
|
||||
color: $blue-d4;
|
||||
}
|
||||
}
|
||||
.clipboard-button {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +253,7 @@
|
||||
// large modals - component editors and interactives
|
||||
// ------------------------
|
||||
.modal-lg {
|
||||
width: 70%;
|
||||
width: 95%;
|
||||
min-width: ($baseline*27.5);
|
||||
height: auto;
|
||||
|
||||
@@ -266,7 +271,7 @@
|
||||
}
|
||||
|
||||
.editor-modes {
|
||||
width: 48%;
|
||||
width: 49%;
|
||||
display: inline-block;
|
||||
|
||||
@include text-align(right);
|
||||
@@ -378,7 +383,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MODAL TYPE: component - video modal (includes special overrides for xblock-related editing view)
|
||||
.modal-lg.modal-type-video {
|
||||
.modal-content {
|
||||
|
||||
@@ -144,7 +144,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<p>${_("Confirm that you have properly configured content in each of your experiment groups.")}</p>
|
||||
</div>
|
||||
<div class="bit external-help">
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about component containers")}</a>
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about component containers")}</a>
|
||||
</div>
|
||||
% elif is_unit_page:
|
||||
<div id="publish-unit"></div>
|
||||
|
||||
@@ -148,7 +148,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
</div>
|
||||
|
||||
<div class="bit external-help">
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about Course Re-runs")}</a>
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about Course Re-runs")}</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ from django.core.urlresolvers import reverse
|
||||
<div style="width: 50%" class="status-studio-frontend">
|
||||
% endif
|
||||
<%static:studiofrontend entry="courseOutlineHealthCheck">
|
||||
<%
|
||||
<%
|
||||
course_key = context_course.id
|
||||
%>
|
||||
{
|
||||
@@ -190,7 +190,7 @@ from django.core.urlresolvers import reverse
|
||||
"settings": ${reverse('settings_handler', kwargs={'course_key_string': six.text_type(course_key)})| n, dump_js_escaped_json}
|
||||
}
|
||||
}
|
||||
</%static:studiofrontend>
|
||||
</%static:studiofrontend>
|
||||
</div>
|
||||
<div class="status-highlights-enabled"></div>
|
||||
</div>
|
||||
@@ -220,14 +220,14 @@ from django.core.urlresolvers import reverse
|
||||
<h3 class="title-3">${_("Reorganizing your course")}</h3>
|
||||
<p>${_("Drag sections, subsections, and units to new locations in the outline.")}</p>
|
||||
<div class="external-help">
|
||||
<a href="${get_online_help_info('outline')['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about the course outline")}</a>
|
||||
<a href="${get_online_help_info('outline')['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about the course outline")}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Setting release dates and grading policies")}</h3>
|
||||
<p>${_("Select the Configure icon for a section or subsection to set its release date. When you configure a subsection, you can also set the grading policy and due date.")}</p>
|
||||
<div class="external-help">
|
||||
<a href="${get_online_help_info('grading')['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about grading policy settings")}</a>
|
||||
<a href="${get_online_help_info('grading')['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about grading policy settings")}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bit">
|
||||
@@ -236,7 +236,7 @@ from django.core.urlresolvers import reverse
|
||||
<p>${Text(_("To make a section, subsection, or unit unavailable to learners, select the Configure icon for that level, then select the appropriate {em_start}Hide{em_end} option. Grades for hidden sections, subsections, and units are not included in grade calculations.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p>
|
||||
<p>${Text(_("To hide the content of a subsection from learners after the subsection due date has passed, select the Configure icon for a subsection, then select {em_start}Hide content after due date{em_end}. Grades for the subsection remain included in grade calculations.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p>
|
||||
<div class="external-help">
|
||||
<a href="${get_online_help_info('visibility')['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about content visibility settings")}</a>
|
||||
<a href="${get_online_help_info('visibility')['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about content visibility settings")}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ else:
|
||||
<p>${_("Use an archive program to extract the data from the .tar.gz file. Extracted data includes the library.xml file, as well as subfolders that contain library content.")}</p>
|
||||
</div>
|
||||
<div class="bit external-help">
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about exporting a library")}</a>
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about exporting a library")}</a>
|
||||
</div>
|
||||
</aside>
|
||||
%else:
|
||||
@@ -269,7 +269,7 @@ else:
|
||||
<p>${_("Use an archive program to extract the data from the .tar.gz file. Extracted data includes the course.xml file, as well as subfolders that contain course content.")}</p>
|
||||
</div>
|
||||
<div class="bit external-help">
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about exporting a course")}</a>
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about exporting a course")}</a>
|
||||
</div>
|
||||
</aside>
|
||||
%endif
|
||||
|
||||
@@ -86,7 +86,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<p>${_("Enrollment track groups allow you to offer different course content to learners in each enrollment track. Learners enrolled in each enrollment track in your course are automatically included in the corresponding enrollment track group.")}</p>
|
||||
<p>${_("On unit pages in the course outline, you can restrict access to components to learners based on their enrollment track.")}</p>
|
||||
<p>${_("You cannot edit enrollment track groups, but you can expand each group to view details of the course content that is designated for learners in the group.")}</p>
|
||||
<p><a href="${get_online_help_info(enrollment_track_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
|
||||
<p><a href="${get_online_help_info(enrollment_track_help_token())['doc_url']} rel="noopener" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
@@ -96,7 +96,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<p>${_("If you have cohorts enabled in your course, you can use content groups to create cohort-specific courseware. In other words, you can customize the content that particular cohorts see in your course.")}</p>
|
||||
<p>${_("Each content group that you create can be associated with one or more cohorts. In addition to making course content available to all learners, you can restrict access to some content to learners in specific content groups. Only learners in the cohorts that are associated with the specified content groups see the additional content.")}</p>
|
||||
<p>${Text(_("Click {em_start}New content group{em_end} to add a new content group. To edit the name of a content group, hover over its box and click {em_start}Edit{em_end}. You can delete a content group only if it is not in use by a unit. To delete a content group, hover over its box and click the delete icon.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p>
|
||||
<p><a href="${get_online_help_info(content_groups_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
|
||||
<p><a href="${get_online_help_info(content_groups_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
% if should_show_experiment_groups:
|
||||
@@ -105,7 +105,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<h3 class="title-3">${_("Experiment Group Configurations")}</h3>
|
||||
<p>${_("Use experiment group configurations if you are conducting content experiments, also known as A/B testing, in your course. Experiment group configurations define how many groups of learners are in a content experiment. When you create a content experiment for a course, you select the group configuration to use.")}</p>
|
||||
<p>${Text(_("Click {em_start}New Group Configuration{em_end} to add a new configuration. To edit a configuration, hover over its box and click {em_start}Edit{em_end}. You can delete a group configuration only if it is not in use in an experiment. To delete a configuration, hover over its box and click the delete icon.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p>
|
||||
<p><a href="${get_online_help_info(experiment_group_configurations_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
|
||||
<p><a href="${get_online_help_info(experiment_group_configurations_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn More")}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
@@ -213,7 +213,7 @@ else:
|
||||
<p>${_("If you change and import a library that is referenced by randomized content blocks in one or more courses, those courses do not automatically use the updated content. You must manually refresh the randomized content blocks to bring them up to date with the latest library content.")}</p>
|
||||
</div>
|
||||
<div class="bit external-help">
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about importing a library")}</a>
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about importing a library")}</a>
|
||||
</div>
|
||||
</aside>
|
||||
%else:
|
||||
@@ -245,7 +245,7 @@ else:
|
||||
<p>${_("If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any Problem components, the student data associated with those Problem components may be lost. This data includes students' problem scores.")}</p>
|
||||
</div>
|
||||
<div class="bit external-help">
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about importing a course")}</a>
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about importing a course")}</a>
|
||||
</div>
|
||||
</aside>
|
||||
%endif
|
||||
|
||||
@@ -519,7 +519,7 @@ from openedx.core.djangolib.js_utils import (
|
||||
<ol class="list-actions">
|
||||
<li class="action-item">
|
||||
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank">${_("Getting Started with {studio_name}").format(studio_name=settings.STUDIO_NAME)}</a>
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank">${_("Getting Started with {studio_name}").format(studio_name=settings.STUDIO_NAME)}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<% if (support_legend.show_legend) { %>
|
||||
<span class="support-documentation">
|
||||
<a class="support-documentation-link"
|
||||
href="https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/create_exercises_and_tools.html#levels-of-support-for-tools" target="_blank">
|
||||
href="https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/create_exercises_and_tools.html#levels-of-support-for-tools" rel="noopener" target="_blank">
|
||||
<%- support_legend.documentation_label %>
|
||||
</a>
|
||||
<span class="support-documentation-level">
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<li class="field-group course-advanced-policy-list-item <%= deprecated ? 'is-deprecated' : '' %>">
|
||||
<div class="field is-not-editable text key" id="<%= key %>">
|
||||
<h3 class="title" id="<%= keyUniqueId %>"><%= display_name %></h3>
|
||||
</div>
|
||||
<% if (!hidden) { %>
|
||||
<li class="field-group course-advanced-policy-list-item <%- deprecated ? 'is-deprecated' : '' %>">
|
||||
<div class="field is-not-editable text key" id="<%- key %>">
|
||||
<h3 class="title" id="<%- keyUniqueId %>"><%- display_name %></h3>
|
||||
</div>
|
||||
|
||||
<div class="field text value">
|
||||
<label class="sr" for="<%= valueUniqueId %>"><%= display_name %></label>
|
||||
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
|
||||
<span class="tip tip-stacked"><%= help %></span>
|
||||
</div>
|
||||
<% if (deprecated) { %>
|
||||
<span class="status"><%= gettext("Deprecated") %></span>
|
||||
<% } %>
|
||||
</li>
|
||||
<div class="field text value">
|
||||
<label class="sr" for="<%- valueUniqueId %>"><%- display_name %></label>
|
||||
<textarea class="json text" id="<%- valueUniqueId %>"><%- value %></textarea>
|
||||
<span class="tip tip-stacked"><%- help %></span>``
|
||||
</div>
|
||||
<% if (deprecated) { %>
|
||||
<span class="status"><%- gettext("Deprecated") %></span>
|
||||
<% } %>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
5
cms/templates/js/copy-clipboard-button.underscore
Normal file
5
cms/templates/js/copy-clipboard-button.underscore
Normal file
@@ -0,0 +1,5 @@
|
||||
<a href="#" class="button action-button clipboard-button" data-tooltip="<%- gettext('Copy Component Location') %>">
|
||||
<i class="fa fa-link"></i>
|
||||
<%- gettext('Copy Component Location') %>
|
||||
<input class="sr" value="<%- location %>"/>
|
||||
</a>
|
||||
@@ -8,5 +8,5 @@
|
||||
<% } else { %>
|
||||
<button class="status-highlights-enabled-value button" aria-labelledby="highlights-enabled-label"><%- gettext('Enable Now') %></button>
|
||||
<% } %>
|
||||
<a class="status-highlights-enabled-info" href="<%- highlights_doc_url %>" target="_blank">Learn more</a>
|
||||
<a class="status-highlights-enabled-info" href="<%- highlights_doc_url %>" rel="noopener" target="_blank">Learn more</a>
|
||||
</div>
|
||||
|
||||
1
cms/templates/js/edit-title-button.underscore
Normal file
1
cms/templates/js/edit-title-button.underscore
Normal file
@@ -0,0 +1 @@
|
||||
<span class="modal-button-title"><%- title %></span> <button data-tooltip="<%- gettext('Edit Title') %>" class="btn-default action-edit title-edit-button"><span class="icon fa fa-pencil" aria-hidden="true"></span><span class="sr"> <%- gettext('Edit Title') %></span></button>
|
||||
@@ -15,7 +15,7 @@
|
||||
),
|
||||
{
|
||||
linkStart: edx.HtmlUtils.interpolateHtml(
|
||||
edx.HtmlUtils.HTML('<a href="{highlightsDocUrl}" target="_blank">'),
|
||||
edx.HtmlUtils.HTML('<a href="{highlightsDocUrl}" rel="noopener" target="_blank">'),
|
||||
{highlightsDocUrl: xblockInfo.attributes.highlights_doc_url}
|
||||
),
|
||||
linkEnd: edx.HtmlUtils.HTML('</a>')
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<%- gettext("License Type") %>
|
||||
</h3>
|
||||
<ul class="license-types">
|
||||
<% var link_start_tpl = '<a href="{url}" target="_blank">'; %>
|
||||
<% var link_start_tpl = '<a href="{url}" rel="noopener" target="_blank">'; %>
|
||||
<% _.each(licenseInfo, function(license, licenseType) { %>
|
||||
<li class="license-type" data-license="<%- licenseType %>">
|
||||
<button name="license-<%- licenseType %>"
|
||||
|
||||
@@ -3,10 +3,4 @@
|
||||
<li class="field comp-setting-entry metadata_entry">
|
||||
</li>
|
||||
<% }) %>
|
||||
<li class="field comp-setting-entry metadata_entry">
|
||||
<div class="wrapper-comp-setting-text">
|
||||
<label class="label setting-label"><%- gettext("Component Location ID") %></label>
|
||||
<span class="setting-text"><%- locator %></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -98,7 +98,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
</div>
|
||||
% endif
|
||||
<div class="bit external-help">
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about content libraries")}</a>
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about content libraries")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
import urllib
|
||||
from django.utils.translation import ugettext as _
|
||||
from contentstore import utils
|
||||
from openedx.core.djangoapps.certificates.api import can_show_certificate_available_date_field
|
||||
@@ -14,6 +13,7 @@
|
||||
dump_js_escaped_json, js_escaped_string
|
||||
)
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from six.moves.urllib import parse as urllib
|
||||
%>
|
||||
|
||||
<%block name="header_extras">
|
||||
@@ -39,7 +39,8 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
SettingsFactory(
|
||||
"${details_url | n, js_escaped_string}",
|
||||
${show_min_grade_warning | n, dump_js_escaped_json},
|
||||
${can_show_certificate_available_date_field(context_course) | n, dump_js_escaped_json}
|
||||
${can_show_certificate_available_date_field(context_course) | n, dump_js_escaped_json},
|
||||
"${upgrade_deadline | n, js_escaped_string}"
|
||||
);
|
||||
});
|
||||
</%block>
|
||||
@@ -84,7 +85,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
% if about_page_editable:
|
||||
% if not marketing_enabled:
|
||||
<div class="note note-promotion note-promotion-courseURL has-actions">
|
||||
<h3 class="title">${_("Course Summary Page")} <span class="tip">${_("(for student enrollment and access)")}</span></h3>
|
||||
<div class="copy">
|
||||
@@ -114,7 +115,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if not about_page_editable:
|
||||
% if marketing_enabled:
|
||||
<div class="notice notice-incontext notice-workflow">
|
||||
<h3 class="title">${_("Promoting Your Course with {platform_name}").format(platform_name=settings.PLATFORM_NAME)}</h3>
|
||||
<div class="copy">
|
||||
@@ -122,6 +123,9 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
'Your course summary page will not be viewable until your course '
|
||||
'has been announced. To provide content for the page and preview '
|
||||
'it, follow the instructions provided by your Program Manager.')}
|
||||
${_(
|
||||
'Please note that changes here may take up to a business day to '
|
||||
'appear on your course summary page.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,9 +207,9 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
</ol>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
|
||||
<hr class="divide" />
|
||||
|
||||
|
||||
<div id="schedule" class="group-settings schedule">
|
||||
<header>
|
||||
<h2 class="title-2">${_('Course Schedule')}</h2>
|
||||
@@ -291,6 +295,27 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
% if upgrade_deadline:
|
||||
<ol class="list-input">
|
||||
<li class="field-group field-group-upgrade-deadline" id="upgrade-deadline">
|
||||
<div class="field date is-not-editable" id="field-upgrade-deadline-date">
|
||||
<label for="course-upgrade-deadline-date">${_("Upgrade Deadline Date")}</label>
|
||||
<input type="text" class="date upgrade-deadline" id="course-upgrade-deadline-date" placeholder="MM/DD/YYYY" autocomplete="off" readonly aria-readonly="true" />
|
||||
<span class="tip tip-stacked">
|
||||
${_("Last day students can upgrade to a verified enrollment.")}
|
||||
${_("Contact your edX partner manager to update these settings.")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="field time is-not-editable" id="field-upgrade-deadline-time">
|
||||
<label for="course-upgrade-deadline-time">${_("Upgrade Deadline Time")}</label>
|
||||
<input type="text" class="time upgrade-deadline" id="course-upgrade-deadline-time" placeholder="HH:MM" autocomplete="off" readonly aria-readonly="true" />
|
||||
<span class="tip tip-stacked timezone">${_("(UTC)")}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% if about_page_editable:
|
||||
@@ -316,10 +341,14 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
|
||||
<hr class="divide" />
|
||||
<div class="group-settings marketing">
|
||||
|
||||
% if about_page_editable:
|
||||
<header>
|
||||
<h2 class="title-2">${_("Introducing Your Course")}</h2>
|
||||
<span class="tip">${_("Information for prospective students")}</span>
|
||||
</header>
|
||||
% endif
|
||||
|
||||
<ol class="list-input">
|
||||
|
||||
% if enable_extended_course_details:
|
||||
@@ -380,6 +409,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
% endif
|
||||
% endif
|
||||
|
||||
% if about_page_editable:
|
||||
<li class="field image" id="field-course-image">
|
||||
<label for="course-image-url">${_("Course Card Image")}</label>
|
||||
<div class="current current-course-image">
|
||||
@@ -412,6 +442,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
|
||||
<button type="button" class="action action-upload-image" id="upload-course-image">${_("Upload Course Card Image")}</button>
|
||||
</div>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
% if enable_extended_course_details:
|
||||
<li class="field image" id="field-banner-image">
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
require(["js/factories/settings_advanced"], function(SettingsAdvancedFactory) {
|
||||
SettingsAdvancedFactory(
|
||||
${advanced_dict | n, dump_js_escaped_json},
|
||||
"${advanced_settings_url | n, js_escaped_string}"
|
||||
"${advanced_settings_url | n, js_escaped_string}",
|
||||
${publisher_enabled | n, dump_js_escaped_json}
|
||||
);
|
||||
});
|
||||
</%block>
|
||||
|
||||
@@ -67,7 +67,7 @@ CMS.URL.LMS_BASE = "${settings.LMS_BASE | n, js_escaped_string}"
|
||||
</div>
|
||||
|
||||
<div class="bit external-help">
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about textbooks")}</a>
|
||||
<a href="${get_online_help_info(online_help_token())['doc_url']}" rel="noopener" target="_blank" class="button external-help-button">${_("Learn more about textbooks")}</a>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3>
|
||||
<div class="copy">
|
||||
|
||||
<p><a class="link-courseURL" rel="external" href="http://localhost:8000/courses/course-v1:AndyA+AA101+1/about" title="This link will open in a new browser window/tab" target="_blank">http://localhost:8000/courses/course-v1:AndyA+AA101+1/about</a></p>
|
||||
<p><a class="link-courseURL" rel="external" href="http://localhost:8000/courses/course-v1:AndyA+AA101+1/about" title="This link will open in a new browser window/tab" rel="noopener" target="_blank">http://localhost:8000/courses/course-v1:AndyA+AA101+1/about</a></p>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
@@ -351,7 +351,7 @@
|
||||
<label class="sr" for="course-overview-cm-textarea">
|
||||
HTML Code Editor
|
||||
</label>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="http://localhost:8000/courses/course-v1:AndyA+AA101+1/about" title="This link will open in a new browser window/tab" target="_blank">your course summary page</a> (formatted in HTML)</span>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="http://localhost:8000/courses/course-v1:AndyA+AA101+1/about" title="This link will open in a new browser window/tab" rel="noopener" target="_blank">your course summary page</a> (formatted in HTML)</span>
|
||||
</li>
|
||||
|
||||
<li class="field image" id="field-course-image">
|
||||
@@ -465,7 +465,7 @@
|
||||
</button>
|
||||
<p class="tip">
|
||||
|
||||
<a href="https://creativecommons.org/about" target="_blank">
|
||||
<a href="https://creativecommons.org/about" rel="noopener" target="_blank">
|
||||
Learn more about Creative Commons
|
||||
</a>
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
<h2 class="sr-only">${_("Account Navigation")}</h2>
|
||||
<ol>
|
||||
<li class="nav-item nav-account-help">
|
||||
<h3 class="title"><span class="label"><a href="${get_online_help_info(online_help_token)['doc_url']}" title="${_('Contextual Online Help')}" target="_blank">${_("Help")}</a></span></h3>
|
||||
<h3 class="title"><span class="label"><a href="${get_online_help_info(online_help_token)['doc_url']}" title="${_('Contextual Online Help')}" rel="noopener" target="_blank">${_("Help")}</a></span></h3>
|
||||
</li>
|
||||
<li class="nav-item nav-account-user">
|
||||
<%include file="user_dropdown.html" args="online_help_token=online_help_token" />
|
||||
@@ -237,7 +237,7 @@
|
||||
<h2 class="sr-only">${_("Account Navigation")}</h2>
|
||||
<ol>
|
||||
<li class="nav-item nav-not-signedin-help">
|
||||
<a href="${get_online_help_info(online_help_token)['doc_url']}" title="${_('Contextual Online Help')}" target="_blank">${_("Help")}</a>
|
||||
<a href="${get_online_help_info(online_help_token)['doc_url']}" title="${_('Contextual Online Help')}" rel="noopener" target="_blank">${_("Help")}</a>
|
||||
</li>
|
||||
% if static.get_value('ALLOW_PUBLIC_ACCOUNT_CREATION', settings.FEATURES.get('ALLOW_PUBLIC_ACCOUNT_CREATION')):
|
||||
<li class="nav-item nav-not-signedin-signup">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%page expression_filter="h"/>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<% isLaTexProblem='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] and enable_latex_compiler %>
|
||||
@@ -18,6 +19,7 @@
|
||||
<span class="problem-editor-icon heading3">
|
||||
<img class="icon" src="${static.url('images/cms-editor_heading.png')}" alt="${_("Insert a heading")}">
|
||||
</span>
|
||||
${_("Heading")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
@@ -25,6 +27,7 @@
|
||||
<span class="problem-editor-icon multiple-choice">
|
||||
<img class="icon" src="${static.url('images/cms-editor_radio.png')}" alt="${_("Add a multiple choice question")}">
|
||||
</span>
|
||||
${_("Multiple Choice")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
@@ -32,6 +35,7 @@
|
||||
<span class="problem-editor-icon checks">
|
||||
<img class="icon" src="${static.url('images/cms-editor_checkbox.png')}" alt="${_("Add a question with checkboxes")}">
|
||||
</span>
|
||||
${_("Checkboxes")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
@@ -39,6 +43,7 @@
|
||||
<span class="problem-editor-icon string">
|
||||
<img class="icon" src="${static.url('images/cms-editor_text.png')}" alt="${_("Insert a text response")}">
|
||||
</span>
|
||||
${_("Text Input")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
@@ -46,6 +51,7 @@
|
||||
<span class="problem-editor-icon number">
|
||||
<img class="icon" src="${static.url('images/cms-editor_number.png')}" alt="${_("Insert a numerical response")}">
|
||||
</span>
|
||||
${_("Numerical Input")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
@@ -53,6 +59,7 @@
|
||||
<span class="problem-editor-icon dropdown">
|
||||
<img class="icon" src="${static.url('images/cms-editor_dropdown.png')}" alt="${_("Insert a dropdown response")}">
|
||||
</span>
|
||||
${_("Dropdown")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
@@ -60,103 +67,108 @@
|
||||
<span class="problem-editor-icon explanation">
|
||||
<img class="icon" src="${static.url('images/cms-editor_explanation.png')}" alt="${_("Add an explanation for this question")}">
|
||||
</span>
|
||||
${_("Explanation")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="editor-tabs">
|
||||
<li><button type="button" class="xml-tab advanced-toggle" data-tab="xml">${_("Advanced Editor")}</button></li>
|
||||
<li><button type="button" class="cheatsheet-toggle" data-tooltip="${_("Toggle Cheatsheet")}">?</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
<textarea class="markdown-box">${markdown | h}</textarea>
|
||||
%endif
|
||||
<textarea class="xml-box" rows="8" cols="40">${data | h}</textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="text/template" id="simple-editor-cheatsheet">
|
||||
<article class="simple-editor-cheatsheet">
|
||||
<div class="cheatsheet-wrapper">
|
||||
<div class="row">
|
||||
<h6>${_("Heading")}</h6>
|
||||
<div class="col sample heading-1">
|
||||
<img class="icon" src="${static.url('images/cms-editor_heading.png')}" alt="${_("Insert a heading")}">
|
||||
</div>
|
||||
<div class="col">
|
||||
<textarea class="markdown-box">${markdown}</textarea>
|
||||
<article class="simple-editor-cheatsheet shown">
|
||||
<div class="cheatsheet-wrapper">
|
||||
<div class="row">
|
||||
<h5>${_("Markdown Help")}</h5>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col sample heading-1">
|
||||
<img class="icon" src="${static.url('images/cms-editor_heading.png')}" alt="${_("Insert a heading")}">
|
||||
<h6>${_("Heading")}</h6>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pre><code>H3
|
||||
=====
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>${_("Multiple Choice")}</h6>
|
||||
<div class="col sample multiple-choice">
|
||||
<img class="icon" src="${static.url('images/cms-editor_radio.png')}" alt="${_("Add a multiple choice question")}">
|
||||
</div>
|
||||
<div class="col">
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col sample multiple-choice">
|
||||
<img class="icon" src="${static.url('images/cms-editor_radio.png')}" alt="${_("Add a multiple choice question")}">
|
||||
<h6>${_("Multiple Choice")}</h6>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pre><code>( ) red
|
||||
( ) green
|
||||
(x) blue</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>${_("Checkboxes")}</h6>
|
||||
<div class="col sample check-multiple">
|
||||
<img class="icon" src="${static.url('images/cms-editor_checkbox.png')}" alt="${_("Add a question with checkboxes")}">
|
||||
</div>
|
||||
<div class="col">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col sample check-multiple">
|
||||
<img class="icon" src="${static.url('images/cms-editor_checkbox.png')}" alt="${_("Add a question with checkboxes")}">
|
||||
<h6>${_("Checkboxes")}</h6>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pre><code>[x] earth
|
||||
[ ] wind
|
||||
[x] water</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>${_("Text Input")}</h6>
|
||||
<div class="col sample string-response">
|
||||
<img class="icon" src="${static.url('images/cms-editor_text.png')}" alt="${_("Insert a text response")}">
|
||||
</div>
|
||||
<div class="col">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col sample string-response">
|
||||
<img class="icon" src="${static.url('images/cms-editor_text.png')}" alt="${_("Insert a text response")}">
|
||||
<h6>${_("Text Input")}</h6>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pre><code>= dog
|
||||
or= cat
|
||||
or= mouse</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col sample numerical-response">
|
||||
<img class="icon" src="${static.url('images/cms-editor_number.png')}" alt="${_("Insert a numerical response")}">
|
||||
<h6>${_("Numerical Input")}</h6>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pre><code>= 3.14 +- 2%</code></pre>
|
||||
<pre><code>= [3.14, 3.15)</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col sample option-reponse">
|
||||
<img class="icon" src="${static.url('images/cms-editor_dropdown.png')}" alt="${_("Insert a dropdown response")}">
|
||||
<h6>${_("Dropdown")}</h6>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pre><code>[[wrong, (right)]]</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>${_("Label")}</h6>
|
||||
<div class="col">
|
||||
<pre><code>>>What is the capital of Argentina?<<</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col sample explanation">
|
||||
<img class="icon" src="${static.url('images/cms-editor_explanation.png')}" alt="${_("Add an explanation for this question")}">
|
||||
<h6>${_("Explanation")}</h6>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pre><code>[explanation] A short explanation of the answer. [explanation]</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>${_("Numerical Input")}</h6>
|
||||
<div class="col sample numerical-response">
|
||||
<img class="icon" src="${static.url('images/cms-editor_number.png')}" alt="${_("Insert a numerical response")}">
|
||||
</div>
|
||||
<div class="col">
|
||||
<pre><code>= 3.14 +- 2%</code></pre>
|
||||
<pre><code>= [3.14, 3.15)</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>${_("Dropdown")}</h6>
|
||||
<div class="col sample option-reponse">
|
||||
<img class="icon" src="${static.url('images/cms-editor_dropdown.png')}" alt="${_("Insert a dropdown response")}">
|
||||
</div>
|
||||
<div class="col">
|
||||
<pre><code>[[wrong, (right)]]</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>${_("Label")}</h6>
|
||||
<div class="col">
|
||||
<pre><code>>>What is the capital of Argentina?<<</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>${_("Explanation")}</h6>
|
||||
<div class="col sample explanation">
|
||||
<img class="icon" src="${static.url('images/cms-editor_explanation.png')}" alt="${_("Add an explanation for this question")}">
|
||||
</div>
|
||||
<div class="col">
|
||||
<pre><code>[explanation] A short explanation of the answer. [explanation]</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</article>
|
||||
</div>
|
||||
%endif
|
||||
</div>
|
||||
<textarea class="xml-box" rows="8" cols="40">${data}</textarea>
|
||||
</section>
|
||||
|
||||
<script type="text/template" id="simple-editor-cheatsheet">
|
||||
|
||||
</script>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
|
||||
19
cms/urls.py
19
cms/urls.py
@@ -270,12 +270,19 @@ urlpatterns += [
|
||||
url(r'^500$', handler500),
|
||||
]
|
||||
|
||||
if settings.FEATURES.get('ENABLE_API_DOCS'):
|
||||
urlpatterns += [
|
||||
url(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
url(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=0)),
|
||||
]
|
||||
# API docs.
|
||||
urlpatterns += [
|
||||
url(
|
||||
r'^swagger(?P<format>\.json|\.yaml)$',
|
||||
schema_view.without_ui(cache_timeout=settings.OPENAPI_CACHE_TIMEOUT), name='schema-json',
|
||||
),
|
||||
url(
|
||||
r'^swagger/$',
|
||||
schema_view.with_ui('swagger', cache_timeout=settings.OPENAPI_CACHE_TIMEOUT),
|
||||
name='schema-swagger-ui',
|
||||
),
|
||||
url(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=settings.OPENAPI_CACHE_TIMEOUT)),
|
||||
]
|
||||
|
||||
if 'openedx.testing.coverage_context_listener' in settings.INSTALLED_APPS:
|
||||
urlpatterns += [
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.core.validators import validate_comma_separated_integer_list
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from edx_django_utils.cache import RequestCache
|
||||
@@ -38,6 +39,7 @@ Mode = namedtuple('Mode',
|
||||
])
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseMode(models.Model):
|
||||
"""
|
||||
We would like to offer a course in a variety of modes.
|
||||
@@ -211,7 +213,7 @@ class CourseMode(models.Model):
|
||||
|
||||
mode_config = settings.COURSE_ENROLLMENT_MODES.get(self.mode_slug, {})
|
||||
min_price_for_mode = mode_config.get('min_price', 0)
|
||||
if self.min_price < min_price_for_mode:
|
||||
if int(self.min_price) < min_price_for_mode:
|
||||
mode_display_name = mode_config.get('display_name', self.mode_slug)
|
||||
raise ValidationError(
|
||||
_(
|
||||
@@ -791,7 +793,7 @@ class CourseMode(models.Model):
|
||||
self.bulk_sku
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return u"{} : {}, min={}".format(
|
||||
self.course_id, self.mode_slug, self.min_price
|
||||
)
|
||||
@@ -902,6 +904,7 @@ class CourseModesArchive(models.Model):
|
||||
expiration_datetime = models.DateTimeField(default=None, null=True, blank=True)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseModeExpirationConfig(ConfigurationModel):
|
||||
"""
|
||||
Configuration for time period from end of course to auto-expire a course mode.
|
||||
@@ -918,6 +921,6 @@ class CourseModeExpirationConfig(ConfigurationModel):
|
||||
)
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
""" Returns the unicode date of the verification window. """
|
||||
return six.text_type(self.verification_window)
|
||||
|
||||
@@ -125,9 +125,11 @@ class MakoRequestContextTest(TestCase):
|
||||
self.assertIsNotNone(get_template_request_context())
|
||||
|
||||
mock_get_current_request = Mock()
|
||||
with patch('edxmako.request_context.get_current_request', mock_get_current_request):
|
||||
# requestcontext should not be None, because the cache is filled
|
||||
self.assertIsNotNone(get_template_request_context())
|
||||
with patch('edxmako.request_context.get_current_request'):
|
||||
with patch('edxmako.request_context.RequestContext.__init__') as mock_context_init:
|
||||
# requestcontext should not be None, because the cache is filled
|
||||
self.assertIsNotNone(get_template_request_context())
|
||||
mock_context_init.assert_not_called()
|
||||
mock_get_current_request.assert_not_called()
|
||||
|
||||
RequestCache.clear_all_namespaces()
|
||||
|
||||
@@ -7,7 +7,7 @@ from __future__ import absolute_import
|
||||
|
||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
from courseware.access import has_access
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
|
||||
|
||||
class IsAdminOrSupportOrAuthenticatedReadOnly(BasePermission):
|
||||
|
||||
@@ -14,7 +14,7 @@ from opaque_keys.edx.locator import CourseKey
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from courseware.models import DynamicUpgradeDeadlineConfiguration
|
||||
from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
||||
|
||||
@@ -62,7 +62,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
return
|
||||
|
||||
for batch_num in range(num_batches):
|
||||
for batch_num in range(int(num_batches)):
|
||||
start = batch_num * batch_size + 1 # ids are 1-based, so add 1
|
||||
end = min(start + batch_size, total + 1)
|
||||
expire_old_entitlements.delay(start, end, logid=str(batch_num))
|
||||
|
||||
@@ -9,6 +9,7 @@ from datetime import timedelta
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.db import IntegrityError, models, transaction
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.timezone import now
|
||||
from model_utils import Choices
|
||||
from model_utils.models import TimeStampedModel
|
||||
@@ -26,6 +27,7 @@ from util.date_utils import strftime_localized
|
||||
log = logging.getLogger("common.entitlements.models")
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseEntitlementPolicy(models.Model):
|
||||
"""
|
||||
Represents the Entitlement's policy for expiration, refunds, and regaining a used certificate
|
||||
@@ -138,7 +140,7 @@ class CourseEntitlementPolicy(models.Model):
|
||||
and not entitlement.enrollment_course_run
|
||||
and not entitlement.expired_at)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return u'Course Entitlement Policy: expiration_period: {}, refund_period: {}, regain_period: {}, mode: {}'\
|
||||
.format(
|
||||
self.expiration_period,
|
||||
@@ -448,6 +450,7 @@ class CourseEntitlement(TimeStampedModel):
|
||||
raise IntegrityError
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseEntitlementSupportDetail(TimeStampedModel):
|
||||
"""
|
||||
Table recording support interactions with an entitlement
|
||||
@@ -492,7 +495,7 @@ class CourseEntitlementSupportDetail(TimeStampedModel):
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
"""Unicode representation of an Entitlement"""
|
||||
return u'Course Entitlement Support Detail: entitlement: {}, support_user: {}, reason: {}'.format(
|
||||
self.entitlement,
|
||||
|
||||
@@ -9,8 +9,10 @@ from six.moves import map
|
||||
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.db.models.fields import TextField
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class AssetBaseUrlConfig(ConfigurationModel):
|
||||
"""
|
||||
Configuration for the base URL used for static assets.
|
||||
@@ -34,10 +36,11 @@ class AssetBaseUrlConfig(ConfigurationModel):
|
||||
def __repr__(self):
|
||||
return '<AssetBaseUrlConfig(base_url={})>'.format(self.get_base_url())
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return six.text_type(repr(self))
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class AssetExcludedExtensionsConfig(ConfigurationModel):
|
||||
"""
|
||||
Configuration for the the excluded file extensions when canonicalizing static asset paths.
|
||||
@@ -63,5 +66,5 @@ class AssetExcludedExtensionsConfig(ConfigurationModel):
|
||||
def __repr__(self):
|
||||
return '<AssetExcludedExtensionsConfig(extensions={})>'.format(self.get_excluded_extensions())
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return six.text_type(repr(self))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import re
|
||||
from six import StringIO
|
||||
from six import BytesIO
|
||||
from six.moves.urllib.parse import parse_qsl, urlparse, urlunparse
|
||||
|
||||
import ddt
|
||||
@@ -331,7 +331,7 @@ class CanonicalContentTest(SharedModuleStoreTestCase):
|
||||
StaticContent: the StaticContent object for the created image
|
||||
"""
|
||||
new_image = Image.new('RGB', dimensions, color)
|
||||
new_buf = StringIO()
|
||||
new_buf = BytesIO()
|
||||
new_image.save(new_buf, format='png')
|
||||
new_buf.seek(0)
|
||||
new_name = name.format(prefix)
|
||||
@@ -355,7 +355,7 @@ class CanonicalContentTest(SharedModuleStoreTestCase):
|
||||
StaticContent: the StaticContent object for the created content
|
||||
|
||||
"""
|
||||
new_buf = StringIO('testingggggggggggg')
|
||||
new_buf = BytesIO(b'testingggggggggggg')
|
||||
new_name = name.format(prefix)
|
||||
new_key = StaticContent.compute_location(cls.courses[prefix].id, new_name)
|
||||
new_content = StaticContent(new_key, new_name, 'application/octet-stream', new_buf.getvalue(), locked=locked)
|
||||
@@ -588,8 +588,6 @@ class CanonicalContentTest(SharedModuleStoreTestCase):
|
||||
|
||||
with check_mongo_calls(mongo_calls):
|
||||
asset_path = StaticContent.get_canonicalized_asset_path(self.courses[prefix].id, start, base_url, exts)
|
||||
print(expected)
|
||||
print(asset_path)
|
||||
self.assertIsNotNone(re.match(expected, asset_path))
|
||||
|
||||
@ddt.data(
|
||||
@@ -786,6 +784,4 @@ class CanonicalContentTest(SharedModuleStoreTestCase):
|
||||
|
||||
with check_mongo_calls(mongo_calls):
|
||||
asset_path = StaticContent.get_canonicalized_asset_path(self.courses[prefix].id, start, base_url, exts)
|
||||
print(expected)
|
||||
print(asset_path)
|
||||
self.assertIsNotNone(re.match(expected, asset_path))
|
||||
|
||||
@@ -10,11 +10,13 @@ from config_models.models import ConfigurationModel
|
||||
from django.contrib import admin
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class GlobalStatusMessage(ConfigurationModel):
|
||||
"""
|
||||
Model that represents the current status message.
|
||||
@@ -51,10 +53,11 @@ class GlobalStatusMessage(ConfigurationModel):
|
||||
cache.set(cache_key, msg)
|
||||
return msg
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return "{} - {} - {}".format(self.change_date, self.enabled, self.message)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseMessage(models.Model):
|
||||
"""
|
||||
Model that allows the administrator to specify banner messages for individual courses.
|
||||
@@ -68,7 +71,7 @@ class CourseMessage(models.Model):
|
||||
course_key = CourseKeyField(max_length=255, blank=True, db_index=True)
|
||||
message = models.TextField(blank=True, null=True)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return six.text_type(self.course_key)
|
||||
|
||||
|
||||
|
||||
@@ -327,14 +327,14 @@ def _get_redirect_to(request):
|
||||
mime_type, _ = mimetypes.guess_type(redirect_to, strict=False)
|
||||
if not is_safe_login_or_logout_redirect(request, redirect_to):
|
||||
log.warning(
|
||||
u'Unsafe redirect parameter detected after login page: %(redirect_to)r',
|
||||
u"Unsafe redirect parameter detected after login page: '%(redirect_to)s'",
|
||||
{"redirect_to": redirect_to}
|
||||
)
|
||||
redirect_to = None
|
||||
elif 'text/html' not in header_accept:
|
||||
log.info(
|
||||
u'Redirect to non html content %(content_type)r detected from %(user_agent)r'
|
||||
u' after login page: %(redirect_to)r',
|
||||
u"Redirect to non html content '%(content_type)s' detected from '%(user_agent)s'"
|
||||
u" after login page: '%(redirect_to)s'",
|
||||
{
|
||||
"redirect_to": redirect_to, "content_type": header_accept,
|
||||
"user_agent": request.META.get('HTTP_USER_AGENT', '')
|
||||
@@ -343,13 +343,13 @@ def _get_redirect_to(request):
|
||||
redirect_to = None
|
||||
elif mime_type:
|
||||
log.warning(
|
||||
u'Redirect to url path with specified filed type %(mime_type)r not allowed: %(redirect_to)r',
|
||||
u"Redirect to url path with specified filed type '%(mime_type)s' not allowed: '%(redirect_to)s'",
|
||||
{"redirect_to": redirect_to, "mime_type": mime_type}
|
||||
)
|
||||
redirect_to = None
|
||||
elif settings.STATIC_URL in redirect_to:
|
||||
log.warning(
|
||||
u'Redirect to static content detected after login page: %(redirect_to)r',
|
||||
u"Redirect to static content detected after login page: '%(redirect_to)s'",
|
||||
{"redirect_to": redirect_to}
|
||||
)
|
||||
redirect_to = None
|
||||
@@ -359,7 +359,7 @@ def _get_redirect_to(request):
|
||||
for theme in themes:
|
||||
if theme.theme_dir_name in next_path:
|
||||
log.warning(
|
||||
u'Redirect to theme content detected after login page: %(redirect_to)r',
|
||||
u"Redirect to theme content detected after login page: '%(redirect_to)s'",
|
||||
{"redirect_to": redirect_to}
|
||||
)
|
||||
redirect_to = None
|
||||
@@ -568,8 +568,9 @@ def _cert_info(user, course_overview, cert_status):
|
||||
# who need to be regraded (we weren't tracking 'notpassing' at first).
|
||||
# We can add a log.warning here once we think it shouldn't happen.
|
||||
return default_info
|
||||
|
||||
status_dict['grade'] = text_type(max(cert_grade_percent, persisted_grade_percent))
|
||||
grades_input = [cert_grade_percent, persisted_grade_percent]
|
||||
max_grade = None if all(grade is None for grade in grades_input) else max(filter(lambda x: x is not None, grades_input))
|
||||
status_dict['grade'] = text_type(max_grade)
|
||||
|
||||
return status_dict
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='courseenrollment',
|
||||
index=models.Index(fields=[b'user', b'-created'], name='student_cou_user_id_b19dcd_idx'),
|
||||
index=models.Index(fields=['user', '-created'], name='student_cou_user_id_b19dcd_idx'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -40,6 +40,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext_noop
|
||||
from django_countries.fields import CountryField
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from edx_rest_api_client.exceptions import SlumberBaseException
|
||||
from eventtracking import tracker
|
||||
@@ -55,7 +56,7 @@ from slumber.exceptions import HttpClientError, HttpServerError
|
||||
from user_util import user_util
|
||||
|
||||
from course_modes.models import CourseMode, get_cosmetic_verified_display_price
|
||||
from courseware.models import (
|
||||
from lms.djangoapps.courseware.models import (
|
||||
CourseDynamicUpgradeDeadlineConfiguration,
|
||||
DynamicUpgradeDeadlineConfiguration,
|
||||
OrgDynamicUpgradeDeadlineConfiguration
|
||||
@@ -64,7 +65,11 @@ from lms.djangoapps.certificates.models import GeneratedCertificate
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
import openedx.core.djangoapps.django_comment_common.comment_client as cc
|
||||
from openedx.core.djangoapps.enrollments.api import _default_course_mode
|
||||
from openedx.core.djangoapps.enrollments.api import (
|
||||
_default_course_mode,
|
||||
get_enrollment_attributes,
|
||||
set_enrollment_attributes
|
||||
)
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager
|
||||
from openedx.core.djangolib.model_mixins import DeletableByUserValue
|
||||
@@ -857,7 +862,7 @@ EVENT_NAME_ENROLLMENT_DEACTIVATED = 'edx.course.enrollment.deactivated'
|
||||
EVENT_NAME_ENROLLMENT_MODE_CHANGED = 'edx.course.enrollment.mode_changed'
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
@python_2_unicode_compatible
|
||||
class LoginFailures(models.Model):
|
||||
"""
|
||||
This model will keep track of failed login attempts.
|
||||
@@ -1083,6 +1088,7 @@ class CourseEnrollmentManager(models.Manager):
|
||||
CourseEnrollmentState = namedtuple('CourseEnrollmentState', 'mode, is_active')
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseEnrollment(models.Model):
|
||||
"""
|
||||
Represents a Student's Enrollment record for a single Course. You should
|
||||
@@ -1159,7 +1165,7 @@ class CourseEnrollment(models.Model):
|
||||
# When the property .course_overview is accessed for the first time, this variable will be set.
|
||||
self._course_overview = None
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return (
|
||||
"[CourseEnrollment] {}: {} ({}); active: ({})"
|
||||
).format(self.user, self.course_id, self.created, self.is_active)
|
||||
@@ -1764,8 +1770,56 @@ class CourseEnrollment(models.Model):
|
||||
# NOTE: This is here to avoid circular references
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client, ECOMMERCE_DATE_FORMAT
|
||||
|
||||
date_placed = self.get_order_attribute_value('date_placed')
|
||||
|
||||
if not date_placed:
|
||||
order_number = self.get_order_attribute_value('order_number')
|
||||
if not order_number:
|
||||
return None
|
||||
|
||||
try:
|
||||
order = ecommerce_api_client(self.user).orders(order_number).get()
|
||||
date_placed = order['date_placed']
|
||||
# also save the attribute so that we don't need to call ecommerce again.
|
||||
username = self.user.username
|
||||
enrollment_attributes = get_enrollment_attributes(username, six.text_type(self.course_id))
|
||||
enrollment_attributes.append(
|
||||
{
|
||||
"namespace": "order",
|
||||
"name": "date_placed",
|
||||
"value": date_placed,
|
||||
}
|
||||
)
|
||||
set_enrollment_attributes(username, six.text_type(self.course_id), enrollment_attributes)
|
||||
except HttpClientError:
|
||||
log.warning(
|
||||
u"Encountered HttpClientError while getting order details from ecommerce. "
|
||||
u"Order={number} and user {user}".format(number=order_number, user=self.user.id))
|
||||
return None
|
||||
|
||||
except HttpServerError:
|
||||
log.warning(
|
||||
u"Encountered HttpServerError while getting order details from ecommerce. "
|
||||
u"Order={number} and user {user}".format(number=order_number, user=self.user.id))
|
||||
return None
|
||||
|
||||
except SlumberBaseException:
|
||||
log.warning(
|
||||
u"Encountered an error while getting order details from ecommerce. "
|
||||
u"Order={number} and user {user}".format(number=order_number, user=self.user.id))
|
||||
return None
|
||||
|
||||
refund_window_start_date = max(
|
||||
datetime.strptime(date_placed, ECOMMERCE_DATE_FORMAT),
|
||||
self.course_overview.start.replace(tzinfo=None)
|
||||
)
|
||||
|
||||
return refund_window_start_date.replace(tzinfo=UTC) + EnrollmentRefundConfiguration.current().refund_window
|
||||
|
||||
def get_order_attribute_value(self, attr_name):
|
||||
""" Get and return course enrollment order attribute's value."""
|
||||
try:
|
||||
attribute = self.attributes.get(namespace='order', name='order_number')
|
||||
attribute = self.attributes.get(namespace='order', name=attr_name)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
except MultipleObjectsReturned:
|
||||
@@ -1776,36 +1830,9 @@ class CourseEnrollment(models.Model):
|
||||
self.user.id,
|
||||
enrollment_id
|
||||
)
|
||||
attribute = self.attributes.filter(namespace='order', name='order_number').last()
|
||||
attribute = self.attributes.filter(namespace='order', name=attr_name).last()
|
||||
|
||||
order_number = attribute.value
|
||||
try:
|
||||
order = ecommerce_api_client(self.user).orders(order_number).get()
|
||||
|
||||
except HttpClientError:
|
||||
log.warning(
|
||||
u"Encountered HttpClientError while getting order details from ecommerce. "
|
||||
u"Order={number} and user {user}".format(number=order_number, user=self.user.id))
|
||||
return None
|
||||
|
||||
except HttpServerError:
|
||||
log.warning(
|
||||
u"Encountered HttpServerError while getting order details from ecommerce. "
|
||||
u"Order={number} and user {user}".format(number=order_number, user=self.user.id))
|
||||
return None
|
||||
|
||||
except SlumberBaseException:
|
||||
log.warning(
|
||||
u"Encountered an error while getting order details from ecommerce. "
|
||||
u"Order={number} and user {user}".format(number=order_number, user=self.user.id))
|
||||
return None
|
||||
|
||||
refund_window_start_date = max(
|
||||
datetime.strptime(order['date_placed'], ECOMMERCE_DATE_FORMAT),
|
||||
self.course_overview.start.replace(tzinfo=None)
|
||||
)
|
||||
|
||||
return refund_window_start_date.replace(tzinfo=UTC) + EnrollmentRefundConfiguration.current().refund_window
|
||||
return attribute.value
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
@@ -2138,6 +2165,7 @@ class ManualEnrollmentAudit(models.Model):
|
||||
return cls.objects.filter(id__in=manual_enrollment_ids).update(reason="", enrolled_email=retired_email)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseEnrollmentAllowed(DeletableByUserValue, models.Model):
|
||||
"""
|
||||
Table of users (specified by email address strings) who are allowed to enroll in a specified course.
|
||||
@@ -2165,7 +2193,7 @@ class CourseEnrollmentAllowed(DeletableByUserValue, models.Model):
|
||||
class Meta(object):
|
||||
unique_together = (('email', 'course_id'),)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created)
|
||||
|
||||
@classmethod
|
||||
@@ -2198,6 +2226,7 @@ class CourseEnrollmentAllowed(DeletableByUserValue, models.Model):
|
||||
|
||||
|
||||
@total_ordering
|
||||
@python_2_unicode_compatible
|
||||
class CourseAccessRole(models.Model):
|
||||
"""
|
||||
Maps users to org, courses, and roles. Used by student.roles.CourseRole and OrgRole.
|
||||
@@ -2243,7 +2272,7 @@ class CourseAccessRole(models.Model):
|
||||
"""
|
||||
return self._key < other._key # pylint: disable=protected-access
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return "[CourseAccessRole] user: {} role: {} org: {} course: {}".format(self.user.username, self.role, self.org, self.course_id)
|
||||
|
||||
|
||||
@@ -2575,6 +2604,7 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class EntranceExamConfiguration(models.Model):
|
||||
"""
|
||||
Represents a Student's entrance exam specific data for a single Course
|
||||
@@ -2594,7 +2624,7 @@ class EntranceExamConfiguration(models.Model):
|
||||
class Meta(object):
|
||||
unique_together = (('user', 'course_id'), )
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return "[EntranceExamConfiguration] %s: %s (%s) = %s" % (
|
||||
self.user, self.course_id, self.created, self.skip_entrance_exam
|
||||
)
|
||||
@@ -2682,6 +2712,7 @@ class SocialLink(models.Model): # pylint: disable=model-missing-unicode
|
||||
social_link = models.CharField(max_length=100, blank=True)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseEnrollmentAttribute(models.Model):
|
||||
"""
|
||||
Provide additional information about the user's enrollment.
|
||||
@@ -2702,7 +2733,7 @@ class CourseEnrollmentAttribute(models.Model):
|
||||
help_text=_("Value of the enrollment attribute")
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
"""Unicode representation of the attribute. """
|
||||
return u"{namespace}:{name}, {value}".format(
|
||||
namespace=self.namespace,
|
||||
@@ -2789,6 +2820,7 @@ class EnrollmentRefundConfiguration(ConfigurationModel):
|
||||
self.refund_window_microseconds = int(refund_window.total_seconds() * 1000000)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class RegistrationCookieConfiguration(ConfigurationModel):
|
||||
"""
|
||||
Configuration for registration cookies.
|
||||
@@ -2805,7 +2837,7 @@ class RegistrationCookieConfiguration(ConfigurationModel):
|
||||
help_text=_("Name of the affiliate cookie")
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
"""Unicode representation of this config. """
|
||||
return u"UTM: {utm_name}; AFFILIATE: {affiliate_name}".format(
|
||||
utm_name=self.utm_cookie_name,
|
||||
@@ -2813,6 +2845,7 @@ class RegistrationCookieConfiguration(ConfigurationModel):
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class UserAttribute(TimeStampedModel):
|
||||
"""
|
||||
Record additional metadata about a user, stored as key/value pairs of text.
|
||||
@@ -2828,12 +2861,11 @@ class UserAttribute(TimeStampedModel):
|
||||
name = models.CharField(max_length=255, help_text=_("Name of this user attribute."), db_index=True)
|
||||
value = models.CharField(max_length=255, help_text=_("Value of this user attribute."))
|
||||
|
||||
def __unicode__(self):
|
||||
"""Unicode representation of this attribute. """
|
||||
return u"[{username}] {name}: {value}".format(
|
||||
def __str__(self):
|
||||
return "[{username}] {name}: {value}".format(
|
||||
name=self.name,
|
||||
value=self.value,
|
||||
username=self.user.username,
|
||||
username=self.user.username
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -2857,6 +2889,7 @@ class UserAttribute(TimeStampedModel):
|
||||
return None
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class LogoutViewConfiguration(ConfigurationModel):
|
||||
"""
|
||||
DEPRECATED: Configuration for the logout view.
|
||||
@@ -2864,7 +2897,7 @@ class LogoutViewConfiguration(ConfigurationModel):
|
||||
.. no_pii:
|
||||
"""
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
"""
|
||||
Unicode representation of the instance.
|
||||
"""
|
||||
|
||||
@@ -215,7 +215,7 @@ class ReactivationEmailTests(EmailTestMixin, CacheIsolationTestCase):
|
||||
Send the reactivation email to the specified user,
|
||||
and return the response as json data.
|
||||
"""
|
||||
return json.loads(send_reactivation_email_for_user(user).content)
|
||||
return json.loads(send_reactivation_email_for_user(user).content.decode('utf-8'))
|
||||
|
||||
def assertReactivateEmailSent(self, email_user):
|
||||
"""
|
||||
@@ -479,8 +479,8 @@ class EmailChangeConfirmationTests(EmailTestMixin, EmailTemplateTagMixin, CacheI
|
||||
response = confirm_email_change(self.request, self.key)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEquals(
|
||||
mock_render_to_response(expected_template, expected_context).content,
|
||||
response.content
|
||||
mock_render_to_response(expected_template, expected_context).content.decode('utf-8'),
|
||||
response.content.decode('utf-8')
|
||||
)
|
||||
|
||||
def assertChangeEmailSent(self, test_body_type):
|
||||
|
||||
@@ -104,7 +104,7 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
|
||||
# Enroll in the course and verify the URL we get sent to
|
||||
resp = self._change_enrollment('enroll')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.content, full_url)
|
||||
self.assertEqual(resp.content.decode('utf-8'), full_url)
|
||||
|
||||
# If we're not expecting to be enrolled, verify that this is the case
|
||||
if enrollment_mode is None:
|
||||
@@ -171,7 +171,7 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
|
||||
with restrict_course(self.course.id) as redirect_url:
|
||||
response = self._change_enrollment('enroll')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content, redirect_url)
|
||||
self.assertEqual(response.content.decode('utf-8'), redirect_url)
|
||||
|
||||
# Verify that we weren't enrolled
|
||||
is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id)
|
||||
@@ -181,7 +181,7 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
|
||||
def test_embargo_allow(self):
|
||||
response = self._change_enrollment('enroll')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content, '')
|
||||
self.assertEqual(response.content.decode('utf-8'), '')
|
||||
|
||||
# Verify that we were enrolled
|
||||
is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id)
|
||||
|
||||
@@ -38,20 +38,20 @@ class TestLoginHelper(TestCase):
|
||||
|
||||
@ddt.data(
|
||||
(logging.WARNING, "WARNING", "https://www.amazon.com", "text/html", None,
|
||||
"Unsafe redirect parameter detected after login page: u'https://www.amazon.com'"),
|
||||
"Unsafe redirect parameter detected after login page: 'https://www.amazon.com'"),
|
||||
(logging.WARNING, "WARNING", "testserver/edx.org/images/logo", "text/html", None,
|
||||
"Redirect to theme content detected after login page: u'testserver/edx.org/images/logo'"),
|
||||
"Redirect to theme content detected after login page: 'testserver/edx.org/images/logo'"),
|
||||
(logging.INFO, "INFO", "favicon.ico", "image/*", "test/agent",
|
||||
"Redirect to non html content 'image/*' detected from 'test/agent' after login page: u'favicon.ico'"),
|
||||
"Redirect to non html content 'image/*' detected from 'test/agent' after login page: 'favicon.ico'"),
|
||||
(logging.WARNING, "WARNING", "https://www.test.com/test.jpg", "image/*", None,
|
||||
"Unsafe redirect parameter detected after login page: u'https://www.test.com/test.jpg'"),
|
||||
"Unsafe redirect parameter detected after login page: 'https://www.test.com/test.jpg'"),
|
||||
(logging.INFO, "INFO", static_url + "dummy.png", "image/*", "test/agent",
|
||||
"Redirect to non html content 'image/*' detected from 'test/agent' after login page: u'" + static_url +
|
||||
"Redirect to non html content 'image/*' detected from 'test/agent' after login page: '" + static_url +
|
||||
"dummy.png" + "'"),
|
||||
(logging.WARNING, "WARNING", "test.png", "text/html", None,
|
||||
"Redirect to url path with specified filed type 'image/png' not allowed: u'test.png'"),
|
||||
"Redirect to url path with specified filed type 'image/png' not allowed: 'test.png'"),
|
||||
(logging.WARNING, "WARNING", static_url + "dummy.png", "text/html", None,
|
||||
"Redirect to url path with specified filed type 'image/png' not allowed: u'" + static_url + "dummy.png" + "'"),
|
||||
"Redirect to url path with specified filed type 'image/png' not allowed: '" + static_url + "dummy.png" + "'"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_next_failures(self, log_level, log_name, unsafe_url, http_accept, user_agent, expected_log):
|
||||
|
||||
@@ -17,7 +17,7 @@ from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from courseware.models import DynamicUpgradeDeadlineConfiguration
|
||||
from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.schedules.models import Schedule
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
@@ -63,7 +63,7 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
|
||||
self.assertIsNone(CourseEnrollment.generate_enrollment_status_hash(AnonymousUser()))
|
||||
|
||||
# No enrollments
|
||||
expected = hashlib.md5(self.user.username).hexdigest()
|
||||
expected = hashlib.md5(self.user.username.encode('utf-8')).hexdigest()
|
||||
self.assertEqual(CourseEnrollment.generate_enrollment_status_hash(self.user), expected)
|
||||
self.assert_enrollment_status_hash_cached(self.user, expected)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from course_modes.tests.factories import CourseModeFactory
|
||||
from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate
|
||||
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
|
||||
from openedx.core.djangoapps.commerce.utils import ECOMMERCE_DATE_FORMAT
|
||||
from student.models import CourseEnrollment
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAttribute, EnrollmentRefundConfiguration
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -140,7 +140,8 @@ class RefundableTest(SharedModuleStoreTestCase):
|
||||
course_start = now + course_start_delta
|
||||
expected_date = now + expected_date_delta
|
||||
refund_period = timedelta(days=days)
|
||||
expected_content = '{{"date_placed": "{date}"}}'.format(date=order_date.strftime(ECOMMERCE_DATE_FORMAT))
|
||||
date_placed = order_date.strftime(ECOMMERCE_DATE_FORMAT)
|
||||
expected_content = '{{"date_placed": "{date}"}}'.format(date=date_placed)
|
||||
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
@@ -165,10 +166,46 @@ class RefundableTest(SharedModuleStoreTestCase):
|
||||
expected_date + refund_period
|
||||
)
|
||||
|
||||
expected_date_placed_attr = {
|
||||
"namespace": "order",
|
||||
"name": "date_placed",
|
||||
"value": date_placed,
|
||||
}
|
||||
|
||||
self.assertIn(
|
||||
expected_date_placed_attr,
|
||||
CourseEnrollmentAttribute.get_enrollment_attributes(self.enrollment)
|
||||
)
|
||||
|
||||
def test_refund_cutoff_date_no_attributes(self):
|
||||
""" Assert that the None is returned when no order number attribute is found."""
|
||||
self.assertIsNone(self.enrollment.refund_cutoff_date())
|
||||
|
||||
@patch('openedx.core.djangoapps.commerce.utils.ecommerce_api_client')
|
||||
def test_refund_cutoff_date_with_date_placed_attr(self, mock_ecommerce_api_client):
|
||||
"""
|
||||
Assert that the refund_cutoff_date returns order placement date if order:date_placed
|
||||
attribute exist without calling ecommerce.
|
||||
"""
|
||||
now = datetime.now(pytz.UTC).replace(microsecond=0)
|
||||
order_date = now + timedelta(days=2)
|
||||
course_start = now + timedelta(days=1)
|
||||
|
||||
self.enrollment.course_overview.start = course_start
|
||||
self.enrollment.attributes.create(
|
||||
enrollment=self.enrollment,
|
||||
namespace='order',
|
||||
name='date_placed',
|
||||
value=order_date.strftime(ECOMMERCE_DATE_FORMAT)
|
||||
)
|
||||
|
||||
refund_config = EnrollmentRefundConfiguration.current()
|
||||
self.assertEqual(
|
||||
self.enrollment.refund_cutoff_date(),
|
||||
order_date + refund_config.refund_window
|
||||
)
|
||||
mock_ecommerce_api_client.assert_not_called()
|
||||
|
||||
@httpretty.activate
|
||||
@override_settings(ECOMMERCE_API_URL=TEST_API_URL)
|
||||
def test_multiple_refunds_dashbaord_page_error(self):
|
||||
|
||||
@@ -11,10 +11,11 @@ import unittest
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, make_password
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.core import mail
|
||||
from django.core.cache import cache
|
||||
from django.http import Http404
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
@@ -75,7 +76,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
bad_pwd_resp = password_reset(bad_pwd_req)
|
||||
# If they've got an unusable password, we return a successful response code
|
||||
self.assertEquals(bad_pwd_resp.status_code, 200)
|
||||
obj = json.loads(bad_pwd_resp.content)
|
||||
obj = json.loads(bad_pwd_resp.content.decode('utf-8'))
|
||||
self.assertEquals(obj, {
|
||||
'success': True,
|
||||
'value': "('registration/password_reset_done.html', [])",
|
||||
@@ -94,7 +95,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
# This prevents someone potentially trying to "brute-force" find out which
|
||||
# emails are and aren't registered with edX
|
||||
self.assertEquals(bad_email_resp.status_code, 200)
|
||||
obj = json.loads(bad_email_resp.content)
|
||||
obj = json.loads(bad_email_resp.content.decode('utf-8'))
|
||||
self.assertEquals(obj, {
|
||||
'success': True,
|
||||
'value': "('registration/password_reset_done.html', [])",
|
||||
@@ -144,7 +145,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
self.assertFalse(dop_models.RefreshToken.objects.filter(user=self.user).exists())
|
||||
self.assertFalse(dot_models.AccessToken.objects.filter(user=self.user).exists())
|
||||
self.assertFalse(dot_models.RefreshToken.objects.filter(user=self.user).exists())
|
||||
obj = json.loads(good_resp.content)
|
||||
obj = json.loads(good_resp.content.decode('utf-8'))
|
||||
self.assertTrue(obj['success'])
|
||||
self.assertIn('e-mailed you instructions for setting your password', obj['value'])
|
||||
|
||||
@@ -286,6 +287,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
kwargs={"uidb36": uidb36, "token": token}
|
||||
)
|
||||
)
|
||||
bad_request.user = AnonymousUser()
|
||||
password_reset_confirm_wrapper(bad_request, uidb36, token)
|
||||
self.user = User.objects.get(pk=self.user.pk)
|
||||
self.assertFalse(self.user.is_active)
|
||||
@@ -299,6 +301,21 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
kwargs={"uidb36": self.uidb36, "token": self.token}
|
||||
)
|
||||
good_reset_req = self.request_factory.get(url)
|
||||
good_reset_req.user = self.user
|
||||
password_reset_confirm_wrapper(good_reset_req, self.uidb36, self.token)
|
||||
self.user = User.objects.get(pk=self.user.pk)
|
||||
self.assertTrue(self.user.is_active)
|
||||
|
||||
def test_reset_password_good_token_with_anonymous_user(self):
|
||||
"""
|
||||
Tests good token and uidb36 in password reset for anonymous user
|
||||
"""
|
||||
url = reverse(
|
||||
"password_reset_confirm",
|
||||
kwargs={"uidb36": self.uidb36, "token": self.token}
|
||||
)
|
||||
good_reset_req = self.request_factory.get(url)
|
||||
good_reset_req.user = AnonymousUser()
|
||||
password_reset_confirm_wrapper(good_reset_req, self.uidb36, self.token)
|
||||
self.user = User.objects.get(pk=self.user.pk)
|
||||
self.assertTrue(self.user.is_active)
|
||||
@@ -315,6 +332,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
)
|
||||
request_params = {'new_password1': 'password1', 'new_password2': 'password2'}
|
||||
confirm_request = self.request_factory.post(url, data=request_params)
|
||||
confirm_request.user = self.user
|
||||
|
||||
# Make a password reset request with mismatching passwords.
|
||||
resp = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token)
|
||||
@@ -338,6 +356,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
kwargs={'uidb36': self.uidb36, 'token': self.token}
|
||||
)
|
||||
reset_req = self.request_factory.get(url)
|
||||
reset_req.user = self.user
|
||||
resp = password_reset_confirm_wrapper(reset_req, self.uidb36, self.token)
|
||||
|
||||
# Verify the response status code is: 200 with password reset fail and also verify that
|
||||
@@ -352,6 +371,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
kwargs={"uidb36": self.uidb36, "token": self.token}
|
||||
)
|
||||
for request in [self.request_factory.get(url), self.request_factory.post(url)]:
|
||||
request.user = self.user
|
||||
response = password_reset_confirm_wrapper(request, self.uidb36, self.token)
|
||||
assert response.context_data['err_msg'] == SYSTEM_MAINTENANCE_MSG
|
||||
self.user.refresh_from_db()
|
||||
@@ -371,7 +391,8 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
password = u'p\u212bssword'
|
||||
request_params = {'new_password1': password, 'new_password2': password}
|
||||
confirm_request = self.request_factory.post(url, data=request_params)
|
||||
response = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token)
|
||||
confirm_request.user = self.user
|
||||
__ = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token)
|
||||
|
||||
user = User.objects.get(pk=self.user.pk)
|
||||
salt_val = user.password.split('$')[1]
|
||||
@@ -404,6 +425,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
)
|
||||
request_params = {'new_password1': password_dict['password'], 'new_password2': password_dict['password']}
|
||||
confirm_request = self.request_factory.post(url, data=request_params)
|
||||
confirm_request.user = self.user
|
||||
|
||||
# Make a password reset request with minimum/maximum passwords characters.
|
||||
response = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token)
|
||||
@@ -421,6 +443,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
kwargs={"uidb36": self.uidb36, "token": self.token}
|
||||
)
|
||||
good_reset_req = self.request_factory.get(url)
|
||||
good_reset_req.user = self.user
|
||||
password_reset_confirm_wrapper(good_reset_req, self.uidb36, self.token)
|
||||
confirm_kwargs = reset_confirm.call_args[1]
|
||||
self.assertEquals(confirm_kwargs['extra_context']['platform_name'], 'Fake University')
|
||||
@@ -445,3 +468,16 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
|
||||
subj, _, _, _ = send_email.call_args[0]
|
||||
|
||||
self.assertIn(platform_name, subj)
|
||||
|
||||
def test_reset_password_with_other_user_link(self):
|
||||
"""
|
||||
Tests that user should not be able to reset password through other user's token
|
||||
"""
|
||||
reset_url = reverse(
|
||||
"password_reset_confirm",
|
||||
kwargs={"uidb36": self.uidb36, "token": self.token}
|
||||
)
|
||||
reset_request = self.request_factory.get(reset_url)
|
||||
reset_request.user = UserFactory.create()
|
||||
|
||||
self.assertRaises(Http404, password_reset_confirm_wrapper, reset_request, self.uidb36, self.token)
|
||||
|
||||
@@ -8,7 +8,7 @@ import six
|
||||
from django.test import TestCase
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from courseware.tests.factories import InstructorFactory, StaffFactory, UserFactory
|
||||
from lms.djangoapps.courseware.tests.factories import InstructorFactory, StaffFactory, UserFactory
|
||||
from student.roles import (
|
||||
CourseBetaTesterRole,
|
||||
CourseInstructorRole,
|
||||
|
||||
@@ -242,8 +242,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
|
||||
# Assert course sharing icons
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
self.assertEqual('Share on Twitter' in response.content, set_marketing or set_social_sharing)
|
||||
self.assertEqual('Share on Facebook' in response.content, set_marketing or set_social_sharing)
|
||||
self.assertEqual('Share on Twitter' in response.content.decode('utf-8'), set_marketing or set_social_sharing)
|
||||
self.assertEqual('Share on Facebook' in response.content.decode('utf-8'), set_marketing or set_social_sharing)
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True})
|
||||
def test_pre_requisites_appear_on_dashboard(self):
|
||||
@@ -749,7 +749,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
schedule = ScheduleFactory(start=self.THREE_YEARS_AGO + timedelta(days=1), enrollment=enrollment)
|
||||
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
dashboard_html = self._remove_whitespace_from_html_string(response.content)
|
||||
dashboard_html = self._remove_whitespace_from_html_string(response.content.decode('utf-8'))
|
||||
access_expired_substring = 'Accessexpired'
|
||||
course_link_class = 'course-target-link'
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ class CourseEndingTest(TestCase):
|
||||
Tests that the higher of the persisted grade and the grade
|
||||
from the certs table is used on the learner dashboard.
|
||||
"""
|
||||
expected_grade = max(persisted_grade, cert_grade)
|
||||
expected_grade = max(filter(lambda x: x is not None, [persisted_grade, cert_grade]))
|
||||
user = Mock(username="fred", id="1")
|
||||
survey_url = "http://a_survey.com"
|
||||
course = Mock(
|
||||
@@ -395,7 +395,7 @@ class DashboardTest(ModuleStoreTestCase):
|
||||
self.assertIsNone(course_mode_info['days_for_upsell'])
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@patch('courseware.views.index.log.warning')
|
||||
@patch('lms.djangoapps.courseware.views.index.log.warning')
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
|
||||
def test_blocked_course_scenario(self, log_warning):
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import track.views
|
||||
from bulk_email.api import is_bulk_email_feature_enabled
|
||||
from bulk_email.models import Optout # pylint: disable=import-error
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.access import has_access
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
from entitlements.models import CourseEntitlement
|
||||
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
|
||||
|
||||
@@ -46,7 +46,7 @@ from six import text_type
|
||||
import track.views
|
||||
from bulk_email.models import Optout
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.courses import get_courses, sort_by_announcement, sort_by_start_date
|
||||
from lms.djangoapps.courseware.courses import get_courses, sort_by_announcement, sort_by_start_date
|
||||
from edxmako.shortcuts import marketing_link, render_to_response, render_to_string
|
||||
from entitlements.models import CourseEntitlement
|
||||
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
|
||||
@@ -137,8 +137,8 @@ def index(request, extra_context=None, user=AnonymousUser()):
|
||||
courses = get_courses(user)
|
||||
|
||||
if configuration_helpers.get_value(
|
||||
"ENABLE_COURSE_SORTING_BY_START_DATE",
|
||||
settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"],
|
||||
"ENABLE_COURSE_SORTING_BY_START_DATE",
|
||||
settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"],
|
||||
):
|
||||
courses = sort_by_start_date(courses)
|
||||
else:
|
||||
@@ -685,7 +685,6 @@ def password_change_request_handler(request):
|
||||
# no user associated with the email
|
||||
if configuration_helpers.get_value('ENABLE_PASSWORD_RESET_FAILURE_EMAIL',
|
||||
settings.FEATURES['ENABLE_PASSWORD_RESET_FAILURE_EMAIL']):
|
||||
|
||||
site = get_current_site()
|
||||
message_context = get_base_template_context(site)
|
||||
|
||||
@@ -809,6 +808,9 @@ def password_reset_confirm_wrapper(request, uidb36=None, token=None):
|
||||
|
||||
try:
|
||||
uid_int = base36_to_int(uidb36)
|
||||
if request.user.is_authenticated and request.user.id != uid_int:
|
||||
raise Http404
|
||||
|
||||
user = User.objects.get(id=uid_int)
|
||||
except (ValueError, User.DoesNotExist):
|
||||
# if there's any error getting a user, just let django's
|
||||
@@ -1167,7 +1169,7 @@ def confirm_email_change(request, key): # pylint: disable=unused-argument
|
||||
# Send it to the old email...
|
||||
try:
|
||||
ace.send(msg)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.warning('Unable to send confirmation email to old address', exc_info=True)
|
||||
response = render_to_response("email_change_failed.html", {'email': user.email})
|
||||
transaction.set_rollback(True)
|
||||
|
||||
@@ -121,7 +121,7 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler):
|
||||
}
|
||||
if status_code < 400 and content:
|
||||
headers["Content-Type"] = "application/json"
|
||||
content = json.dumps(content)
|
||||
content = json.dumps(content).encode('utf-8')
|
||||
else:
|
||||
headers["Content-Type"] = "text/html"
|
||||
|
||||
@@ -131,7 +131,7 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler):
|
||||
"""
|
||||
Create a note, assign id, annotator_schema_version, created and updated dates.
|
||||
"""
|
||||
note = json.loads(self.request_content)
|
||||
note = json.loads(self.request_content.decode('utf-8'))
|
||||
note.update({
|
||||
"id": uuid4().hex,
|
||||
"annotator_schema_version": "v1.0",
|
||||
@@ -146,7 +146,7 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler):
|
||||
The same as self._create, but it works a list of notes.
|
||||
"""
|
||||
try:
|
||||
notes = json.loads(self.request_content)
|
||||
notes = json.loads(self.request_content.decode('utf-8'))
|
||||
except ValueError:
|
||||
self.respond(400, "Bad Request")
|
||||
return
|
||||
@@ -181,7 +181,7 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler):
|
||||
"""
|
||||
Update the note by note id.
|
||||
"""
|
||||
note = self.server.update_note(note_id, json.loads(self.request_content))
|
||||
note = self.server.update_note(note_id, json.loads(self.request_content.decode('utf-8')))
|
||||
if note:
|
||||
self.respond(content=note)
|
||||
else:
|
||||
|
||||
@@ -92,7 +92,7 @@ class StubHttpRequestHandler(BaseHTTPRequestHandler, object):
|
||||
Retrieve the content of the request.
|
||||
"""
|
||||
try:
|
||||
length = int(self.headers.getheader('content-length'))
|
||||
length = int(self.headers.get('content-length'))
|
||||
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user