Merge pull request #4 from edx/master

Syncing a fork
This commit is contained in:
AmiT
2019-09-29 14:57:08 +05:30
committed by GitHub
834 changed files with 18027 additions and 10144 deletions

18
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View 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
View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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):

View File

@@ -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):

View File

@@ -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(

View File

@@ -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))

View File

@@ -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):
"""

View File

@@ -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,

View File

@@ -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))

View File

@@ -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:

View File

@@ -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

View File

@@ -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",
]

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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):

View File

@@ -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'))

View File

@@ -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'})

View File

@@ -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(

View File

@@ -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

View 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, "")

View File

@@ -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
],

View File

@@ -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)

View File

@@ -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")

View File

@@ -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')

View File

@@ -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

View File

@@ -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 = ""

View File

@@ -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',

View File

@@ -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')):

View File

@@ -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.

View File

@@ -308,3 +308,5 @@ derive_settings(__name__)
SYSTEM_WIDE_ROLE_CLASSES = os.environ.get("SYSTEM_WIDE_ROLE_CLASSES", [])
DEFAULT_MOBILE_AVAILABLE = True
PROCTORING_SETTINGS = {}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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({

View File

@@ -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';
}

View File

@@ -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({

View File

@@ -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

View File

@@ -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() {

View File

@@ -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');

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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',

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>
<% } %>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>')

View File

@@ -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 %>"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>&gt;&gt;What is the capital of Argentina?&lt;&lt;</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>&gt;&gt;What is the capital of Argentina?&lt;&lt;</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" />

View File

@@ -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 += [

View File

@@ -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)

View File

@@ -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()

View File

@@ -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):

View File

@@ -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

View File

@@ -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))

View File

@@ -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,

View File

@@ -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))

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

@@ -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'),
),
]

View File

@@ -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.
"""

View File

@@ -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):

View File

@@ -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)

View File

@@ -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):

View File

@@ -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)

View File

@@ -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):

View File

@@ -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)

View File

@@ -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,

View File

@@ -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'

View File

@@ -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):

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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