diff --git a/.eslintignore b/.eslintignore index 8277bea272..60f59bde2a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -20,13 +20,6 @@ test_root/staticfiles common/static/xmodule -# Coffeescript directories (don't lint autogenerated files) -cms/static/coffee -lms/static/coffee -common/static/coffee -common/lib/capa/capa/tests/test_files/js - - # Symlinks into common/lib/xmodule/xmodule/js cms/static/xmodule_js lms/static/xmodule_js @@ -36,27 +29,40 @@ lms/static/xmodule_js cms/djangoapps/pipeline_js/templates -# This directory is about half Coffee and half JS, things get messy here so just ignore all existing coffee paths +# These are es2015 spec files that used to be in an ignored path. +# Now they live with the rest of the code, but we want to ignore them +# until the surrounding code is es2015 and we have a chance to clean them. +# We need to ignore them here, because es2015 will cause a parse error +# even if we add an eslint-disable line to the file. +cms/static/js/spec/models/course_spec.js +cms/static/js/spec/models/metadata_spec.js +cms/static/js/spec/models/section_spec.js +cms/static/js/spec/models/settings_course_grader_spec.js +cms/static/js/spec/models/settings_grading_spec.js +cms/static/js/spec/models/textbook_spec.js +cms/static/js/spec/models/upload_spec.js +cms/static/js/spec/views/assets_squire_spec.js +cms/static/js/spec/views/course_info_spec.js +cms/static/js/spec/views/metadata_edit_spec.js +cms/static/js/spec/views/textbook_spec.js +cms/static/js/spec/views/upload_spec.js +common/lib/capa/capa/tests/test_files/js/test_problem_display.js +common/lib/capa/capa/tests/test_files/js/test_problem_generator.js +common/lib/capa/capa/tests/test_files/js/test_problem_grader.js +common/lib/capa/capa/tests/test_files/js/xproblem.js common/lib/xmodule/xmodule/js/spec/annotatable/display_spec.js common/lib/xmodule/xmodule/js/spec/capa/display_spec.js common/lib/xmodule/xmodule/js/spec/html/edit_spec.js -common/lib/xmodule/xmodule/js/spec/problem/edit_spec.js common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.js +common/lib/xmodule/xmodule/js/spec/problem/edit_spec.js common/lib/xmodule/xmodule/js/spec/tabs/edit.js +lms/static/js/spec/calculator_spec.js +lms/static/js/spec/courseware_spec.js +lms/static/js/spec/feedback_form_spec.js +lms/static/js/spec/helper.js +lms/static/js/spec/histogram_spec.js +lms/static/js/spec/modules/tab_spec.js +lms/static/js/spec/requirejs_spec.js -common/lib/xmodule/xmodule/js/src/annotatable/display.js -common/lib/xmodule/xmodule/js/src/conditional/display.js -common/lib/xmodule/xmodule/js/src/discussion/display.js -common/lib/xmodule/xmodule/js/src/html/display.js -common/lib/xmodule/xmodule/js/src/html/edit.js -common/lib/xmodule/xmodule/js/src/raw/edit/json.js -common/lib/xmodule/xmodule/js/src/raw/edit/metadata-only.js -common/lib/xmodule/xmodule/js/src/raw/edit/xml.js -common/lib/xmodule/xmodule/js/src/sequence/edit.js -common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.js -common/lib/xmodule/xmodule/js/src/vertical/edit.js - -# This file is responsible for almost half of the repo's total issues. -common/lib/xmodule/xmodule/js/src/capa/schematic.js !**/.eslintrc.js diff --git a/.gitignore b/.gitignore index 7dc4ee8dd4..dce113d0e7 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ cover_html/ reports/ jscover.log jscover.log.* +.pytest_cache/ .tddium* common/test/data/test_unicode/static/ test_root/courses/ diff --git a/AUTHORS b/AUTHORS index ad82c26e2d..5bee98e40e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -178,12 +178,12 @@ Alasdair Swan Paul Medlock-Walton Henry Tareque Eugeny Kolpakov -Omar Al-Ithawi +Omar Al-Ithawi Louis Pilfold Akiva Leffert Mike Bifulco Jim Zheng -Afzal Wali +Afzal Wali Julien Romagnoli Wenjie Wu Aamir diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000000..c593b2e916 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,368 @@ +def runPythonTests() { + ansiColor('gnome-terminal') { + sshagent(credentials: ['jenkins-worker'], ignoreMissing: true) { + checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: '${sha1}']], + doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], + userRemoteConfigs: [[credentialsId: 'jenkins-worker', + refspec: '+refs/heads/*:refs/remotes/origin/* +refs/pull/*:refs/remotes/origin/pr/*', + url: 'git@github.com:edx/edx-platform.git']]] + console_output = sh(returnStdout: true, script: 'bash scripts/all-tests.sh').trim() + dir('stdout') { + writeFile file: "${TEST_SUITE}-${SHARD}-stdout.log", text: console_output + } + stash includes: 'reports/**/*coverage*', name: "${TEST_SUITE}-${SHARD}-reports" + } + } +} + +def savePythonTestArtifacts() { + archiveArtifacts allowEmptyArchive: true, artifacts: 'reports/**/*,test_root/log/**/*.log,**/nosetests.xml,stdout/*.log,*.log' + junit '**/nosetests.xml' +} + +pipeline { + + agent { label "coverage-worker" } + + options { + timestamps() + timeout(75) + } + + stages { + stage('Run Tests') { + parallel { + stage('lms-unit-1') { + agent { label "jenkins-worker" } + environment { + SHARD = 1 + TEST_SUITE = 'lms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('lms-unit-2') { + agent { label "jenkins-worker" } + environment { + SHARD = 2 + TEST_SUITE = 'lms-unit' + } + steps{ + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('lms-unit-3') { + agent { label "jenkins-worker" } + environment { + SHARD = 3 + TEST_SUITE = 'lms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('lms-unit-4') { + agent { label "jenkins-worker" } + environment { + SHARD = 4 + TEST_SUITE = 'lms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('lms-unit-5') { + agent { label "jenkins-worker" } + environment { + SHARD = 5 + TEST_SUITE = 'lms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('lms-unit-6') { + agent { label "jenkins-worker" } + environment { + SHARD = 6 + TEST_SUITE = 'lms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('lms-unit-7') { + agent { label "jenkins-worker" } + environment { + SHARD = 7 + TEST_SUITE = 'lms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('lms-unit-8') { + agent { label "jenkins-worker" } + environment { + SHARD = 8 + TEST_SUITE = 'lms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('lms-unit-9') { + agent { label "jenkins-worker" } + environment { + SHARD = 9 + TEST_SUITE = 'lms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('lms-unit-10') { + agent { label "jenkins-worker" } + environment { + SHARD = 10 + TEST_SUITE = 'lms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('cms-unit-1') { + agent { label "jenkins-worker" } + environment { + SHARD = 1 + TEST_SUITE = 'cms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('cms-unit-2') { + agent { label "jenkins-worker" } + environment { + SHARD = 2 + TEST_SUITE = 'cms-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('commonlib-unit-1') { + agent { label "jenkins-worker" } + environment { + SHARD = 1 + TEST_SUITE = 'commonlib-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('commonlib-unit-2') { + agent { label "jenkins-worker" } + environment { + SHARD = 2 + TEST_SUITE = 'commonlib-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + stage('commonlib-unit-3') { + agent { label "jenkins-worker" } + environment { + SHARD = 3 + TEST_SUITE = 'commonlib-unit' + } + steps { + script { + runPythonTests() + } + } + post { + always { + script { + savePythonTestArtifacts() + } + } + } + } + } + } + stage('Run coverage') { + environment { + CODE_COV_TOKEN = credentials('CODE_COV_TOKEN') + TARGET_BRANCH = "origin/master" + CI_BRANCH = "${ghprbSourceBranch}" + SUBSET_JOB = "null" // Keep this variable until we can remove the $SUBSET_JOB path from .coveragerc + } + steps { + ansiColor('gnome-terminal') { + sshagent(credentials: ['jenkins-worker'], ignoreMissing: true) { + checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: '${sha1}']], + doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], + userRemoteConfigs: [[credentialsId: 'jenkins-worker', + refspec: '+refs/heads/*:refs/remotes/origin/* +refs/pull/*:refs/remotes/origin/pr/*', + url: 'git@github.com:edx/edx-platform.git']]] + unstash 'lms-unit-1-reports' + unstash 'lms-unit-2-reports' + unstash 'lms-unit-3-reports' + unstash 'lms-unit-4-reports' + unstash 'lms-unit-5-reports' + unstash 'lms-unit-6-reports' + unstash 'lms-unit-7-reports' + unstash 'lms-unit-8-reports' + unstash 'lms-unit-9-reports' + unstash 'lms-unit-10-reports' + unstash 'cms-unit-1-reports' + unstash 'cms-unit-2-reports' + unstash 'commonlib-unit-1-reports' + unstash 'commonlib-unit-2-reports' + unstash 'commonlib-unit-3-reports' + sh "./scripts/jenkins-report.sh" + } + } + } + post { + always { + publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, + reportDir: 'reports', reportFiles: 'diff_coverage_combined.html', + reportName: 'Diff Coverage Report', reportTitles: '']) + publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, + reportDir: 'reports/cover', reportFiles: 'index.html', + reportName: 'Coverage.py Report', reportTitles: '']) + } + } + } + } +} diff --git a/Makefile b/Makefile index 8501377cd4..7a3f218f53 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,14 @@ # Do things in edx-platform +.PHONY: clean extract_translations help pull_translations push_translations requirements upgrade # Careful with mktemp syntax: it has to work on Mac and Ubuntu, which have differences. PRIVATE_FILES := $(shell mktemp -u /tmp/private_files.XXXXXX) -clean: +help: ## display this help message + @echo "Please use \`make ' where is one of" + @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' + +clean: ## archive and delete most git-ignored files # Remove all the git-ignored stuff, but save and restore things marked # by start-noclean/end-noclean. Include Makefile in the tarball so that # there's always at least one file even if there are no private files. @@ -13,16 +18,13 @@ clean: tar xf $(PRIVATE_FILES) rm $(PRIVATE_FILES) -extract_translations: - # Extract localizable strings from sources +extract_translations: ## extract localizable strings from sources i18n_tool extract -vv -push_translations: - # Push source strings to Transifex for translation +push_translations: ## push source strings to Transifex for translation i18n_tool transifex push -pull_translations: - ## Pull translations from Transifex +pull_translations: ## pull translations from Transifex git clean -fdX conf/locale i18n_tool transifex pull i18n_tool extract @@ -32,3 +34,32 @@ pull_translations: git clean -fdX conf/locale/rtl git clean -fdX conf/locale/eo i18n_tool validate + +requirements: ## install development environment requirements + pip install -qr requirements/edx/development.txt --exists-action w + +upgrade: ## update the pip requirements files to use the latest releases satisfying our constraints + pip install -qr requirements/edx/pip-tools.txt + # Make sure to compile files after any other files they include! + pip-compile --upgrade -o requirements/edx/pip-tools.txt requirements/edx/pip-tools.in + pip-compile --upgrade -o requirements/edx/coverage.txt requirements/edx/coverage.in + pip-compile --upgrade -o requirements/edx/paver.txt requirements/edx/paver.in + pip-compile --upgrade -o requirements/edx-sandbox/shared.txt requirements/edx-sandbox/shared.in + pip-compile --upgrade -o requirements/edx-sandbox/base.txt requirements/edx-sandbox/base.in + pip-compile --upgrade -o requirements/edx/base.txt requirements/edx/base.in + pip-compile --upgrade -o requirements/edx/testing.txt requirements/edx/testing.in + pip-compile --upgrade -o requirements/edx/development.txt requirements/edx/development.in + # Post process all of the files generated above to work around open pip-tools issues + scripts/post-pip-compile.sh \ + requirements/edx/pip-tools.txt \ + requirements/edx/coverage.txt \ + requirements/edx/paver.txt \ + requirements/edx-sandbox/shared.txt \ + requirements/edx-sandbox/base.txt \ + requirements/edx/base.txt \ + requirements/edx/testing.txt \ + requirements/edx/development.txt + # Let tox control the Django version for tests + grep "^django==" requirements/edx/base.txt > requirements/edx/django.txt + sed '/^[dD]jango==/d' requirements/edx/testing.txt > requirements/edx/testing.tmp + mv requirements/edx/testing.tmp requirements/edx/testing.txt diff --git a/circle.yml b/circle.yml index 09fefa5a74..017a6507ee 100644 --- a/circle.yml +++ b/circle.yml @@ -12,26 +12,13 @@ dependencies: - npm install - pip install setuptools - - pip install --exists-action w -r requirements/edx/paver.txt # Mirror what paver install_prereqs does. # After a successful build, CircleCI will # cache the virtualenv at that state, so that # the next build will not need to install them # from scratch again. - - pip install --exists-action w -r requirements/edx/pre.txt - - pip install --exists-action w -r requirements/edx/github.txt - - pip install --exists-action w -r requirements/edx/local.txt - - # HACK: within base.txt stevedore had a - # dependency on a version range of pbr. - # Install a version which falls within that range. - - pip install --exists-action w pbr==0.9.0 - - pip install --exists-action w -r requirements/edx/django.txt - - pip install --exists-action w -r requirements/edx/base.txt - - pip install --exists-action w -r requirements/edx/paver.txt - pip install --exists-action w -r requirements/edx/testing.txt - - if [ -e requirements/edx/post.txt ]; then pip install --exists-action w -r requirements/edx/post.txt ; fi - pip install coveralls==1.0 diff --git a/cms/djangoapps/contentstore/api/views.py b/cms/djangoapps/contentstore/api/views.py index 5761f34d5c..aa4f20d087 100644 --- a/cms/djangoapps/contentstore/api/views.py +++ b/cms/djangoapps/contentstore/api/views.py @@ -35,7 +35,7 @@ class CourseImportExportViewMixin(DeveloperErrorViewMixin): Ensures that the user is authenticated (e.g. not an AnonymousUser) """ super(CourseImportExportViewMixin, self).perform_authentication(request) - if request.user.is_anonymous(): + if request.user.is_anonymous: raise AuthenticationFailed @@ -101,6 +101,11 @@ class CourseImportView(CourseImportExportViewMixin, GenericAPIView): } """ + # TODO: ARCH-91 + # This view is excluded from Swagger doc generation because it + # does not specify a serializer class. + exclude_from_schema = True + def post(self, request, course_id): """ Kicks off an asynchronous course import and returns an ID to be used to check diff --git a/cms/djangoapps/contentstore/config/waffle.py b/cms/djangoapps/contentstore/config/waffle.py index 28d46e18b7..f86154053c 100644 --- a/cms/djangoapps/contentstore/config/waffle.py +++ b/cms/djangoapps/contentstore/config/waffle.py @@ -2,18 +2,32 @@ This module contains various configuration settings via waffle switches for the contentstore app. """ -from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace +from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleSwitchNamespace # Namespace WAFFLE_NAMESPACE = u'studio' # Switches ENABLE_ACCESSIBILITY_POLICY_PAGE = u'enable_policy_page' -ENABLE_ASSETS_SEARCH = u'enable_assets_search' def waffle(): """ - Returns the namespaced, cached, audited Waffle class for Studio pages. + Returns the namespaced, cached, audited Waffle Switch class for Studio pages. """ return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Studio: ') + + +def waffle_flags(): + """ + Returns the namespaced, cached, audited Waffle Flag class for Studio pages. + """ + return WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Studio: ') + + +# Flags +ENABLE_IN_CONTEXT_IMAGE_SELECTION = CourseWaffleFlag( + waffle_namespace=waffle_flags(), + flag_name=u'enable_in_context_image_selection', + flag_undefined_default=False +) diff --git a/cms/djangoapps/contentstore/features/course-updates.feature b/cms/djangoapps/contentstore/features/course-updates.feature deleted file mode 100644 index f71d6c3d78..0000000000 --- a/cms/djangoapps/contentstore/features/course-updates.feature +++ /dev/null @@ -1,37 +0,0 @@ -@shard_2 -Feature: CMS.Course updates - As a course author, I want to be able to provide updates to my students - - # Internet explorer can't select all so the update appears weirdly - @skip_internetexplorer - Scenario: Users can change handouts - Given I have opened a new course in Studio - And I go to the course updates page - When I modify the handout to "
    Test
" - Then I see the handout "Test" - And I see a "saving" notification - - Scenario: Static links are rewritten when previewing handouts - Given I have opened a new course in Studio - And I go to the course updates page - When I modify the handout to "
" - # Can only do partial text matches because of the quotes with in quotes (and regexp step matching). - Then I see the handout image link "my_img.jpg" - And I change the handout from "/static/my_img.jpg" to "" - Then I see the handout image link "modified.jpg" - And when I reload the page - Then I see the handout image link "modified.jpg" - - Scenario: Users cannot save handouts with bad html until edit or update it properly - Given I have opened a new course in Studio - And I go to the course updates page - When I modify the handout to "

[LINK TEXT]

" - Then I see the handout error text - And I see handout save button disabled - When I edit the handout to "

home

" - Then I see handout save button re-enabled - When I save handout edit - # Can only do partial text matches because of the quotes with in quotes (and regexp step matching). - Then I see the handout "https://www.google.com.pk/" - And when I reload the page - Then I see the handout "https://www.google.com.pk/" diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py deleted file mode 100644 index 286d44848b..0000000000 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ /dev/null @@ -1,89 +0,0 @@ -# pylint: disable=missing-docstring - -from lettuce import step, world -from nose.tools import assert_in - -from cms.djangoapps.contentstore.features.common import get_codemirror_value, type_in_codemirror - - -@step(u'I go to the course updates page') -def go_to_updates(_step): - menu_css = 'li.nav-course-courseware' - updates_css = 'li.nav-course-courseware-updates a' - world.css_click(menu_css) - world.css_click(updates_css) - world.wait_for_visible('#course-handouts-view') - - -@step(u'I change the handout from "([^"]*)" to "([^"]*)"$') -def change_existing_handout(_step, before, after): - verify_text_in_editor_and_update('div.course-handouts .edit-button', before, after) - - -@step(u'I modify the handout to "([^"]*)"$') -def edit_handouts(_step, text): - edit_css = 'div.course-handouts > .edit-button' - world.css_click(edit_css) - change_text(text) - - -@step(u'I see the handout "([^"]*)"$') -def check_handout(_step, handout): - handout_css = 'div.handouts-content' - assert_in(handout, world.css_html(handout_css)) - - -@step(u'I see the handout image link "([^"]*)"$') -def check_handout_image_link(_step, image_file): - handout_css = 'div.handouts-content' - handout_html = world.css_html(handout_css) - asset_key = world.scenario_dict['COURSE'].id.make_asset_key(asset_type='asset', path=image_file) - assert_in(unicode(asset_key), handout_html) - - -@step(u'I see the handout error text') -def check_handout_error(_step): - handout_error_css = 'div#handout_error' - assert world.css_has_class(handout_error_css, 'is-shown') - - -@step(u'I see handout save button disabled') -def check_handout_error(_step): - handout_save_button = 'form.edit-handouts-form .save-button' - assert world.css_has_class(handout_save_button, 'is-disabled') - - -@step(u'I edit the handout to "([^"]*)"$') -def edit_handouts(_step, text): - type_in_codemirror(0, text) - - -@step(u'I see handout save button re-enabled') -def check_handout_error(_step): - handout_save_button = 'form.edit-handouts-form .save-button' - assert not world.css_has_class(handout_save_button, 'is-disabled') - - -@step(u'I save handout edit') -def check_handout_error(_step): - save_css = '.save-button' - world.css_click(save_css) - - -def change_text(text): - type_in_codemirror(0, text) - save_css = '.save-button' - world.css_click(save_css) - - -def verify_text_in_editor_and_update(button_css, before, after): - world.css_click(button_css) - text = get_codemirror_value() - assert_in(before, text) - change_text(after) - - -@step('I see a "(saving|deleting)" notification') -def i_see_a_mini_notification(_step, _type): - saving_css = '.wrapper-notification-mini' - assert world.is_css_present(saving_css) diff --git a/cms/djangoapps/contentstore/features/html-editor.py b/cms/djangoapps/contentstore/features/html-editor.py index a858fabbf0..84497bee7d 100644 --- a/cms/djangoapps/contentstore/features/html-editor.py +++ b/cms/djangoapps/contentstore/features/html-editor.py @@ -167,6 +167,10 @@ def check_toolbar_buttons(step): 'forecolor', # This is our custom "code style" button, which uses an image instead of a class. 'none', + 'alignleft', + 'aligncenter', + 'alignright', + 'alignjustify', 'bullist', 'numlist', 'outdent', diff --git a/cms/djangoapps/contentstore/management/commands/create_course.py b/cms/djangoapps/contentstore/management/commands/create_course.py index 76e1947bba..68a903326f 100644 --- a/cms/djangoapps/contentstore/management/commands/create_course.py +++ b/cms/djangoapps/contentstore/management/commands/create_course.py @@ -10,6 +10,7 @@ from django.core.management.base import BaseCommand, CommandError from contentstore.management.commands.utils import user_from_str from contentstore.views.course import create_new_course_in_store from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.exceptions import DuplicateCourseError MODULESTORE_CHOICES = (ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @@ -75,12 +76,16 @@ class Command(BaseCommand): } if name: fields["display_name"] = name - new_course = create_new_course_in_store( - storetype, - user, - org, - number, - run, - fields - ) - self.stdout.write(u"Created {}".format(text_type(new_course.id))) + + try: + new_course = create_new_course_in_store( + storetype, + user, + org, + number, + run, + fields + ) + self.stdout.write(u"Created {}".format(text_type(new_course.id))) + except DuplicateCourseError: + self.stdout.write(u"Course already exists") diff --git a/cms/djangoapps/contentstore/management/commands/migrate_transcripts.py b/cms/djangoapps/contentstore/management/commands/migrate_transcripts.py new file mode 100644 index 0000000000..9d235ea88d --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/migrate_transcripts.py @@ -0,0 +1,123 @@ +""" +Command to migrate transcripts to django storage. +""" + +import logging +from django.core.management import BaseCommand, CommandError +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.locator import CourseLocator +from cms.djangoapps.contentstore.tasks import ( + DEFAULT_ALL_COURSES, + DEFAULT_FORCE_UPDATE, + DEFAULT_COMMIT, + enqueue_async_migrate_transcripts_tasks +) +from openedx.core.lib.command_utils import get_mutually_exclusive_required_option, parse_course_keys +from openedx.core.djangoapps.video_config.models import TranscriptMigrationSetting +from xmodule.modulestore.django import modulestore + +log = logging.getLogger(__name__) + + +class Command(BaseCommand): + """ + Example usage: + $ ./manage.py cms migrate_transcripts --all-courses --force-update --commit + $ ./manage.py cms migrate_transcripts --course-id 'Course1' --course-id 'Course2' --commit + $ ./manage.py cms migrate_transcripts --from-settings + """ + help = 'Migrates transcripts to S3 for one or more courses.' + + def add_arguments(self, parser): + """ + Add arguments to the command parser. + """ + parser.add_argument( + '--course-id', '--course_id', + dest='course_ids', + action='append', + help=u'Migrates transcripts for the list of courses.' + ) + parser.add_argument( + '--all-courses', '--all', '--all_courses', + dest='all_courses', + action='store_true', + default=DEFAULT_ALL_COURSES, + help=u'Migrates transcripts to the configured django storage for all courses.' + ) + parser.add_argument( + '--from-settings', '--from_settings', + dest='from_settings', + help='Migrate Transcripts with settings set via django admin', + action='store_true', + default=False, + ) + parser.add_argument( + '--force-update', '--force_update', + dest='force_update', + action='store_true', + default=DEFAULT_FORCE_UPDATE, + help=u'Force migrate transcripts for the requested courses, overwrite if already present.' + ) + parser.add_argument( + '--commit', + dest='commit', + action='store_true', + default=DEFAULT_COMMIT, + help=u'Commits the discovered video transcripts to django storage. ' + u'Without this flag, the command will return the transcripts discovered for migration.' + ) + + def _parse_course_key(self, raw_value): + """ Parses course key from string """ + try: + result = CourseKey.from_string(raw_value) + except InvalidKeyError: + raise CommandError("Invalid course_key: '%s'." % raw_value) + + if not isinstance(result, CourseLocator): + raise CommandError(u"Argument {0} is not a course key".format(raw_value)) + + return result + + def _get_migration_options(self, options): + """ + Returns the command arguments configured via django admin. + """ + force_update = options['force_update'] + commit = options['commit'] + courses_mode = get_mutually_exclusive_required_option(options, 'course_ids', 'all_courses', 'from_settings') + if courses_mode == 'all_courses': + course_keys = [course.id for course in modulestore().get_course_summaries()] + elif courses_mode == 'course_ids': + course_keys = map(self._parse_course_key, options['course_ids']) + else: + if self._latest_settings().all_courses: + course_keys = [course.id for course in modulestore().get_course_summaries()] + else: + course_keys = parse_course_keys(self._latest_settings().course_ids.split()) + force_update = self._latest_settings().force_update + commit = self._latest_settings().commit + + return course_keys, force_update, commit + + def _latest_settings(self): + """ + Return the latest version of the TranscriptMigrationSetting + """ + return TranscriptMigrationSetting.current() + + def handle(self, *args, **options): + """ + Invokes the migrate transcripts enqueue function. + """ + course_keys, force_update, commit = self._get_migration_options(options) + kwargs = {'force_update': force_update, 'commit': commit} + try: + enqueue_async_migrate_transcripts_tasks( + course_keys, + **kwargs + ) + except InvalidKeyError as exc: + raise CommandError(u'Invalid course key: ' + unicode(exc)) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_create_course.py b/cms/djangoapps/contentstore/management/commands/tests/test_create_course.py index 8ba3b86ed3..e0fba10939 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_create_course.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_create_course.py @@ -1,6 +1,7 @@ """ Unittests for creating a course in an chosen modulestore """ +from StringIO import StringIO import ddt from django.core.management import CommandError, call_command from django.test import TestCase @@ -59,6 +60,29 @@ class TestCreateCourse(ModuleStoreTestCase): # pylint: disable=protected-access self.assertEqual(store, modulestore()._get_modulestore_for_courselike(new_key).get_modulestore_type()) + def test_duplicate_course(self): + """ + Test that creating a duplicate course exception is properly handled + """ + call_command( + "create_course", + "split", + str(self.user.email), + "org", "course", "run", "dummy-course-name" + ) + + # create the course again + out = StringIO() + call_command( + "create_course", + "split", + str(self.user.email), + "org", "course", "run", "dummy-course-name", + stderr=out + ) + expected = u"Course already exists" + self.assertIn(out.getvalue().strip(), expected) + @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) def test_get_course_with_different_case(self, default_store): """ diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py b/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py new file mode 100644 index 0000000000..c458b77911 --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +""" +Tests for course transcript migration management command. +""" +import logging +from datetime import datetime +import pytz +from django.test import TestCase +from django.core.management import call_command, CommandError +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.video_module.transcripts_utils import save_to_store +from edxval import api as api +from testfixtures import LogCapture + +LOGGER_NAME = "cms.djangoapps.contentstore.tasks" + +SRT_FILEDATA = ''' +0 +00:00:00,270 --> 00:00:02,720 +sprechen sie deutsch? + +1 +00:00:02,720 --> 00:00:05,430 +Ja, ich spreche Deutsch + +2 +00:00:6,500 --> 00:00:08,600 +可以用“我不太懂艺术 但我知道我喜欢什么”做比喻 +''' + +CRO_SRT_FILEDATA = ''' +0 +00:00:00,270 --> 00:00:02,720 +Dobar dan! + +1 +00:00:02,720 --> 00:00:05,430 +Kako ste danas? + +2 +00:00:6,500 --> 00:00:08,600 +可以用“我不太懂艺术 但我知道我喜欢什么”做比喻 +''' + + +VIDEO_DICT_STAR = dict( + client_video_id='TWINKLE TWINKLE', + duration=42.0, + edx_video_id='test_edx_video_id', + status='upload', +) + + +class TestArgParsing(TestCase): + """ + Tests for parsing arguments for the `migrate_transcripts` management command + """ + def test_no_args(self): + errstring = "Must specify exactly one of --course_ids, --all_courses, --from_settings" + with self.assertRaisesRegexp(CommandError, errstring): + call_command('migrate_transcripts') + + def test_invalid_course(self): + errstring = "Invalid course_key: 'invalid-course'." + with self.assertRaisesRegexp(CommandError, errstring): + call_command('migrate_transcripts', '--course-id', 'invalid-course') + + +class TestMigrateTranscripts(ModuleStoreTestCase): + """ + Tests migrating video transcripts in courses from contentstore to django storage + """ + def setUp(self): + """ Common setup. """ + super(TestMigrateTranscripts, self).setUp() + self.store = modulestore() + self.course = CourseFactory.create() + self.course_2 = CourseFactory.create() + + video = { + 'edx_video_id': 'test_edx_video_id', + 'client_video_id': 'test1.mp4', + 'duration': 42.0, + 'status': 'upload', + 'courses': [unicode(self.course.id)], + 'encoded_videos': [], + 'created': datetime.now(pytz.utc) + } + api.create_video(video) + + video_sample_xml = ''' + + ''' + + video_sample_xml_2 = ''' + + ''' + self.video_descriptor = ItemFactory.create( + parent_location=self.course.location, category='video', + data={'data': video_sample_xml} + ) + self.video_descriptor_2 = ItemFactory.create( + parent_location=self.course_2.location, category='video', + data={'data': video_sample_xml_2} + ) + + save_to_store(SRT_FILEDATA, 'subs_grmtran1.srt', 'text/srt', self.video_descriptor.location) + save_to_store(CRO_SRT_FILEDATA, 'subs_croatian1.srt', 'text/srt', self.video_descriptor.location) + + def test_migrated_transcripts_count_with_commit(self): + """ + Test migrating transcripts with commit + """ + # check that transcript does not exist + languages = api.get_available_transcript_languages(self.video_descriptor.edx_video_id) + self.assertEqual(len(languages), 0) + self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr')) + self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge')) + + # now call migrate_transcripts command and check the transcript availability + call_command('migrate_transcripts', '--course-id', unicode(self.course.id), '--commit') + + languages = api.get_available_transcript_languages(self.video_descriptor.edx_video_id) + self.assertEqual(len(languages), 2) + self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr')) + self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge')) + + def test_migrated_transcripts_without_commit(self): + """ + Test migrating transcripts as a dry-run + """ + # check that transcripts do not exist + languages = api.get_available_transcript_languages(self.video_descriptor.edx_video_id) + self.assertEqual(len(languages), 0) + self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr')) + self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge')) + + # now call migrate_transcripts command and check the transcript availability + call_command('migrate_transcripts', '--course-id', unicode(self.course.id)) + + # check that transcripts still do not exist + languages = api.get_available_transcript_languages(self.video_descriptor.edx_video_id) + self.assertEqual(len(languages), 0) + self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr')) + self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge')) + + def test_migrate_transcripts_availability(self): + """ + Test migrating transcripts + """ + translations = self.video_descriptor.available_translations(self.video_descriptor.get_transcripts_info()) + self.assertItemsEqual(translations, ['hr', 'ge']) + self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr')) + self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge')) + + # now call migrate_transcripts command and check the transcript availability + call_command('migrate_transcripts', '--course-id', unicode(self.course.id), '--commit') + + self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr')) + self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge')) + + def test_migrate_transcripts_idempotency(self): + """ + Test migrating transcripts multiple times + """ + translations = self.video_descriptor.available_translations(self.video_descriptor.get_transcripts_info()) + self.assertItemsEqual(translations, ['hr', 'ge']) + self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr')) + self.assertFalse(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge')) + + # now call migrate_transcripts command and check the transcript availability + call_command('migrate_transcripts', '--course-id', unicode(self.course.id), '--commit') + + self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr')) + self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge')) + + # now call migrate_transcripts command again and check the transcript availability + call_command('migrate_transcripts', '--course-id', unicode(self.course.id), '--commit') + + self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr')) + self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge')) + + # now call migrate_transcripts command with --force-update and check the transcript availability + call_command('migrate_transcripts', '--course-id', unicode(self.course.id), '--force-update', '--commit') + + self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'hr')) + self.assertTrue(api.is_transcript_available(self.video_descriptor.edx_video_id, 'ge')) + + def test_migrate_transcripts_logging(self): + """ + Test migrate transcripts logging and output + """ + expected_log = ( + (LOGGER_NAME, + 'INFO', + u'[Transcript migration] process for course {} started. Migrating 1 videos'.format( + unicode(self.course.id) + )), + (LOGGER_NAME, + 'INFO', + '[Transcript migration] Migrating 2 transcripts'), + (LOGGER_NAME, + 'INFO', + u'[Transcript migration] process for course {} ended. Processed 2 transcripts'.format( + unicode(self.course.id) + )), + (LOGGER_NAME, + 'INFO', + '[Transcript migration] Result: Language hr transcript of video test_edx_video_id will be migrated' + '\nLanguage ge transcript of video test_edx_video_id will be migrated') + ) + + with LogCapture(LOGGER_NAME, level=logging.INFO) as logger: + call_command('migrate_transcripts', '--course-id', unicode(self.course.id)) + logger.check( + *expected_log + ) + + def test_migrate_transcripts_exception_logging(self): + """ + Test migrate transcripts exception logging + """ + expected_log = ( + (LOGGER_NAME, + 'INFO', + u'[Transcript migration] process for course {} started. Migrating 1 videos'.format( + unicode(self.course_2.id) + )), + (LOGGER_NAME, + 'INFO', + '[Transcript migration] Migrating 1 transcripts'), + (LOGGER_NAME, + 'INFO', + u'[Transcript migration] process for ge transcript started'), + (LOGGER_NAME, + 'ERROR', + "[Transcript migration] Exception: u'No transcript for `ge` language'"), + (LOGGER_NAME, + 'INFO', + u'[Transcript migration] process for course {} ended. Processed 1 transcripts'.format( + unicode(self.course_2.id) + )), + (LOGGER_NAME, + 'INFO', + "[Transcript migration] Result: Failed: language ge of video test_edx_video_id_2 with exception " + "No transcript for `ge` language") + ) + + with LogCapture(LOGGER_NAME, level=logging.INFO) as logger: + call_command('migrate_transcripts', '--course-id', unicode(self.course_2.id), '--commit') + logger.check( + *expected_log + ) diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index 5c24e8f585..dfb395530e 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -13,10 +13,14 @@ from tempfile import NamedTemporaryFile, mkdtemp from celery.task import task from celery.utils.log import get_task_logger +from celery_utils.chordable_django_backend import chord, chord_task +from celery_utils.persist_on_failure import LoggedPersistOnFailureTask from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.auth.models import User from django.core.exceptions import SuspiciousOperation from django.core.files import File +from django.core.files.base import ContentFile from django.test import RequestFactory from django.utils.text import get_valid_filename from django.utils.translation import ugettext as _ @@ -47,10 +51,243 @@ from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError from xmodule.modulestore.xml_exporter import export_course_to_xml, export_library_to_xml from xmodule.modulestore.xml_importer import import_course_from_xml, import_library_from_xml +from xmodule.video_module.transcripts_utils import ( + Transcript, + clean_video_id, + get_transcript_from_contentstore, + TranscriptsGenerationException +) +from xmodule.modulestore import ModuleStoreEnum +from xmodule.exceptions import NotFoundError +from edxval.api import ( + ValCannotCreateError, + create_video_transcript, + is_transcript_available, + create_or_update_video_transcript, + create_external_video, +) + +User = get_user_model() LOGGER = get_task_logger(__name__) FILE_READ_CHUNK = 1024 # bytes FULL_COURSE_REINDEX_THRESHOLD = 1 +DEFAULT_ALL_COURSES = False +DEFAULT_FORCE_UPDATE = False +DEFAULT_COMMIT = False + +RETRY_DELAY_SECONDS = 30 +COURSE_LEVEL_TIMEOUT_SECONDS = 1200 +VIDEO_LEVEL_TIMEOUT_SECONDS = 300 + + +def enqueue_async_migrate_transcripts_tasks( + course_keys, + force_update=DEFAULT_FORCE_UPDATE, + commit=DEFAULT_COMMIT +): + """ + Fires new Celery tasks for all the input courses or for all courses. + + Arguments: + course_keys: Command line course ids as list of CourseKey objects, + force_update: Overwrite file in S3. Default is False, + commit: Update S3 or dry-run the command to see which transcripts will be affected. Default is False. + """ + kwargs = { + 'force_update': force_update, + 'commit': commit + } + + tasks = [ + async_migrate_transcript.s( + unicode(course_key), + **kwargs + ) for course_key in course_keys + ] + callback = task_status_callback.s() + status = chord(tasks)(callback) + for res in status.get(): + LOGGER.info("[Transcript migration] Result: %s", '\n'.join(res)) + + +@chord_task +def task_status_callback(results): + """ + Callback for collating the results of chord. + """ + return results + + +@chord_task( + bind=True, + base=LoggedPersistOnFailureTask, + default_retry_delay=RETRY_DELAY_SECONDS, + max_retries=1, + time_limit=COURSE_LEVEL_TIMEOUT_SECONDS +) +def async_migrate_transcript(self, course_key, **kwargs): + #pylint: disable=unused-argument + """ + Migrates the transcripts of all videos in a course as a new celery task. + """ + try: + if not modulestore().get_course(CourseKey.from_string(course_key)): + raise KeyError(u'Invalid course key: ' + unicode(course_key)) + except KeyError as exc: + LOGGER.exception('[Transcript migration] Exception: %r', text_type(exc)) + return 'Failed: course {course_key} with exception {exception}'.format( + course_key=course_key, + exception=text_type(exc) + ) + force_update = kwargs['force_update'] + sub_tasks = [] + + all_videos = get_videos_from_store(CourseKey.from_string(course_key)) + LOGGER.info( + "[Transcript migration] process for course %s started. Migrating %s videos", + course_key, + len(all_videos) + ) + for video in all_videos: + all_lang_transcripts = video.transcripts + english_transcript = video.sub + if english_transcript: + all_lang_transcripts.update({'en': video.sub}) + for lang, _ in all_lang_transcripts.items(): + transcript_already_present = is_transcript_available( + clean_video_id(video.edx_video_id), + lang + ) + if transcript_already_present and force_update: + sub_tasks.append(async_migrate_transcript_subtask.s( + video, lang, True, **kwargs + )) + elif not transcript_already_present: + sub_tasks.append(async_migrate_transcript_subtask.s( + video, lang, False, **kwargs + )) + LOGGER.info("[Transcript migration] Migrating %s transcripts", len(sub_tasks)) + callback = task_status_callback.s() + status = chord(sub_tasks)(callback) + LOGGER.info( + "[Transcript migration] process for course %s ended. Processed %s transcripts", + course_key, + len(status.get()) + ) + return status.get() + + +def get_videos_from_store(course_key): + """ + Returns all videos in a course as list. + + Arguments: + course_key: CourseKey object + """ + store = modulestore() + all_videos = [] + for video in store.get_items(course_key, qualifiers={'category': 'video'}, + revision=ModuleStoreEnum.RevisionOption.published_only, include_orphans=False): + all_videos.append(video) + + for video in store.get_items(course_key, qualifiers={'category': 'video'}, + revision=ModuleStoreEnum.RevisionOption.draft_only, include_orphans=False): + all_videos.append(video) + + return all_videos + + +@chord_task( + bind=True, + base=LoggedPersistOnFailureTask, + default_retry_delay=RETRY_DELAY_SECONDS, + max_retries=2, + time_limit=VIDEO_LEVEL_TIMEOUT_SECONDS +) +def async_migrate_transcript_subtask(self, *args, **kwargs): + #pylint: disable=unused-argument + """ + Migrates a transcript of a given video in a course as a new celery task. + """ + video, language_code, force_update = args + commit = kwargs['commit'] + result = None + if commit is not True: + return 'Language {0} transcript of video {1} will be migrated'.format( + language_code, + video.edx_video_id + ) + LOGGER.info("[Transcript migration] process for %s transcript started", language_code) + try: + transcript_info = video.get_transcripts_info() + transcript_content, _, _ = get_transcript_from_contentstore( + video, language_code, Transcript.SJSON, transcript_info) + edx_video_id = clean_video_id(video.edx_video_id) + + if not edx_video_id: + video.edx_video_id = create_external_video('external-video') + video.save_with_metadata(user=User.objects.get(username='staff')) + if edx_video_id: + result = save_transcript_to_storage( + edx_video_id, + language_code, + transcript_content, + Transcript.SJSON, + force_update + ) + except (NotFoundError, TranscriptsGenerationException, ValCannotCreateError) as exc: + LOGGER.exception('[Transcript migration] Exception: %r', text_type(exc)) + return 'Failed: language {language} of video {video} with exception {exception}'.format( + language=language_code, + video=video.edx_video_id, + exception=text_type(exc) + ) + LOGGER.info("[Transcript migration] process for %s transcript ended", language_code) + if result is not None: + return 'Success: language {0} of video {1}'.format(language_code, video.edx_video_id) + else: + return 'Failed: language {0} of video {1}'.format(language_code, video.edx_video_id) + + +def save_transcript_to_storage( + edx_video_id, + language_code, + transcript_content, + file_format=Transcript.SJSON, + force_update=False +): + """ + Pushes a given transcript's data to django storage. + """ + try: + result = None + edx_video_id = clean_video_id(edx_video_id) + if force_update: + result = create_or_update_video_transcript( + edx_video_id, + language_code, + dict({'file_format': file_format}), + ContentFile(transcript_content) + ) + LOGGER.info("[Transcript migration] save_transcript_to_storage %s for %s with create_or_update method", + True if result else False, edx_video_id) + else: + result = create_video_transcript( + edx_video_id, + language_code, + file_format, + ContentFile(transcript_content) + ) + LOGGER.info( + "[Transcript migration] save_transcript_to_storage %s for %s with create method", + result, + edx_video_id + ) + return result + except ValCannotCreateError as err: + LOGGER.exception("[Transcript migration] save_transcript_to_storage_failed: %s", err) + raise def clone_instance(instance, field_values): diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 21351cb0a1..d5c3bce6e3 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -92,6 +92,8 @@ class ImportRequiredTestCases(ContentStoreTestCase): """ Tests which legitimately need to import a course """ + shard = 1 + def test_no_static_link_rewrites_on_import(self): course_items = import_course_from_xml( self.store, self.user.id, TEST_DATA_DIR, ['toy'], create_if_not_present=True @@ -609,6 +611,8 @@ class MiscCourseTests(ContentStoreTestCase): """ Tests that rely on the toy courses. """ + shard = 1 + def setUp(self): super(MiscCourseTests, self).setUp() # save locs not items b/c the items won't have the subsequently created children in them until refetched @@ -1157,6 +1161,7 @@ class ContentStoreTest(ContentStoreTestCase): """ Tests for the CMS ContentStore application. """ + shard = 1 duplicate_course_error = ("There is already a course defined with the same organization and course number. " "Please change either organization or course number to be unique.") @@ -1805,6 +1810,7 @@ class ContentStoreTest(ContentStoreTestCase): class MetadataSaveTestCase(ContentStoreTestCase): """Test that metadata is correctly cached and decached.""" + shard = 1 def setUp(self): super(MetadataSaveTestCase, self).setUp() @@ -1866,6 +1872,8 @@ class RerunCourseTest(ContentStoreTestCase): """ Tests for Rerunning a course via the view handler """ + shard = 1 + def setUp(self): super(RerunCourseTest, self).setUp() self.destination_course_data = { @@ -1950,6 +1958,23 @@ class RerunCourseTest(ContentStoreTestCase): self.assertEqual(0, len(videos)) self.assertInCourseListing(destination_course_key) + def test_rerun_course_video_upload_token(self): + """ + Test when rerunning a course with video upload token, video upload token is not copied to new course. + """ + # Create a course with video upload token. + source_course = CourseFactory.create(video_upload_pipeline={"course_video_upload_token": 'test-token'}) + + destination_course_key = self.post_rerun_request(source_course.id) + self.verify_rerun_course(source_course.id, destination_course_key, self.destination_course_data['display_name']) + self.assertInCourseListing(destination_course_key) + + # Verify video upload pipeline is empty. + source_course = self.store.get_course(source_course.id) + new_course = self.store.get_course(destination_course_key) + self.assertDictEqual(source_course.video_upload_pipeline, {"course_video_upload_token": 'test-token'}) + self.assertEqual(new_course.video_upload_pipeline, {}) + def test_rerun_course_success(self): source_course = CourseFactory.create() create_video( @@ -1970,6 +1995,10 @@ class RerunCourseTest(ContentStoreTestCase): self.assertEqual(1, len(source_videos)) self.assertEqual(source_videos, target_videos) + # Verify that video upload token is empty for rerun. + new_course = self.store.get_course(destination_course_key) + self.assertEqual(new_course.video_upload_pipeline, {}) + def test_rerun_course_resets_advertised_date(self): source_course = CourseFactory.create(advertised_start="01-12-2015") destination_course_key = self.post_rerun_request(source_course.id) @@ -2108,6 +2137,8 @@ class ContentLicenseTest(ContentStoreTestCase): """ Tests around content licenses """ + shard = 1 + def test_course_license_export(self): content_store = contentstore() root_dir = path(mkdtemp_clean()) @@ -2146,6 +2177,8 @@ class EntryPageTestCase(TestCase): """ Tests entry pages that aren't specific to a course. """ + shard = 1 + def setUp(self): super(EntryPageTestCase, self).setUp() self.client = AjaxEnabledTestClient() @@ -2180,6 +2213,7 @@ class SigninPageTestCase(TestCase): important to make sure that the script is functional independently of any other script. """ + shard = 1 def test_csrf_token_is_present_in_form(self): # Expected html: diff --git a/cms/djangoapps/contentstore/tests/test_course_listing.py b/cms/djangoapps/contentstore/tests/test_course_listing.py index 77a09e0138..60f18dcd6d 100644 --- a/cms/djangoapps/contentstore/tests/test_course_listing.py +++ b/cms/djangoapps/contentstore/tests/test_course_listing.py @@ -6,7 +6,6 @@ import random import ddt from ccx_keys.locator import CCXLocator -from chrono import Timer from django.conf import settings from django.test import RequestFactory from mock import Mock, patch diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index a8d1dc765d..f4c230eeff 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -41,6 +41,8 @@ class CourseSettingsEncoderTest(CourseTestCase): """ Tests for CourseSettingsEncoder. """ + shard = 1 + def test_encoder(self): details = CourseDetails.fetch(self.course.id) jsondetails = json.dumps(details, cls=CourseSettingsEncoder) @@ -87,6 +89,8 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): """ Tests for modifying content on the first course settings page (course dates, overview, etc.). """ + shard = 1 + def alter_field(self, url, details, field, val): """ Change the one field to the given value and then invoke the update post to see if it worked. @@ -435,6 +439,8 @@ class CourseGradingTest(CourseTestCase): """ Tests for the course settings grading page. """ + shard = 1 + def test_initial_grader(self): test_grader = CourseGradingModel(self.course) self.assertIsNotNone(test_grader.graders) @@ -769,6 +775,8 @@ class CourseMetadataEditingTest(CourseTestCase): """ Tests for CourseMetadata. """ + shard = 1 + def setUp(self): CourseTestCase.setUp(self) self.fullcourse = CourseFactory.create() @@ -1158,6 +1166,8 @@ class CourseGraderUpdatesTest(CourseTestCase): """ Test getting, deleting, adding, & updating graders """ + shard = 1 + def setUp(self): """Compute the url to use in tests""" super(CourseGraderUpdatesTest, self).setUp() @@ -1223,6 +1233,8 @@ class CourseEnrollmentEndFieldTest(CourseTestCase): Base class to test the enrollment end fields in the course settings details view in Studio when using marketing site flag and global vs non-global staff to access the page. """ + shard = 1 + NOT_EDITABLE_HELPER_MESSAGE = "Contact your edX partner manager to update these settings." NOT_EDITABLE_DATE_WRAPPER = "
" NOT_EDITABLE_TIME_WRAPPER = "
" diff --git a/cms/djangoapps/contentstore/tests/test_courseware_index.py b/cms/djangoapps/contentstore/tests/test_courseware_index.py index a00c9f6061..035513d869 100644 --- a/cms/djangoapps/contentstore/tests/test_courseware_index.py +++ b/cms/djangoapps/contentstore/tests/test_courseware_index.py @@ -185,6 +185,7 @@ class MixedWithOptionsTestCase(MixedSplitTestCase): @ddt.ddt class TestCoursewareSearchIndexer(MixedWithOptionsTestCase): """ Tests the operation of the CoursewareSearchIndexer """ + shard = 1 WORKS_WITH_STORES = (ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @@ -605,6 +606,7 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase): @ddt.ddt class TestLargeCourseDeletions(MixedWithOptionsTestCase): """ Tests to excerise deleting items from a course """ + shard = 1 WORKS_WITH_STORES = (ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @@ -688,6 +690,7 @@ class TestTaskExecution(SharedModuleStoreTestCase): being present, which allows us to ensure that when the listener is executed, it is done as expected. """ + shard = 1 @classmethod def setUpClass(cls): @@ -782,6 +785,7 @@ class TestTaskExecution(SharedModuleStoreTestCase): @ddt.ddt class TestLibrarySearchIndexer(MixedWithOptionsTestCase): """ Tests the operation of the CoursewareSearchIndexer """ + shard = 1 # libraries work only with split, so do library indexer WORKS_WITH_STORES = (ModuleStoreEnum.Type.split, ) @@ -955,6 +959,8 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): """ Tests indexing of content groups on course modules using mongo modulestore. """ + shard = 1 + MODULESTORE = TEST_DATA_MONGO_MODULESTORE INDEX_NAME = CoursewareSearchIndexer.INDEX_NAME diff --git a/cms/djangoapps/contentstore/tests/test_libraries.py b/cms/djangoapps/contentstore/tests/test_libraries.py index 455f562f7a..dfeccb82ae 100644 --- a/cms/djangoapps/contentstore/tests/test_libraries.py +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -36,6 +36,8 @@ class LibraryTestCase(ModuleStoreTestCase): """ Common functionality for content libraries tests """ + shard = 1 + def setUp(self): super(LibraryTestCase, self).setUp() @@ -148,6 +150,8 @@ class TestLibraries(LibraryTestCase): """ High-level tests for libraries """ + shard = 1 + @ddt.data( (2, 1, 1), (2, 2, 2), @@ -480,6 +484,8 @@ class TestLibraryAccess(SignalDisconnectTestMixin, LibraryTestCase): """ Test Roles and Permissions related to Content Libraries """ + shard = 1 + def setUp(self): """ Create a library, staff user, and non-staff user """ super(TestLibraryAccess, self).setUp() @@ -813,6 +819,8 @@ class TestOverrides(LibraryTestCase): """ Test that overriding block Scope.settings fields from a library in a specific course works """ + shard = 1 + def setUp(self): super(TestOverrides, self).setUp() self.original_display_name = "A Problem Block" @@ -997,6 +1005,8 @@ class TestIncompatibleModuleStore(LibraryTestCase): """ Tests for proper validation errors with an incompatible course modulestore. """ + shard = 1 + def setUp(self): super(TestIncompatibleModuleStore, self).setUp() # Create a course in an incompatible modulestore. diff --git a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py index bc67a6aad2..2a004433f8 100644 --- a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py +++ b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py @@ -191,7 +191,9 @@ class TestYoutubeSubsBase(SharedModuleStoreTestCase): @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class TestDownloadYoutubeSubs(TestYoutubeSubsBase): - """Tests for `download_youtube_subs` function.""" + """ + Tests for `download_youtube_subs` function. + """ org = 'MITx' number = '999' @@ -238,13 +240,6 @@ class TestDownloadYoutubeSubs(TestYoutubeSubsBase): mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_2'}) - # Check asset status after import of transcript. - filename = 'subs_{0}.srt.sjson'.format(good_youtube_sub) - content_location = StaticContent.compute_location(self.course.id, filename) - self.assertTrue(contentstore().find(content_location)) - - self.clear_sub_content(good_youtube_sub) - def test_subs_for_html5_vid_with_periods(self): """ This is to verify a fix whereby subtitle files uploaded against @@ -269,16 +264,6 @@ class TestDownloadYoutubeSubs(TestYoutubeSubsBase): with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException): transcripts_utils.download_youtube_subs(bad_youtube_sub, self.course, settings) - # Check asset status after import of transcript. - filename = 'subs_{0}.srt.sjson'.format(bad_youtube_sub) - content_location = StaticContent.compute_location( - self.course.id, filename - ) - with self.assertRaises(NotFoundError): - contentstore().find(content_location) - - self.clear_sub_content(bad_youtube_sub) - def test_success_downloading_chinese_transcripts(self): # Disabled 11/14/13 @@ -367,13 +352,6 @@ class TestDownloadYoutubeSubs(TestYoutubeSubsBase): params={'lang': 'en', 'v': 'good_id_2', 'name': 'Custom'} ) - # Check asset status after import of transcript. - filename = 'subs_{0}.srt.sjson'.format(good_youtube_sub) - content_location = StaticContent.compute_location(self.course.id, filename) - self.assertTrue(contentstore().find(content_location)) - - self.clear_sub_content(good_youtube_sub) - class TestGenerateSubsFromSource(TestDownloadYoutubeSubs): """Tests for `generate_subs_from_source` function.""" @@ -766,7 +744,7 @@ class TestGetTranscript(SharedModuleStoreTestCase): edx_video_id=u'1234-5678-90' ) - def create_transcript(self, subs_id, language=u'en', filename='video.srt'): + def create_transcript(self, subs_id, language=u'en', filename='video.srt', youtube_id_1_0='', html5_sources=None): """ create transcript. """ @@ -774,21 +752,26 @@ class TestGetTranscript(SharedModuleStoreTestCase): if language != u'en': transcripts = {language: filename} + html5_sources = html5_sources or [] self.video = ItemFactory.create( category='video', parent_location=self.vertical.location, sub=subs_id, + youtube_id_1_0=youtube_id_1_0, transcripts=transcripts, - edx_video_id=u'1234-5678-90' + edx_video_id=u'1234-5678-90', + html5_sources=html5_sources ) - if subs_id: - transcripts_utils.save_subs_to_store( - self.subs_sjson, - subs_id, - self.video, - language=language, - ) + possible_subs = [subs_id, youtube_id_1_0] + transcripts_utils.get_html5_ids(html5_sources) + for possible_sub in possible_subs: + if possible_sub: + transcripts_utils.save_subs_to_store( + self.subs_sjson, + possible_sub, + self.video, + language=language, + ) def create_srt_file(self, content): """ @@ -834,31 +817,69 @@ class TestGetTranscript(SharedModuleStoreTestCase): ) @ddt.data( + # video.sub transcript { 'language': u'en', 'subs_id': 'video_101', - 'filename': 'en_video_101.srt', + 'youtube_id_1_0': '', + 'html5_sources': [], + 'expected_filename': 'en_video_101.srt', }, + # if video.sub is present, rest will be skipped. + { + 'language': u'en', + 'subs_id': 'video_101', + 'youtube_id_1_0': 'test_yt_id', + 'html5_sources': ['www.abc.com/foo.mp4'], + 'expected_filename': 'en_video_101.srt', + }, + # video.youtube_id_1_0 transcript + { + 'language': u'en', + 'subs_id': '', + 'youtube_id_1_0': 'test_yt_id', + 'html5_sources': [], + 'expected_filename': 'en_test_yt_id.srt', + }, + # video.html5_sources transcript + { + 'language': u'en', + 'subs_id': '', + 'youtube_id_1_0': '', + 'html5_sources': ['www.abc.com/foo.mp4'], + 'expected_filename': 'en_foo.srt', + }, + # non-english transcript { 'language': u'ur', 'subs_id': '', - 'filename': 'ur_video_101.srt', + 'youtube_id_1_0': '', + 'html5_sources': [], + 'expected_filename': 'ur_video_101.srt', }, ) @ddt.unpack - def test_get_transcript_from_content_store(self, language, subs_id, filename): + def test_get_transcript_from_contentstore( + self, + language, + subs_id, + youtube_id_1_0, + html5_sources, + expected_filename + ): """ Verify that `get_transcript` function returns correct data when transcript is in content store. """ - self.upload_file(self.create_srt_file(self.subs_srt), self.video.location, filename) - self.create_transcript(subs_id, language, filename) - content, filename, mimetype = transcripts_utils.get_transcript( + base_filename = 'video_101.srt' + self.upload_file(self.create_srt_file(self.subs_srt), 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, language ) self.assertEqual(content, self.subs[language]) - self.assertEqual(filename, filename) + self.assertEqual(file_name, expected_filename) self.assertEqual(mimetype, self.srt_mime_type) def test_get_transcript_from_content_store_for_ur(self): @@ -938,3 +959,43 @@ class TestGetTranscript(SharedModuleStoreTestCase): exception_message = text_type(no_en_transcript_exception.exception) self.assertEqual(exception_message, 'No transcript for `en` language') + + @ddt.data( + transcripts_utils.TranscriptsGenerationException, + UnicodeDecodeError('aliencodec', b'\x02\x01', 1, 2, 'alien codec found!') + ) + @patch('xmodule.video_module.transcripts_utils.Transcript') + def test_get_transcript_val_exceptions(self, exception_to_raise, mock_Transcript): + """ + Verify that `get_transcript_from_val` function raises `NotFoundError` when specified exceptions raised. + """ + mock_Transcript.convert.side_effect = exception_to_raise + transcripts_info = self.video.get_transcripts_info() + lang = self.video.get_default_transcript_language(transcripts_info) + edx_video_id = transcripts_utils.clean_video_id(self.video.edx_video_id) + with self.assertRaises(NotFoundError): + transcripts_utils.get_transcript_from_val( + edx_video_id, + lang=lang, + output_format=transcripts_utils.Transcript.SRT + ) + + @ddt.data( + transcripts_utils.TranscriptsGenerationException, + UnicodeDecodeError('aliencodec', b'\x02\x01', 1, 2, 'alien codec found!') + ) + @patch('xmodule.video_module.transcripts_utils.Transcript') + def test_get_transcript_content_store_exceptions(self, exception_to_raise, mock_Transcript): + """ + Verify that `get_transcript_from_contentstore` function raises `NotFoundError` when specified exceptions raised. + """ + mock_Transcript.asset.side_effect = exception_to_raise + transcripts_info = self.video.get_transcripts_info() + lang = self.video.get_default_transcript_language(transcripts_info) + with self.assertRaises(NotFoundError): + transcripts_utils.get_transcript_from_contentstore( + self.video, + language=lang, + output_format=transcripts_utils.Transcript.SRT, + transcripts_info=transcripts_info + ) diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index 2a4157f2b1..dc2ce98466 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -313,7 +313,12 @@ class CourseTestCase(ProceduralCourseTestMixin, ModuleStoreTestCase): self.assertEqual(course1_item.data, course2_item.data) # compare meta-data - self.assertEqual(own_metadata(course1_item), own_metadata(course2_item)) + course1_metadata = own_metadata(course1_item) + course2_metadata = own_metadata(course2_item) + # Omit edx_video_id as it can be different in case of extrnal video imports. + course1_metadata.pop('edx_video_id', None) + course2_metadata.pop('edx_video_id', None) + self.assertEqual(course1_metadata, course2_metadata) # compare children self.assertEqual(course1_item.has_children, course2_item.has_children) diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index c6a6ed84a3..f65f281151 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -150,6 +150,7 @@ def container_handler(request, usage_key_string): index += 1 return render_to_response('container.html', { + 'language_code': request.LANGUAGE_CODE, 'context_course': course, # Needed only for display of menus at top of page. 'action': action, 'xblock': xblock, diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index d8a45c0fa9..354efd8fb9 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -5,6 +5,7 @@ import copy import json import logging import random +import re import string # pylint: disable=deprecated-module import django.utils @@ -56,7 +57,6 @@ from milestones import api as milestones_api from models.settings.course_grading import CourseGradingModel from models.settings.course_metadata import CourseMetadata from models.settings.encoder import CourseSettingsEncoder -from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements from openedx.core.djangoapps.models.course_details import CourseDetails @@ -586,13 +586,18 @@ def _deprecated_blocks_info(course_module, deprecated_block_types): 'advance_settings_url': reverse_course_url('advanced_settings_handler', course_module.id) } - try: - structure_data = api.course_structure(course_module.id, block_types=deprecated_block_types) - except errors.CourseStructureNotAvailableError: - return data + deprecated_blocks = modulestore().get_items( + course_module.id, + qualifiers={ + 'category': re.compile('^' + '$|^'.join(deprecated_block_types) + '$') + } + ) - for block in structure_data['blocks'].values(): - data['blocks'].append([reverse_usage_url('container_handler', block['parent']), block['display_name']]) + for block in deprecated_blocks: + data['blocks'].append([ + reverse_usage_url('container_handler', block.parent), + block.display_name + ]) return data @@ -906,6 +911,7 @@ def rerun_course(user, source_course_key, org, number, run, fields, async=True): # Clear the fields that must be reset for the rerun fields['advertised_start'] = None + fields['video_upload_pipeline'] = {} json_fields = json.dumps(fields, cls=EdxJSONEncoder) args = [unicode(source_course_key), unicode(destination_course_key), user.id, json_fields] diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py index f054564534..0a7e568ac7 100644 --- a/cms/djangoapps/contentstore/views/public.py +++ b/cms/djangoapps/contentstore/views/public.py @@ -24,7 +24,7 @@ def signup(request): Display the signup form. """ csrf_token = csrf(request)['csrf_token'] - if request.user.is_authenticated(): + if request.user.is_authenticated: return redirect('/course/') if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'): # Redirect to course to login to process their certificate if SSL is enabled @@ -68,7 +68,7 @@ def login_page(request): def howitworks(request): "Proxy view" - if request.user.is_authenticated(): + if request.user.is_authenticated: return redirect('/home/') else: return render_to_response('howitworks.html', {}) diff --git a/cms/djangoapps/contentstore/views/tests/test_certificates.py b/cms/djangoapps/contentstore/views/tests/test_certificates.py index ecc5e067d9..a0de7d9b44 100644 --- a/cms/djangoapps/contentstore/views/tests/test_certificates.py +++ b/cms/djangoapps/contentstore/views/tests/test_certificates.py @@ -106,6 +106,7 @@ class CertificatesBaseTestCase(object): """ Mixin with base test cases for the certificates. """ + shard = 1 def _remove_ids(self, content): """ @@ -199,6 +200,8 @@ class CertificatesListHandlerTestCase( """ Test cases for certificates_list_handler. """ + shard = 1 + def setUp(self): """ Set up CertificatesListHandlerTestCase. @@ -425,6 +428,7 @@ class CertificatesDetailHandlerTestCase( """ Test cases for CertificatesDetailHandlerTestCase. """ + shard = 1 _id = 0 diff --git a/cms/djangoapps/contentstore/views/tests/test_container_page.py b/cms/djangoapps/contentstore/views/tests/test_container_page.py index 76f0c186fa..432247f099 100644 --- a/cms/djangoapps/contentstore/views/tests/test_container_page.py +++ b/cms/djangoapps/contentstore/views/tests/test_container_page.py @@ -211,6 +211,7 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase): """ request = RequestFactory().get('foo') request.user = self.user + request.LANGUAGE_CODE = 'en' # Check for invalid 'usage_key_strings' self.assertRaises( diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index dababa230e..c68066f4e0 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -42,6 +42,8 @@ class TestCourseIndex(CourseTestCase): """ Unit tests for getting the list of courses and the course outline. """ + shard = 1 + def setUp(self): """ Add a course with odd characters in the fields @@ -315,6 +317,8 @@ class TestCourseIndexArchived(CourseTestCase): """ Unit tests for testing the course index list when there are archived courses. """ + shard = 1 + NOW = datetime.datetime.now(pytz.utc) DAY = datetime.timedelta(days=1) YESTERDAY = NOW - DAY @@ -426,6 +430,7 @@ class TestCourseOutline(CourseTestCase): """ Unit tests for the course outline. """ + shard = 1 ENABLED_SIGNALS = ['course_published'] def setUp(self): @@ -628,79 +633,12 @@ class TestCourseOutline(CourseTestCase): expected_block_types ) - @ddt.data( - {'delete_vertical': True}, - {'delete_vertical': False}, - ) - @ddt.unpack - def test_deprecated_blocks_list_updated_correctly(self, delete_vertical): - """ - Verify that deprecated blocks list shown on banner is updated correctly. - - Here is the scenario: - This list of deprecated blocks shown on banner contains published - and un-published blocks. That list should be updated when we delete - un-published block(s). This behavior should be same if we delete - unpublished vertical or problem. - """ - block_types = ['notes'] - course_module = modulestore().get_item(self.course.location) - - vertical1 = ItemFactory.create( - parent_location=self.sequential.location, category='vertical', display_name='Vert1 Subsection1' - ) - problem1 = ItemFactory.create( - parent_location=vertical1.location, - category='notes', - display_name='notes problem in vert1', - publish_item=False - ) - - info = _deprecated_blocks_info(course_module, block_types) - # info['blocks'] should be empty here because there is nothing - # published or un-published present - self.assertEqual(info['blocks'], []) - - vertical2 = ItemFactory.create( - parent_location=self.sequential.location, category='vertical', display_name='Vert2 Subsection1' - ) - ItemFactory.create( - parent_location=vertical2.location, - category='notes', - display_name='notes problem in vert2', - pubish_item=True - ) - # At this point CourseStructure will contain both the above - # published and un-published verticals - - info = _deprecated_blocks_info(course_module, block_types) - self.assertItemsEqual( - info['blocks'], - [ - [reverse_usage_url('container_handler', vertical1.location), 'notes problem in vert1'], - [reverse_usage_url('container_handler', vertical2.location), 'notes problem in vert2'] - ] - ) - - # Delete the un-published vertical or problem so that CourseStructure updates its data - if delete_vertical: - self.store.delete_item(vertical1.location, self.user.id) - else: - self.store.delete_item(problem1.location, self.user.id) - - info = _deprecated_blocks_info(course_module, block_types) - # info['blocks'] should only contain the info about vertical2 which is published. - # There shouldn't be any info present about un-published vertical1 - self.assertEqual( - info['blocks'], - [[reverse_usage_url('container_handler', vertical2.location), 'notes problem in vert2']] - ) - class TestCourseReIndex(CourseTestCase): """ Unit tests for the course outline. """ + shard = 1 SUCCESSFUL_RESPONSE = _("Course has been successfully reindexed.") ENABLED_SIGNALS = ['course_published'] diff --git a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py index 2f6110430b..38aadf2886 100644 --- a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py +++ b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py @@ -168,6 +168,8 @@ class GroupConfigurationsBaseTestCase(object): """ Mixin with base test cases for the group configurations. """ + shard = 1 + def _remove_ids(self, content): """ Remove ids from the response. We cannot predict IDs, because they're @@ -240,6 +242,8 @@ class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurations """ Test cases for group_configurations_list_handler. """ + shard = 1 + def _url(self): """ Return url for the handler. @@ -331,6 +335,7 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio Test cases for group_configurations_detail_handler. """ + shard = 1 ID = 0 def _url(self, cid=-1): @@ -634,6 +639,8 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods): """ Tests for usage information of configurations and content groups. """ + shard = 1 + def _get_user_partition(self, scheme): """ Returns the first user partition with the specified scheme. @@ -1066,6 +1073,8 @@ class GroupConfigurationsValidationTestCase(CourseTestCase, HelperMethods): """ Tests for validation in Group Configurations. """ + shard = 1 + @patch('xmodule.split_test_module.SplitTestDescriptor.validate_split_test') def verify_validation_add_usage_info(self, expected_result, mocked_message, mocked_validation_messages): """ diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 8dde922dc0..ea7b8fcf7f 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -125,6 +125,7 @@ class ItemTest(CourseTestCase): @ddt.ddt class GetItemTest(ItemTest): """Tests for '/xblock' GET url.""" + shard = 1 def _get_preview(self, usage_key, data=None): """ Makes a request to xblock preview handler """ @@ -469,6 +470,8 @@ class GetItemTest(ItemTest): @ddt.ddt class DeleteItem(ItemTest): """Tests for '/xblock' DELETE url.""" + shard = 1 + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_delete_static_page(self, store): course = CourseFactory.create(default_store=store) @@ -485,6 +488,8 @@ class TestCreateItem(ItemTest): """ Test the create_item handler thoroughly """ + shard = 1 + def test_create_nicely(self): """ Try the straightforward use cases @@ -660,6 +665,8 @@ class TestDuplicateItem(ItemTest, DuplicateHelper): """ Test the duplicate method. """ + shard = 1 + def setUp(self): """ Creates the test course structure and a few components to 'duplicate'. """ super(TestDuplicateItem, self).setUp() @@ -766,6 +773,8 @@ class TestMoveItem(ItemTest): """ Tests for move item. """ + shard = 1 + def setUp(self): """ Creates the test course structure to build course outline tree. @@ -1317,6 +1326,8 @@ class TestDuplicateItemWithAsides(ItemTest, DuplicateHelper): """ Test the duplicate method for blocks with asides. """ + shard = 1 + MODULESTORE = TEST_DATA_SPLIT_MODULESTORE def setUp(self): @@ -1379,6 +1390,8 @@ class TestEditItemSetup(ItemTest): """ Setup for xblock update tests. """ + shard = 1 + def setUp(self): """ Creates the test course structure and a couple problems to 'edit'. """ super(TestEditItemSetup, self).setUp() @@ -1409,6 +1422,8 @@ class TestEditItem(TestEditItemSetup): """ Test xblock update. """ + shard = 1 + def test_delete_field(self): """ Sending null in for a field 'deletes' it @@ -1848,6 +1863,7 @@ class TestEditItemSplitMongo(TestEditItemSetup): """ Tests for EditItem running on top of the SplitMongoModuleStore. """ + shard = 1 MODULESTORE = TEST_DATA_SPLIT_MODULESTORE def test_editing_view_wrappers(self): @@ -1869,6 +1885,8 @@ class TestEditSplitModule(ItemTest): """ Tests around editing instances of the split_test module. """ + shard = 1 + def setUp(self): super(TestEditSplitModule, self).setUp() self.user = UserFactory() @@ -2090,6 +2108,8 @@ class TestEditSplitModule(ItemTest): @ddt.ddt class TestComponentHandler(TestCase): + shard = 1 + def setUp(self): super(TestComponentHandler, self).setUp() @@ -2150,6 +2170,7 @@ class TestComponentTemplates(CourseTestCase): """ Unit tests for the generation of the component templates for a course. """ + shard = 1 def setUp(self): super(TestComponentTemplates, self).setUp() @@ -2385,6 +2406,8 @@ class TestXBlockInfo(ItemTest): """ Unit tests for XBlock's outline handling. """ + shard = 1 + def setUp(self): super(TestXBlockInfo, self).setUp() user_id = self.user.id @@ -2731,6 +2754,8 @@ class TestLibraryXBlockInfo(ModuleStoreTestCase): """ Unit tests for XBlock Info for XBlocks in a content library """ + shard = 1 + def setUp(self): super(TestLibraryXBlockInfo, self).setUp() user_id = self.user.id @@ -2780,6 +2805,8 @@ class TestLibraryXBlockCreation(ItemTest): """ Tests the adding of XBlocks to Library """ + shard = 1 + def test_add_xblock(self): """ Verify we can add an XBlock to a Library. @@ -2816,6 +2843,7 @@ class TestXBlockPublishingInfo(ItemTest): """ Unit tests for XBlock's outline handling. """ + shard = 1 FIRST_SUBSECTION_PATH = [0] FIRST_UNIT_PATH = [0, 0] SECOND_UNIT_PATH = [0, 1] diff --git a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py index 443131af6b..da838b1806 100644 --- a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py +++ b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py @@ -5,6 +5,7 @@ from io import BytesIO from mock import Mock, patch, ANY from django.test.testcases import TestCase +from django.core.urlresolvers import reverse from edxval import api from contentstore.tests.utils import CourseTestCase @@ -177,26 +178,24 @@ class TranscriptCredentialsValidationTest(TestCase): @ddt.ddt -@patch( - 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', - Mock(return_value=True) -) class TranscriptDownloadTest(CourseTestCase): """ Tests for transcript download handler. """ - VIEW_NAME = 'transcript_download_handler' - def get_url_for_course_key(self, course_id): - return reverse_course_url(self.VIEW_NAME, course_id) + @property + def view_url(self): + """ + Returns url for this view + """ + return reverse('transcript_download_handler') def test_302_with_anonymous_user(self): """ Verify that redirection happens in case of unauthorized request. """ self.client.logout() - transcript_download_url = self.get_url_for_course_key(self.course.id) - response = self.client.get(transcript_download_url, content_type='application/json') + response = self.client.get(self.view_url, content_type='application/json') self.assertEqual(response.status_code, 302) def test_405_with_not_allowed_request_method(self): @@ -204,26 +203,14 @@ class TranscriptDownloadTest(CourseTestCase): Verify that 405 is returned in case of not-allowed request methods. Allowed request methods include GET. """ - transcript_download_url = self.get_url_for_course_key(self.course.id) - response = self.client.post(transcript_download_url, content_type='application/json') + response = self.client.post(self.view_url, content_type='application/json') self.assertEqual(response.status_code, 405) - def test_404_with_feature_disabled(self): - """ - Verify that 404 is returned if the corresponding feature is disabled. - """ - transcript_download_url = self.get_url_for_course_key(self.course.id) - with patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled') as feature: - feature.return_value = False - response = self.client.get(transcript_download_url, content_type='application/json') - self.assertEqual(response.status_code, 404) - @patch('contentstore.views.transcript_settings.get_video_transcript_data') def test_transcript_download_handler(self, mock_get_video_transcript_data): """ Tests that transcript download handler works as expected. """ - transcript_download_url = self.get_url_for_course_key(self.course.id) mock_get_video_transcript_data.return_value = { 'content': json.dumps({ "start": [10], @@ -235,7 +222,7 @@ class TranscriptDownloadTest(CourseTestCase): # Make request to transcript download handler response = self.client.get( - transcript_download_url, + self.view_url, data={ 'edx_video_id': '123', 'language_code': 'en' @@ -277,34 +264,30 @@ class TranscriptDownloadTest(CourseTestCase): Tests that transcript download handler with missing attributes. """ # Make request to transcript download handler - transcript_download_url = self.get_url_for_course_key(self.course.id) - response = self.client.get(transcript_download_url, data=request_payload) + response = self.client.get(self.view_url, data=request_payload) # Assert the response self.assertEqual(response.status_code, 400) self.assertEqual(json.loads(response.content)['error'], expected_error_message) @ddt.ddt -@patch( - 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', - Mock(return_value=True) -) class TranscriptUploadTest(CourseTestCase): """ Tests for transcript upload handler. """ - VIEW_NAME = 'transcript_upload_handler' - - def get_url_for_course_key(self, course_id): - return reverse_course_url(self.VIEW_NAME, course_id) + @property + def view_url(self): + """ + Returns url for this view + """ + return reverse('transcript_upload_handler') def test_302_with_anonymous_user(self): """ Verify that redirection happens in case of unauthorized request. """ self.client.logout() - transcript_upload_url = self.get_url_for_course_key(self.course.id) - response = self.client.post(transcript_upload_url, content_type='application/json') + response = self.client.post(self.view_url, content_type='application/json') self.assertEqual(response.status_code, 302) def test_405_with_not_allowed_request_method(self): @@ -312,31 +295,19 @@ class TranscriptUploadTest(CourseTestCase): Verify that 405 is returned in case of not-allowed request methods. Allowed request methods include POST. """ - transcript_upload_url = self.get_url_for_course_key(self.course.id) - response = self.client.get(transcript_upload_url, content_type='application/json') + response = self.client.get(self.view_url, content_type='application/json') self.assertEqual(response.status_code, 405) - def test_404_with_feature_disabled(self): - """ - Verify that 404 is returned if the corresponding feature is disabled. - """ - transcript_upload_url = self.get_url_for_course_key(self.course.id) - with patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled') as feature: - feature.return_value = False - response = self.client.post(transcript_upload_url, content_type='application/json') - self.assertEqual(response.status_code, 404) - @patch('contentstore.views.transcript_settings.create_or_update_video_transcript') @patch('contentstore.views.transcript_settings.get_available_transcript_languages', Mock(return_value=['en'])) def test_transcript_upload_handler(self, mock_create_or_update_video_transcript): """ Tests that transcript upload handler works as expected. """ - transcript_upload_url = self.get_url_for_course_key(self.course.id) transcript_file_stream = BytesIO('0\n00:00:00,010 --> 00:00:00,100\nПривіт, edX вітає вас.\n\n') # Make request to transcript upload handler response = self.client.post( - transcript_upload_url, + self.view_url, { 'edx_video_id': '123', 'language_code': 'en', @@ -395,9 +366,8 @@ class TranscriptUploadTest(CourseTestCase): """ Tests the transcript upload handler when the required attributes are missing. """ - transcript_upload_url = self.get_url_for_course_key(self.course.id) # Make request to transcript upload handler - response = self.client.post(transcript_upload_url, request_payload, format='multipart') + response = self.client.post(self.view_url, request_payload, format='multipart') self.assertEqual(response.status_code, 400) self.assertEqual(json.loads(response.content)['error'], expected_error_message) @@ -407,14 +377,13 @@ class TranscriptUploadTest(CourseTestCase): Tests that upload handler do not update transcript's language if a transcript with the same language already present for an edx_video_id. """ - transcript_upload_url = self.get_url_for_course_key(self.course.id) # Make request to transcript upload handler request_payload = { 'edx_video_id': '1234', 'language_code': 'en', 'new_language_code': 'es' } - response = self.client.post(transcript_upload_url, request_payload, format='multipart') + response = self.client.post(self.view_url, request_payload, format='multipart') self.assertEqual(response.status_code, 400) self.assertEqual( json.loads(response.content)['error'], @@ -427,10 +396,9 @@ class TranscriptUploadTest(CourseTestCase): Tests the transcript upload handler with an image file. """ with make_image_file() as image_file: - transcript_upload_url = self.get_url_for_course_key(self.course.id) # Make request to transcript upload handler response = self.client.post( - transcript_upload_url, + self.view_url, { 'edx_video_id': '123', 'language_code': 'en', @@ -451,11 +419,10 @@ class TranscriptUploadTest(CourseTestCase): """ Tests the transcript upload handler with an invalid transcript file. """ - transcript_upload_url = self.get_url_for_course_key(self.course.id) transcript_file_stream = BytesIO('An invalid transcript SubRip file content') # Make request to transcript upload handler response = self.client.post( - transcript_upload_url, + self.view_url, { 'edx_video_id': '123', 'language_code': 'en', @@ -473,11 +440,7 @@ class TranscriptUploadTest(CourseTestCase): @ddt.ddt -@patch( - 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', - Mock(return_value=True) -) -class TranscriptUploadTest(CourseTestCase): +class TranscriptDeleteTest(CourseTestCase): """ Tests for transcript deletion handler. """ @@ -504,16 +467,6 @@ class TranscriptUploadTest(CourseTestCase): response = self.client.post(transcript_delete_url) self.assertEqual(response.status_code, 405) - def test_404_with_feature_disabled(self): - """ - Verify that 404 is returned if the corresponding feature is disabled. - """ - transcript_delete_url = self.get_url_for_course_key(self.course.id, edx_video_id='test_id', language_code='en') - with patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled') as feature: - feature.return_value = False - response = self.client.delete(transcript_delete_url) - self.assertEqual(response.status_code, 404) - def test_404_with_non_staff_user(self): """ Verify that 404 is returned if the user doesn't have studio write access. diff --git a/cms/djangoapps/contentstore/views/tests/test_transcripts.py b/cms/djangoapps/contentstore/views/tests/test_transcripts.py index 9d99c072d9..0fae175fd9 100644 --- a/cms/djangoapps/contentstore/views/tests/test_transcripts.py +++ b/cms/djangoapps/contentstore/views/tests/test_transcripts.py @@ -1,9 +1,10 @@ """Tests for items views.""" import copy +from codecs import BOM_UTF8 import ddt import json -import os +from mock import patch, Mock import tempfile import textwrap from uuid import uuid4 @@ -11,7 +12,7 @@ from uuid import uuid4 from django.conf import settings from django.core.urlresolvers import reverse from django.test.utils import override_settings -from mock import patch, Mock +from edxval.api import create_video from opaque_keys.edx.keys import UsageKey from contentstore.tests.utils import CourseTestCase, mock_requests_get @@ -20,11 +21,32 @@ from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore from xmodule.exceptions import NotFoundError from xmodule.modulestore.django import modulestore -from xmodule.video_module import transcripts_utils +from xmodule.video_module.transcripts_utils import ( + GetTranscriptsFromYouTubeException, + get_video_transcript_content, + remove_subs_from_store, + Transcript, +) TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex +SRT_TRANSCRIPT_CONTENT = """0 +00:00:10,500 --> 00:00:13,000 +Elephant's Dream + +1 +00:00:15,000 --> 00:00:18,000 +At the left we can see... + +""" + +SJSON_TRANSCRIPT_CONTENT = Transcript.convert( + SRT_TRANSCRIPT_CONTENT, + Transcript.SRT, + Transcript.SJSON, +) + @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class BaseTranscripts(CourseTestCase): @@ -95,498 +117,716 @@ class BaseTranscripts(CourseTestCase): 1.5: item.youtube_id_1_5 } - -class TestUploadTranscripts(BaseTranscripts): - """ - Tests for '/transcripts/upload' url. - """ - def setUp(self): - """Create initial data.""" - super(TestUploadTranscripts, self).setUp() - - self.good_srt_file = tempfile.NamedTemporaryFile(suffix='.srt') - self.good_srt_file.write(textwrap.dedent(""" - 1 - 00:00:10,500 --> 00:00:13,000 - Elephant's Dream - - 2 - 00:00:15,000 --> 00:00:18,000 - At the left we can see... - """)) - self.good_srt_file.seek(0) - - self.bad_data_srt_file = tempfile.NamedTemporaryFile(suffix='.srt') - self.bad_data_srt_file.write('Some BAD data') - self.bad_data_srt_file.seek(0) - - self.bad_name_srt_file = tempfile.NamedTemporaryFile(suffix='.BAD') - self.bad_name_srt_file.write(textwrap.dedent(""" - 1 - 00:00:10,500 --> 00:00:13,000 - Elephant's Dream - - 2 - 00:00:15,000 --> 00:00:18,000 - At the left we can see... - """)) - self.bad_name_srt_file.seek(0) - - self.ufeff_srt_file = tempfile.NamedTemporaryFile(suffix='.srt') - - def test_success_video_module_source_subs_uploading(self): - self.item.data = textwrap.dedent(""" - - """) - modulestore().update_item(self.item, self.user.id) - - link = reverse('upload_transcripts') - filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] - resp = self.client.post(link, { - 'locator': self.video_usage_key, - 'transcript-file': self.good_srt_file, - 'video_list': json.dumps([{ - 'type': 'html5', - 'video': filename, - 'mode': 'mp4', - }]) - }) - self.assertEqual(resp.status_code, 200) - self.assertEqual(json.loads(resp.content).get('status'), 'Success') - - item = modulestore().get_item(self.video_usage_key) - self.assertEqual(item.sub, filename) - - content_location = StaticContent.compute_location( - self.course.id, 'subs_{0}.srt.sjson'.format(filename)) - self.assertTrue(contentstore().find(content_location)) - - def test_fail_data_without_id(self): - link = reverse('upload_transcripts') - resp = self.client.post(link, {'transcript-file': self.good_srt_file}) - self.assertEqual(resp.status_code, 400) - self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "locator" form data.') - - def test_fail_data_without_file(self): - link = reverse('upload_transcripts') - resp = self.client.post(link, {'locator': self.video_usage_key}) - self.assertEqual(resp.status_code, 400) - self.assertEqual(json.loads(resp.content).get('status'), 'POST data without "file" form data.') - - def test_fail_data_with_bad_locator(self): - # Test for raising `InvalidLocationError` exception. - link = reverse('upload_transcripts') - filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] - resp = self.client.post(link, { - 'locator': 'BAD_LOCATOR', - 'transcript-file': self.good_srt_file, - 'video_list': json.dumps([{ - 'type': 'html5', - 'video': filename, - 'mode': 'mp4', - }]) - }) - self.assertEqual(resp.status_code, 400) - self.assertEqual(json.loads(resp.content).get('status'), "Can't find item by locator.") - - # Test for raising `ItemNotFoundError` exception. - link = reverse('upload_transcripts') - filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] - resp = self.client.post(link, { - 'locator': '{0}_{1}'.format(self.video_usage_key, 'BAD_LOCATOR'), - 'transcript-file': self.good_srt_file, - 'video_list': json.dumps([{ - 'type': 'html5', - 'video': filename, - 'mode': 'mp4', - }]) - }) - self.assertEqual(resp.status_code, 400) - self.assertEqual(json.loads(resp.content).get('status'), "Can't find item by locator.") - - def test_fail_for_non_video_module(self): - # non_video module: setup + def create_non_video_module(self): + """ + Setup non video module for tests. + """ data = { 'parent_locator': unicode(self.course.location), 'category': 'non_video', 'type': 'non_video' } - resp = self.client.ajax_post('/xblock/', data) - usage_key = self._get_usage_key(resp) + response = self.client.ajax_post('/xblock/', data) + usage_key = self._get_usage_key(response) item = modulestore().get_item(usage_key) item.data = '' modulestore().update_item(item, self.user.id) - # non_video module: testing + return usage_key - link = reverse('upload_transcripts') - filename = os.path.splitext(os.path.basename(self.good_srt_file.name))[0] - resp = self.client.post(link, { - 'locator': unicode(usage_key), - 'transcript-file': self.good_srt_file, - 'video_list': json.dumps([{ - 'type': 'html5', - 'video': filename, - 'mode': 'mp4', - }]) - }) - self.assertEqual(resp.status_code, 400) - self.assertEqual(json.loads(resp.content).get('status'), 'Transcripts are supported only for "video" modules.') + def assert_response(self, response, expected_status_code, expected_message): + response_content = json.loads(response.content) + self.assertEqual(response.status_code, expected_status_code) + self.assertEqual(response_content['status'], expected_message) - def test_fail_bad_xml(self): - self.item.data = '<<
diff --git a/cms/templates/js/previous-video-upload-list.underscore b/cms/templates/js/previous-video-upload-list.underscore index 3248aa78f8..e3569d0403 100644 --- a/cms/templates/js/previous-video-upload-list.underscore +++ b/cms/templates/js/previous-video-upload-list.underscore @@ -14,9 +14,7 @@
<%- gettext("Name") %>
<%- gettext("Date Added") %>
<%- gettext("Video ID") %>
- <% if (isVideoTranscriptEnabled) { %>
<%- gettext("Transcripts") %>
- <% } %>
<%- gettext("Status") %>
<%- gettext("Action") %>
diff --git a/cms/templates/js/previous-video-upload.underscore b/cms/templates/js/previous-video-upload.underscore index 716198f921..4ab630d5fd 100644 --- a/cms/templates/js/previous-video-upload.underscore +++ b/cms/templates/js/previous-video-upload.underscore @@ -5,9 +5,7 @@
<%- client_video_id %>
<%- created %>
<%- edx_video_id %>
- <% if (isVideoTranscriptEnabled) { %>
- <% } %>
<%- status %>
    diff --git a/cms/templates/js/video/metadata-translations-entry.underscore b/cms/templates/js/video/metadata-translations-entry.underscore index a7a5453f73..1afef78cd9 100644 --- a/cms/templates/js/video/metadata-translations-entry.underscore +++ b/cms/templates/js/video/metadata-translations-entry.underscore @@ -1,14 +1,11 @@ <%= model.get('help') %> diff --git a/cms/templates/js/video/metadata-translations-item.underscore b/cms/templates/js/video/metadata-translations-item.underscore index 55e75d23f7..cb2fdd23a4 100644 --- a/cms/templates/js/video/metadata-translations-item.underscore +++ b/cms/templates/js/video/metadata-translations-item.underscore @@ -1,12 +1,13 @@ -
  • - <%= gettext("Remove") %> +
  • + <%= gettext("Remove") %> -
  • diff --git a/cms/templates/js/video/transcripts/file-upload.underscore b/cms/templates/js/video/transcripts/file-upload.underscore index 8edc816f9c..925b846c01 100644 --- a/cms/templates/js/video/transcripts/file-upload.underscore +++ b/cms/templates/js/video/transcripts/file-upload.underscore @@ -6,5 +6,4 @@ - diff --git a/cms/templates/js/video/transcripts/messages/transcripts-found.underscore b/cms/templates/js/video/transcripts/messages/transcripts-found.underscore index 653981ea20..a803a453f7 100644 --- a/cms/templates/js/video/transcripts/messages/transcripts-found.underscore +++ b/cms/templates/js/video/transcripts/messages/transcripts-found.underscore @@ -10,7 +10,7 @@ - "> + "> <%= gettext("Download Transcript for Editing") %>
diff --git a/cms/templates/js/video/transcripts/messages/transcripts-uploaded.underscore b/cms/templates/js/video/transcripts/messages/transcripts-uploaded.underscore index 2993090a11..4740d280e5 100644 --- a/cms/templates/js/video/transcripts/messages/transcripts-uploaded.underscore +++ b/cms/templates/js/video/transcripts/messages/transcripts-uploaded.underscore @@ -10,7 +10,7 @@ - "> + "> <%= gettext("Download Transcript for Editing") %> diff --git a/cms/templates/register.html b/cms/templates/register.html index 0ff0024179..86f639056e 100644 --- a/cms/templates/register.html +++ b/cms/templates/register.html @@ -50,6 +50,7 @@ from django.core.urlresolvers import reverse
  • +
  • diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index ecdabc0658..512078e088 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -190,7 +190,7 @@ % endif % endif - % if user.is_authenticated(): + % if user.is_authenticated: