diff --git a/.coveragerc b/.coveragerc index fab115cb37..596d280140 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,6 @@ # .coveragerc for edx-platform [run] -data_file = reports/.coverage +data_file = reports/${TEST_SUITE}.coverage source = cms common/djangoapps @@ -49,4 +49,4 @@ output = reports/coverage.xml jenkins_source = /home/jenkins/workspace/$JOB_NAME /home/jenkins/workspace/$SUBSET_JOB - /edx/app/edxapp/edx-platform + /home/jenkins/edx-platform diff --git a/.github/renovate.json b/.github/renovate.json index 327fbf5f3d..e27406fabe 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -4,7 +4,7 @@ "schedule:weekdays", ":preserveSemverRanges" ], - "prConcurrentLimit": 2, + "prConcurrentLimit": 5, "includePaths": [ "package.json" ] diff --git a/.gitignore b/.gitignore index 26775845b4..eb0bc8d037 100644 --- a/.gitignore +++ b/.gitignore @@ -143,10 +143,3 @@ dist # Locally generated PII reports pii_report - -# Local documentation builds -docs/_build -docs/cms -docs/common -docs/lms -docs/openedx diff --git a/Makefile b/Makefile index c9fb8b2987..92d761a8da 100644 --- a/Makefile +++ b/Makefile @@ -19,8 +19,7 @@ clean: ## archive and delete most git-ignored files rm $(PRIVATE_FILES) docs: ## build the developer documentation for this repository - rm -rf docs/_build docs/cms docs/common docs/lms docs/openedx - cd docs; make html + cd docs/guides; make clean html extract_translations: ## extract localizable strings from sources i18n_tool extract -v diff --git a/cms/djangoapps/api/v1/serializers/course_runs.py b/cms/djangoapps/api/v1/serializers/course_runs.py index 44f722e79e..90de622bac 100644 --- a/cms/djangoapps/api/v1/serializers/course_runs.py +++ b/cms/djangoapps/api/v1/serializers/course_runs.py @@ -1,10 +1,14 @@ """ Course run serializers. """ +from __future__ import absolute_import + import logging -import time +import time # pylint: disable=unused-import + import six from django.contrib.auth import get_user_model from django.db import transaction from django.utils.translation import ugettext_lazy as _ +from opaque_keys import InvalidKeyError from rest_framework import serializers from rest_framework.fields import empty @@ -82,7 +86,7 @@ class CourseRunTeamSerializerMixin(serializers.Serializer): def image_is_jpeg_or_png(value): content_type = value.content_type - if content_type not in IMAGE_TYPES.keys(): + if content_type not in list(IMAGE_TYPES.keys()): raise serializers.ValidationError( u'Only JPEG and PNG image types are supported. {} is not valid'.format(content_type)) @@ -125,7 +129,7 @@ class CourseRunImageSerializer(serializers.Serializer): class CourseRunSerializerCommonFieldsMixin(serializers.Serializer): schedule = CourseRunScheduleSerializer(source='*', required=False) pacing_type = CourseRunPacingTypeField(source='self_paced', required=False, - choices=(('instructor_paced', False), ('self_paced', True),)) + choices=((False, 'instructor_paced'), (True, 'self_paced'),)) class CourseRunSerializer(CourseRunSerializerCommonFieldsMixin, CourseRunTeamSerializerMixin, serializers.Serializer): @@ -165,29 +169,40 @@ class CourseRunCreateSerializer(CourseRunSerializer): class CourseRunRerunSerializer(CourseRunSerializerCommonFieldsMixin, CourseRunTeamSerializerMixin, serializers.Serializer): title = serializers.CharField(source='display_name', required=False) + number = serializers.CharField(source='id.course', required=False) run = serializers.CharField(source='id.run') - def validate_run(self, value): + def validate(self, attrs): course_run_key = self.instance.id + _id = attrs.get('id') + number = _id.get('course', course_run_key.course) + run = _id['run'] store = modulestore() - with store.default_store('split'): - new_course_run_key = store.make_course_key(course_run_key.org, course_run_key.course, value) + try: + with store.default_store('split'): + new_course_run_key = store.make_course_key(course_run_key.org, number, run) + except InvalidKeyError: + raise serializers.ValidationError( + u'Invalid key supplied. Ensure there are no special characters in the Course Number.' + ) if store.has_course(new_course_run_key, ignore_case=True): - raise serializers.ValidationError(u'Course run {key} already exists'.format(key=new_course_run_key)) - return value + raise serializers.ValidationError( + {'run': u'Course run {key} already exists'.format(key=new_course_run_key)} + ) + return attrs def update(self, instance, validated_data): course_run_key = instance.id _id = validated_data.pop('id') + number = _id.get('course', course_run_key.course) + run = _id['run'] team = validated_data.pop('team', []) user = self.context['request'].user fields = { 'display_name': instance.display_name } fields.update(validated_data) - new_course_run_key = rerun_course( - user, course_run_key, course_run_key.org, course_run_key.course, _id['run'], fields, False - ) + new_course_run_key = rerun_course(user, course_run_key, course_run_key.org, number, run, fields, False) course_run = get_course_and_check_access(new_course_run_key, user) self.update_team(course_run, team) diff --git a/cms/djangoapps/api/v1/tests/test_serializers/test_course_runs.py b/cms/djangoapps/api/v1/tests/test_serializers/test_course_runs.py index 39ac9c0043..d73595674f 100644 --- a/cms/djangoapps/api/v1/tests/test_serializers/test_course_runs.py +++ b/cms/djangoapps/api/v1/tests/test_serializers/test_course_runs.py @@ -1,16 +1,21 @@ +"""Tests for course run serializers""" + +from __future__ import absolute_import + import datetime import ddt import pytz from django.test import RequestFactory -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory from openedx.core.lib.courses import course_image_url from student.roles import CourseInstructorRole, CourseStaffRole from student.tests.factories import UserFactory -from ..utils import serialize_datetime +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + from ...serializers.course_runs import CourseRunSerializer +from ..utils import serialize_datetime @ddt.ddt diff --git a/cms/djangoapps/api/v1/tests/test_views/test_course_runs.py b/cms/djangoapps/api/v1/tests/test_views/test_course_runs.py index d14301eff8..fd58859526 100644 --- a/cms/djangoapps/api/v1/tests/test_views/test_course_runs.py +++ b/cms/djangoapps/api/v1/tests/test_views/test_course_runs.py @@ -1,13 +1,22 @@ +"""Tests for Course run views""" + +from __future__ import absolute_import + import datetime import ddt import pytz from django.core.files.uploadedfile import SimpleUploadedFile -from django.urls import reverse from django.test import RequestFactory +from django.urls import reverse +from mock import patch from opaque_keys.edx.keys import CourseKey -from openedx.core.lib.courses import course_image_url from rest_framework.test import APIClient + +from openedx.core.lib.courses import course_image_url +from student.models import CourseAccessRole +from student.tests.factories import TEST_PASSWORD, AdminFactory, UserFactory +from util.organizations_helpers import add_organization, get_course_organizations from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore from xmodule.exceptions import NotFoundError @@ -15,10 +24,8 @@ from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ToyCourseFactory -from student.models import CourseAccessRole -from student.tests.factories import AdminFactory, TEST_PASSWORD, UserFactory -from ..utils import serialize_datetime from ...serializers.course_runs import CourseRunSerializer +from ..utils import serialize_datetime @ddt.ddt @@ -315,19 +322,25 @@ class CourseRunViewSetTests(ModuleStoreTestCase): # There should now be an image stored contentstore().find(content_key) + @patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True}) @ddt.data( - ('instructor_paced', False), - ('self_paced', True), + ('instructor_paced', False, 'NotOriginalNumber1x'), + ('self_paced', True, None), ) @ddt.unpack - def test_rerun(self, pacing_type, expected_self_paced_value): - course_run = ToyCourseFactory() + def test_rerun(self, pacing_type, expected_self_paced_value, number): + original_course_run = ToyCourseFactory() + add_organization({ + 'name': 'Test Organization', + 'short_name': original_course_run.id.org, + 'description': 'Testing Organization Description', + }) start = datetime.datetime.now(pytz.UTC).replace(microsecond=0) end = start + datetime.timedelta(days=30) user = UserFactory() role = 'instructor' run = '3T2017' - url = reverse('api:v1:course_run-rerun', kwargs={'pk': str(course_run.id)}) + url = reverse('api:v1:course_run-rerun', kwargs={'pk': str(original_course_run.id)}) data = { 'run': run, 'schedule': { @@ -342,16 +355,31 @@ class CourseRunViewSetTests(ModuleStoreTestCase): ], 'pacing_type': pacing_type, } + # If number is supplied, this should become the course number used in the course run key + # If not, it should default to the original course run number that the rerun is based on. + if number: + data.update({'number': number}) response = self.client.post(url, data, format='json') assert response.status_code == 201 course_run_key = CourseKey.from_string(response.data['id']) course_run = modulestore().get_course(course_run_key) + assert course_run.id.run == run assert course_run.self_paced is expected_self_paced_value + + if number: + assert course_run.id.course == number + assert course_run.id.course != original_course_run.id.course + else: + assert course_run.id.course == original_course_run.id.course + self.assert_course_run_schedule(course_run, start, end) self.assert_access_role(course_run, user, role) self.assert_course_access_role_count(course_run, 1) + course_orgs = get_course_organizations(course_run_key) + self.assertEqual(len(course_orgs), 1) + self.assertEqual(course_orgs[0]['short_name'], original_course_run.id.org) def test_rerun_duplicate_run(self): course_run = ToyCourseFactory() @@ -362,3 +390,16 @@ class CourseRunViewSetTests(ModuleStoreTestCase): response = self.client.post(url, data, format='json') assert response.status_code == 400 assert response.data == {'run': [u'Course run {key} already exists'.format(key=course_run.id)]} + + def test_rerun_invalid_number(self): + course_run = ToyCourseFactory() + url = reverse('api:v1:course_run-rerun', kwargs={'pk': str(course_run.id)}) + data = { + 'run': '2T2019', + 'number': '!@#$%^&*()', + } + response = self.client.post(url, data, format='json') + assert response.status_code == 400 + assert response.data == {'non_field_errors': [ + u'Invalid key supplied. Ensure there are no special characters in the Course Number.' + ]} diff --git a/cms/djangoapps/cms_user_tasks/apps.py b/cms/djangoapps/cms_user_tasks/apps.py index ae496cb14a..95edb59d8c 100644 --- a/cms/djangoapps/cms_user_tasks/apps.py +++ b/cms/djangoapps/cms_user_tasks/apps.py @@ -3,6 +3,8 @@ CMS user tasks application configuration Signal handlers are connected here. """ +from __future__ import absolute_import + from django.apps import AppConfig diff --git a/cms/djangoapps/cms_user_tasks/tasks.py b/cms/djangoapps/cms_user_tasks/tasks.py index 01a86bb048..bd7fe85819 100644 --- a/cms/djangoapps/cms_user_tasks/tasks.py +++ b/cms/djangoapps/cms_user_tasks/tasks.py @@ -2,6 +2,8 @@ Celery tasks used by cms_user_tasks """ +from __future__ import absolute_import + from boto.exception import NoAuthHandlerFound from celery.exceptions import MaxRetriesExceededError from celery.task import task diff --git a/cms/djangoapps/contentstore/admin.py b/cms/djangoapps/contentstore/admin.py index 2a73566ee3..54adb440b8 100644 --- a/cms/djangoapps/contentstore/admin.py +++ b/cms/djangoapps/contentstore/admin.py @@ -2,11 +2,11 @@ Admin site bindings for contentstore """ +from __future__ import absolute_import + from config_models.admin import ConfigurationModelAdmin from django.contrib import admin -from contentstore.models import PushNotificationConfig, VideoUploadConfig - +from contentstore.models import VideoUploadConfig admin.site.register(VideoUploadConfig, ConfigurationModelAdmin) -admin.site.register(PushNotificationConfig, ConfigurationModelAdmin) diff --git a/cms/djangoapps/contentstore/api/tests/base.py b/cms/djangoapps/contentstore/api/tests/base.py index d8bd02ee17..619b0ad502 100644 --- a/cms/djangoapps/contentstore/api/tests/base.py +++ b/cms/djangoapps/contentstore/api/tests/base.py @@ -1,6 +1,8 @@ """ Base test case for the course API views. """ +from __future__ import absolute_import + from django.core.urlresolvers import reverse from rest_framework.test import APITestCase diff --git a/cms/djangoapps/contentstore/api/tests/test_import.py b/cms/djangoapps/contentstore/api/tests/test_import.py index 5c9f3f34b6..6df2efb690 100644 --- a/cms/djangoapps/contentstore/api/tests/test_import.py +++ b/cms/djangoapps/contentstore/api/tests/test_import.py @@ -1,6 +1,8 @@ """ Tests for the course import API views """ +from __future__ import absolute_import + import os import tarfile import tempfile @@ -9,10 +11,10 @@ from django.urls import reverse from path import Path as path from rest_framework import status from rest_framework.test import APITestCase +from user_tasks.models import UserTaskStatus from lms.djangoapps.courseware.tests.factories import StaffFactory from student.tests.factories import UserFactory -from user_tasks.models import UserTaskStatus from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory diff --git a/cms/djangoapps/contentstore/api/tests/test_quality.py b/cms/djangoapps/contentstore/api/tests/test_quality.py index 14249ffaa9..e6c9898d4f 100644 --- a/cms/djangoapps/contentstore/api/tests/test_quality.py +++ b/cms/djangoapps/contentstore/api/tests/test_quality.py @@ -1,6 +1,8 @@ """ Tests for the course import API views """ +from __future__ import absolute_import + from rest_framework import status from .base import BaseCourseViewTest diff --git a/cms/djangoapps/contentstore/api/tests/test_validation.py b/cms/djangoapps/contentstore/api/tests/test_validation.py index ecbd6cbffe..8330db9855 100644 --- a/cms/djangoapps/contentstore/api/tests/test_validation.py +++ b/cms/djangoapps/contentstore/api/tests/test_validation.py @@ -1,7 +1,10 @@ """ Tests for the course import API views """ +from __future__ import absolute_import + from datetime import datetime + from django.core.urlresolvers import reverse from rest_framework import status from rest_framework.test import APITestCase diff --git a/cms/djangoapps/contentstore/api/urls.py b/cms/djangoapps/contentstore/api/urls.py index e5bdc40d4e..20357254fc 100644 --- a/cms/djangoapps/contentstore/api/urls.py +++ b/cms/djangoapps/contentstore/api/urls.py @@ -1,10 +1,11 @@ """ Course API URLs. """ +from __future__ import absolute_import + from django.conf import settings from django.conf.urls import url from cms.djangoapps.contentstore.api.views import course_import, course_quality, course_validation - app_name = 'contentstore' urlpatterns = [ diff --git a/cms/djangoapps/contentstore/api/views/course_import.py b/cms/djangoapps/contentstore/api/views/course_import.py index d13f17a241..239bc27694 100644 --- a/cms/djangoapps/contentstore/api/views/course_import.py +++ b/cms/djangoapps/contentstore/api/views/course_import.py @@ -1,20 +1,20 @@ """ APIs related to Course Import. """ +from __future__ import absolute_import + import base64 import logging import os -from path import Path as path -from six import text_type - from django.conf import settings - from django.core.files import File +from path import Path as path from rest_framework import status from rest_framework.exceptions import AuthenticationFailed from rest_framework.generics import GenericAPIView from rest_framework.response import Response +from six import text_type from user_tasks.models import UserTaskStatus from contentstore.storage import course_import_export_storage @@ -23,7 +23,6 @@ from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_c from .utils import course_author_access_required - log = logging.getLogger(__name__) diff --git a/cms/djangoapps/contentstore/api/views/course_quality.py b/cms/djangoapps/contentstore/api/views/course_quality.py index c3921aaca3..e90b948f06 100644 --- a/cms/djangoapps/contentstore/api/views/course_quality.py +++ b/cms/djangoapps/contentstore/api/views/course_quality.py @@ -1,19 +1,23 @@ # pylint: disable=missing-docstring +from __future__ import absolute_import + import logging import time + import numpy as np -from scipy import stats +import six +from edxval.api import get_videos_for_course from rest_framework.generics import GenericAPIView from rest_framework.response import Response +from scipy import stats from contentstore.views.item import highlights_setting -from edxval.api import get_videos_for_course -from openedx.core.lib.cache_utils import request_cached from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes +from openedx.core.lib.cache_utils import request_cached from openedx.core.lib.graph_traversals import traverse_pre_order from xmodule.modulestore.django import modulestore -from .utils import get_bool_param, course_author_access_required +from .utils import course_author_access_required, get_bool_param log = logging.getLogger(__name__) @@ -153,25 +157,25 @@ class CourseQualityView(DeveloperErrorViewMixin, GenericAPIView): def _subsections_quality(self, course, request): subsection_unit_dict = self._get_subsections_and_units(course, request) num_block_types_per_subsection_dict = {} - for subsection_key, unit_dict in subsection_unit_dict.iteritems(): + for subsection_key, unit_dict in six.iteritems(subsection_unit_dict): leaf_block_types_in_subsection = ( unit_info['leaf_block_types'] - for unit_info in unit_dict.itervalues() + for unit_info in six.itervalues(unit_dict) ) num_block_types_per_subsection_dict[subsection_key] = len(set().union(*leaf_block_types_in_subsection)) return dict( total_visible=len(num_block_types_per_subsection_dict), - num_with_one_block_type=list(num_block_types_per_subsection_dict.itervalues()).count(1), - num_block_types=self._stats_dict(list(num_block_types_per_subsection_dict.itervalues())), + num_with_one_block_type=list(six.itervalues(num_block_types_per_subsection_dict)).count(1), + num_block_types=self._stats_dict(list(six.itervalues(num_block_types_per_subsection_dict))), ) def _units_quality(self, course, request): subsection_unit_dict = self._get_subsections_and_units(course, request) num_leaf_blocks_per_unit = [ unit_info['num_leaf_blocks'] - for unit_dict in subsection_unit_dict.itervalues() - for unit_info in unit_dict.itervalues() + for unit_dict in six.itervalues(subsection_unit_dict) + for unit_info in six.itervalues(unit_dict) ] return dict( total_visible=len(num_leaf_blocks_per_unit), diff --git a/cms/djangoapps/contentstore/api/views/course_validation.py b/cms/djangoapps/contentstore/api/views/course_validation.py index a05704c0ad..5594440142 100644 --- a/cms/djangoapps/contentstore/api/views/course_validation.py +++ b/cms/djangoapps/contentstore/api/views/course_validation.py @@ -1,10 +1,13 @@ # pylint: disable=missing-docstring +from __future__ import absolute_import + import logging -from rest_framework.generics import GenericAPIView -from rest_framework.response import Response import dateutil +import six from pytz import UTC +from rest_framework.generics import GenericAPIView +from rest_framework.response import Response from contentstore.course_info_model import get_course_updates from contentstore.views.certificates import CertificateManager @@ -12,8 +15,7 @@ from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_c from xmodule.course_metadata_utils import DEFAULT_GRADING_POLICY from xmodule.modulestore.django import modulestore -from .utils import get_bool_param, course_author_access_required - +from .utils import course_author_access_required, get_bool_param log = logging.getLogger(__name__) @@ -118,7 +120,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView): ] assignments_with_dates_before_start = ( [ - {'id': unicode(a.location), 'display_name': a.display_name} + {'id': six.text_type(a.location), 'display_name': a.display_name} for a in assignments_with_dates if a.due < course.start ] @@ -128,7 +130,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView): assignments_with_dates_after_end = ( [ - {'id': unicode(a.location), 'display_name': a.display_name} + {'id': six.text_type(a.location), 'display_name': a.display_name} for a in assignments_with_dates if a.due > course.end ] @@ -144,7 +146,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView): ] assignments_with_dates_before_start = ( [ - {'id': unicode(a.location), 'display_name': a.display_name} + {'id': six.text_type(a.location), 'display_name': a.display_name} for a in assignments_with_dates if a.due < course.start ] @@ -154,7 +156,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView): assignments_with_dates_after_end = ( [ - {'id': unicode(a.location), 'display_name': a.display_name} + {'id': six.text_type(a.location), 'display_name': a.display_name} for a in assignments_with_dates if a.due > course.end ] @@ -175,14 +177,14 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView): parent_unit = modulestore().get_item(ora.parent) parent_assignment = modulestore().get_item(parent_unit.parent) assignments_with_ora_dates_before_start.append({ - 'id': unicode(parent_assignment.location), + 'id': six.text_type(parent_assignment.location), 'display_name': parent_assignment.display_name }) if course.end and self._has_date_after_end(ora, course.end): parent_unit = modulestore().get_item(ora.parent) parent_assignment = modulestore().get_item(parent_unit.parent) assignments_with_ora_dates_after_end.append({ - 'id': unicode(parent_assignment.location), + 'id': six.text_type(parent_assignment.location), 'display_name': parent_assignment.display_name }) diff --git a/cms/djangoapps/contentstore/api/views/utils.py b/cms/djangoapps/contentstore/api/views/utils.py index bbf0534bc3..7ae2fb456c 100644 --- a/cms/djangoapps/contentstore/api/views/utils.py +++ b/cms/djangoapps/contentstore/api/views/utils.py @@ -1,11 +1,13 @@ """ Common utilities for Contentstore APIs. """ +from __future__ import absolute_import + from contextlib import contextmanager +from opaque_keys.edx.keys import CourseKey from rest_framework import status from rest_framework.generics import GenericAPIView -from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.util.forms import to_bool from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes diff --git a/cms/djangoapps/contentstore/apps.py b/cms/djangoapps/contentstore/apps.py index 770ce3a9a0..65388470ec 100644 --- a/cms/djangoapps/contentstore/apps.py +++ b/cms/djangoapps/contentstore/apps.py @@ -4,6 +4,8 @@ Contentstore Application Configuration Above-modulestore level signal handlers are connected here. """ +from __future__ import absolute_import + from django.apps import AppConfig diff --git a/cms/djangoapps/contentstore/config/waffle.py b/cms/djangoapps/contentstore/config/waffle.py index 6457e77b03..3fb8bb0979 100644 --- a/cms/djangoapps/contentstore/config/waffle.py +++ b/cms/djangoapps/contentstore/config/waffle.py @@ -2,6 +2,8 @@ This module contains various configuration settings via waffle switches for the contentstore app. """ +from __future__ import absolute_import + from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleSwitchNamespace # Namespace diff --git a/cms/djangoapps/contentstore/config/waffle_utils.py b/cms/djangoapps/contentstore/config/waffle_utils.py index 86840acd7e..ac25116922 100644 --- a/cms/djangoapps/contentstore/config/waffle_utils.py +++ b/cms/djangoapps/contentstore/config/waffle_utils.py @@ -1,5 +1,7 @@ """Util methods for Waffle checks""" +from __future__ import absolute_import + from cms.djangoapps.contentstore.config.waffle import ENABLE_CHECKLISTS_QUALITY diff --git a/cms/djangoapps/contentstore/course_group_config.py b/cms/djangoapps/contentstore/course_group_config.py index f07c51eebd..911c356d18 100644 --- a/cms/djangoapps/contentstore/course_group_config.py +++ b/cms/djangoapps/contentstore/course_group_config.py @@ -1,9 +1,11 @@ """ Class for manipulating groups configuration on a course object. """ -from collections import defaultdict +from __future__ import absolute_import + import json import logging +from collections import defaultdict from django.utils.translation import ugettext as _ @@ -58,7 +60,7 @@ class GroupConfiguration(object): Deserialize given json that represents group configuration. """ try: - configuration = json.loads(json_string) + configuration = json.loads(json_string.decode("utf-8")) except ValueError: raise GroupConfigurationsValidationError(_("invalid JSON")) configuration["version"] = UserPartition.VERSION diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index d5631d9e94..d9e647d032 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -12,15 +12,16 @@ Current db representation: } """ +from __future__ import absolute_import + import logging import re from django.http import HttpResponseBadRequest from django.utils.translation import ugettext as _ -from cms.djangoapps.contentstore.push_notification import enqueue_push_course_update from openedx.core.lib.xblock_utils import get_course_update_items -from xmodule.html_module import CourseInfoModule +from xmodule.html_module import CourseInfoBlock from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError @@ -47,7 +48,6 @@ def update_course_updates(location, update, passed_id=None, user=None): Either add or update the given course update. Add: If the passed_id is absent or None, the course update is added. - If push_notification_selected is set in the update, a celery task for the push notification is created. Update: It will update it if it has a passed_id which has a valid value. Until updates have distinct values, the passed_id is the location url + an index into the html structure. @@ -74,10 +74,9 @@ def update_course_updates(location, update, passed_id=None, user=None): "id": len(course_update_items) + 1, "date": update["date"], "content": update["content"], - "status": CourseInfoModule.STATUS_VISIBLE + "status": CourseInfoBlock.STATUS_VISIBLE } course_update_items.append(course_update_dict) - enqueue_push_course_update(update, location.course_key) # update db record save_course_update_items(location, course_updates, course_update_items, user) @@ -104,14 +103,14 @@ def _get_visible_update(course_update_items): """ if isinstance(course_update_items, dict): # single course update item - if course_update_items.get("status") != CourseInfoModule.STATUS_DELETED: + if course_update_items.get("status") != CourseInfoBlock.STATUS_DELETED: return _make_update_dict(course_update_items) else: # requested course update item has been deleted (soft delete) return {"error": _("Course update not found."), "status": 404} return ([_make_update_dict(update) for update in course_update_items - if update.get("status") != CourseInfoModule.STATUS_DELETED]) + if update.get("status") != CourseInfoBlock.STATUS_DELETED]) # pylint: disable=unused-argument @@ -136,7 +135,7 @@ def delete_course_update(location, update, passed_id, user): if 0 < passed_index <= len(course_update_items): course_update_item = course_update_items[passed_index - 1] # soft delete course update item - course_update_item["status"] = CourseInfoModule.STATUS_DELETED + course_update_item["status"] = CourseInfoBlock.STATUS_DELETED course_update_items[passed_index - 1] = course_update_item # update db record diff --git a/cms/djangoapps/contentstore/courseware_index.py b/cms/djangoapps/contentstore/courseware_index.py index 4161571e18..2ca873688d 100644 --- a/cms/djangoapps/contentstore/courseware_index.py +++ b/cms/djangoapps/contentstore/courseware_index.py @@ -11,7 +11,7 @@ from django.urls import resolve from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy from search.search_engine_base import SearchEngine -from six import add_metaclass +from six import add_metaclass, string_types, text_type from contentstore.course_group_config import GroupConfiguration from course_modes.models import CourseMode @@ -193,22 +193,22 @@ class SearchIndexerBase(object): for split_test_child in item.get_children(): if split_partition: for group in split_partition.groups: - group_id = unicode(group.id) + group_id = text_type(group.id) child_location = item.group_id_to_child.get(group_id, None) if child_location == split_test_child.location: groups_usage_info.update({ - unicode(get_item_location(split_test_child)): [group_id], + text_type(get_item_location(split_test_child)): [group_id], }) for component in split_test_child.get_children(): groups_usage_info.update({ - unicode(get_item_location(component)): [group_id] + text_type(get_item_location(component)): [group_id] }) if groups_usage_info: item_location = get_item_location(item) - item_content_groups = groups_usage_info.get(unicode(item_location), None) + item_content_groups = groups_usage_info.get(text_type(item_location), None) - item_id = unicode(cls._id_modifier(item.scope_ids.usage_id)) + item_id = text_type(cls._id_modifier(item.scope_ids.usage_id)) indexed_items.add(item_id) if item.has_children: # determine if it's okay to skip adding the children herein based upon how recently any may have changed @@ -367,7 +367,7 @@ class CoursewareSearchIndexer(SearchIndexerBase): @classmethod def _get_location_info(cls, normalized_structure_key): """ Builds location info dictionary """ - return {"course": unicode(normalized_structure_key), "org": normalized_structure_key.org} + return {"course": text_type(normalized_structure_key), "org": normalized_structure_key.org} @classmethod def do_course_reindex(cls, modulestore, course_key): @@ -389,7 +389,7 @@ class CoursewareSearchIndexer(SearchIndexerBase): for name, group in groups.items(): for module in group: view, args, kwargs = resolve(module['url']) # pylint: disable=unused-variable - usage_key_string = unicode(kwargs['usage_key_string']) + usage_key_string = text_type(kwargs['usage_key_string']) if groups_usage_dict.get(usage_key_string, None): groups_usage_dict[usage_key_string].append(name) else: @@ -418,7 +418,7 @@ class CoursewareSearchIndexer(SearchIndexerBase): while parent is not None: path_component_name = parent.display_name if not path_component_name: - path_component_name = unicode(cls.UNNAMED_MODULE_NAME) + path_component_name = text_type(cls.UNNAMED_MODULE_NAME) location_path.append(path_component_name) parent = parent.get_parent() location_path.reverse() @@ -454,7 +454,7 @@ class LibrarySearchIndexer(SearchIndexerBase): @classmethod def _get_location_info(cls, normalized_structure_key): """ Builds location info dictionary """ - return {"library": unicode(normalized_structure_key)} + return {"library": text_type(normalized_structure_key)} @classmethod def _id_modifier(cls, usage_id): @@ -586,7 +586,7 @@ class CourseAboutSearchIndexer(object): if not searcher: return - course_id = unicode(course.id) + course_id = text_type(course.id) course_info = { 'id': course_id, 'course': course_id, @@ -621,7 +621,7 @@ class CourseAboutSearchIndexer(object): if section_content: if about_information.index_flags & AboutInfo.ANALYSE: analyse_content = section_content - if isinstance(section_content, basestring): + if isinstance(section_content, string_types): analyse_content = strip_html_content_to_text(section_content) course_info['content'][about_information.property_name] = analyse_content if about_information.index_flags & AboutInfo.PROPERTY: @@ -645,7 +645,7 @@ class CourseAboutSearchIndexer(object): @classmethod def _get_location_info(cls, normalized_structure_key): """ Builds location info dictionary """ - return {"course": unicode(normalized_structure_key), "org": normalized_structure_key.org} + return {"course": text_type(normalized_structure_key), "org": normalized_structure_key.org} @classmethod def remove_deleted_items(cls, structure_key): diff --git a/cms/djangoapps/contentstore/debug_file_uploader.py b/cms/djangoapps/contentstore/debug_file_uploader.py index 12acfe95c3..30c0651f6d 100644 --- a/cms/djangoapps/contentstore/debug_file_uploader.py +++ b/cms/djangoapps/contentstore/debug_file_uploader.py @@ -1,3 +1,7 @@ +""" Upload file handler to help test progress bars in uploads. """ + +from __future__ import absolute_import + import time from django.core.files.uploadhandler import FileUploadHandler diff --git a/cms/djangoapps/contentstore/git_export_utils.py b/cms/djangoapps/contentstore/git_export_utils.py index 086f9d9f34..ed3922aca8 100644 --- a/cms/djangoapps/contentstore/git_export_utils.py +++ b/cms/djangoapps/contentstore/git_export_utils.py @@ -3,15 +3,18 @@ Utilities for export a course's XML into a git repository, committing and pushing the changes. """ +from __future__ import absolute_import + import logging import os import subprocess -from urlparse import urlparse +import six from django.conf import settings from django.contrib.auth.models import User from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from six.moves.urllib.parse import urlparse # pylint: disable=import-error from xmodule.contentstore.django import contentstore from xmodule.modulestore.django import modulestore @@ -32,7 +35,7 @@ class GitExportError(Exception): def __init__(self, message): # Force the lazy i18n values to turn into actual unicode objects - super(GitExportError, self).__init__(unicode(message)) + super(GitExportError, self).__init__(six.text_type(message)) NO_EXPORT_DIR = _(u"GIT_REPO_EXPORT_DIR not set or path {0} doesn't exist, " "please create it, or configure a different path with " diff --git a/cms/djangoapps/contentstore/management/commands/clean_cert_name.py b/cms/djangoapps/contentstore/management/commands/clean_cert_name.py index 25c3636626..4477a146ca 100644 --- a/cms/djangoapps/contentstore/management/commands/clean_cert_name.py +++ b/cms/djangoapps/contentstore/management/commands/clean_cert_name.py @@ -3,11 +3,13 @@ A single-use management command that provides an interactive way to remove erroneous certificate names. """ +from __future__ import absolute_import + from collections import namedtuple -from six.moves import input -from six import text_type from django.core.management.base import BaseCommand +from six import text_type +from six.moves import input, range, zip from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore diff --git a/cms/djangoapps/contentstore/management/commands/cleanup_assets.py b/cms/djangoapps/contentstore/management/commands/cleanup_assets.py index 0eff9ad035..f335b11c66 100644 --- a/cms/djangoapps/contentstore/management/commands/cleanup_assets.py +++ b/cms/djangoapps/contentstore/management/commands/cleanup_assets.py @@ -2,6 +2,8 @@ Script for removing all redundant Mac OS metadata files (with filename ".DS_Store" or with filename which starts with "._") for all courses """ +from __future__ import absolute_import + import logging from django.core.management.base import BaseCommand diff --git a/cms/djangoapps/contentstore/management/commands/create_course.py b/cms/djangoapps/contentstore/management/commands/create_course.py index fdfad57927..0f4c7877bc 100644 --- a/cms/djangoapps/contentstore/management/commands/create_course.py +++ b/cms/djangoapps/contentstore/management/commands/create_course.py @@ -1,18 +1,19 @@ """ Django management command to create a course in a specific modulestore """ +from __future__ import absolute_import + from datetime import datetime, timedelta -from six import text_type from django.contrib.auth.models import User from django.core.management.base import BaseCommand, CommandError +from six import text_type 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) diff --git a/cms/djangoapps/contentstore/management/commands/delete_course.py b/cms/djangoapps/contentstore/management/commands/delete_course.py index 1f6c706f1e..f16ea35a68 100644 --- a/cms/djangoapps/contentstore/management/commands/delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/delete_course.py @@ -1,14 +1,18 @@ -from __future__ import print_function -from six import text_type +""" +Management Command to delete course. +""" +from __future__ import absolute_import, print_function from django.core.management.base import BaseCommand, CommandError from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from six import text_type from contentstore.utils import delete_course from xmodule.contentstore.django import contentstore from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore + from .prompt import query_yes_no diff --git a/cms/djangoapps/contentstore/management/commands/delete_orphans.py b/cms/djangoapps/contentstore/management/commands/delete_orphans.py index 146f283ab4..0c1b7c427c 100644 --- a/cms/djangoapps/contentstore/management/commands/delete_orphans.py +++ b/cms/djangoapps/contentstore/management/commands/delete_orphans.py @@ -1,5 +1,5 @@ """Script for deleting orphans""" -from __future__ import print_function +from __future__ import absolute_import, print_function from django.core.management.base import BaseCommand, CommandError from opaque_keys import InvalidKeyError diff --git a/cms/djangoapps/contentstore/management/commands/edit_course_tabs.py b/cms/djangoapps/contentstore/management/commands/edit_course_tabs.py index 14ebfcbbd6..f59dd7bdba 100644 --- a/cms/djangoapps/contentstore/management/commands/edit_course_tabs.py +++ b/cms/djangoapps/contentstore/management/commands/edit_course_tabs.py @@ -6,7 +6,7 @@ # Run it this way: # ./manage.py cms --settings dev edit_course_tabs --course Stanford/CS99/2013_spring # -from __future__ import print_function +from __future__ import absolute_import, print_function from django.core.management.base import BaseCommand, CommandError from opaque_keys.edx.keys import CourseKey diff --git a/cms/djangoapps/contentstore/management/commands/empty_asset_trashcan.py b/cms/djangoapps/contentstore/management/commands/empty_asset_trashcan.py index 563d9c44b6..c4c78f33aa 100644 --- a/cms/djangoapps/contentstore/management/commands/empty_asset_trashcan.py +++ b/cms/djangoapps/contentstore/management/commands/empty_asset_trashcan.py @@ -1,4 +1,8 @@ -from django.core.management.base import BaseCommand, CommandError +"""Script to Empty the trashcan""" + +from __future__ import absolute_import + +from django.core.management.base import BaseCommand from opaque_keys.edx.keys import CourseKey from xmodule.contentstore.utils import empty_asset_trashcan @@ -8,6 +12,9 @@ from .prompt import query_yes_no class Command(BaseCommand): + """ + Command to Empty the trashcan. + """ help = 'Empty the trashcan. Can pass an optional course_id to limit the damage.' def add_arguments(self, parser): diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index ae56d8f231..db188a87bf 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -1,7 +1,8 @@ """ Script for exporting courseware from Mongo to a tar.gz file """ -from __future__ import print_function +from __future__ import absolute_import, print_function + import os from django.core.management.base import BaseCommand, CommandError diff --git a/cms/djangoapps/contentstore/management/commands/export_all_courses.py b/cms/djangoapps/contentstore/management/commands/export_all_courses.py index 4fd67dfa29..b6933f9309 100644 --- a/cms/djangoapps/contentstore/management/commands/export_all_courses.py +++ b/cms/djangoapps/contentstore/management/commands/export_all_courses.py @@ -1,10 +1,10 @@ """ Script for exporting all courseware from Mongo to a directory and listing the courses which failed to export """ -from __future__ import print_function -from six import text_type +from __future__ import absolute_import, print_function from django.core.management.base import BaseCommand +from six import text_type from xmodule.contentstore.django import contentstore from xmodule.modulestore.django import modulestore diff --git a/cms/djangoapps/contentstore/management/commands/export_content_library.py b/cms/djangoapps/contentstore/management/commands/export_content_library.py index 9c914485a1..fd47644d30 100644 --- a/cms/djangoapps/contentstore/management/commands/export_content_library.py +++ b/cms/djangoapps/contentstore/management/commands/export_content_library.py @@ -1,7 +1,8 @@ """ Script for exporting a content library from Mongo to a tar.gz file """ -from __future__ import print_function +from __future__ import absolute_import, print_function + import os import shutil @@ -9,9 +10,9 @@ from django.core.management.base import BaseCommand, CommandError from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import LibraryLocator -from xmodule.modulestore.django import modulestore from cms.djangoapps.contentstore import tasks +from xmodule.modulestore.django import modulestore class Command(BaseCommand): diff --git a/cms/djangoapps/contentstore/management/commands/export_olx.py b/cms/djangoapps/contentstore/management/commands/export_olx.py index 4d02aeefd7..bb11b7ad7b 100644 --- a/cms/djangoapps/contentstore/management/commands/export_olx.py +++ b/cms/djangoapps/contentstore/management/commands/export_olx.py @@ -14,6 +14,8 @@ At present, it differs from Studio exports in several ways: * It only supports the export of courses. It does not export libraries. """ +from __future__ import absolute_import + import os import re import shutil diff --git a/cms/djangoapps/contentstore/management/commands/fix_not_found.py b/cms/djangoapps/contentstore/management/commands/fix_not_found.py index 7c41230417..05ddf9009f 100644 --- a/cms/djangoapps/contentstore/management/commands/fix_not_found.py +++ b/cms/djangoapps/contentstore/management/commands/fix_not_found.py @@ -1,13 +1,14 @@ """ Script for fixing the item not found errors in a course """ +from __future__ import absolute_import + from django.core.management.base import BaseCommand, CommandError from opaque_keys.edx.keys import CourseKey from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore - # To run from command line: ./manage.py cms fix_not_found course-v1:org+course+run diff --git a/cms/djangoapps/contentstore/management/commands/force_publish.py b/cms/djangoapps/contentstore/management/commands/force_publish.py index 22839e386b..f02a3404fd 100644 --- a/cms/djangoapps/contentstore/management/commands/force_publish.py +++ b/cms/djangoapps/contentstore/management/commands/force_publish.py @@ -1,7 +1,8 @@ """ Script for force publishing a course """ -from __future__ import print_function +from __future__ import absolute_import, print_function + from django.core.management.base import BaseCommand, CommandError from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey @@ -12,7 +13,6 @@ from xmodule.modulestore.django import modulestore from .prompt import query_yes_no from .utils import get_course_versions - # To run from command line: ./manage.py cms force_publish course-v1:org+course+run diff --git a/cms/djangoapps/contentstore/management/commands/generate_courses.py b/cms/djangoapps/contentstore/management/commands/generate_courses.py index ab8b2fdeed..eaa4a530a2 100644 --- a/cms/djangoapps/contentstore/management/commands/generate_courses.py +++ b/cms/djangoapps/contentstore/management/commands/generate_courses.py @@ -1,12 +1,14 @@ """ Django management command to generate a test course from a course config json """ +from __future__ import absolute_import + import json import logging -from six import text_type from django.contrib.auth.models import User from django.core.management.base import BaseCommand, CommandError +from six import text_type from contentstore.management.commands.utils import user_from_str from contentstore.views.course import create_new_course_in_store @@ -73,7 +75,7 @@ class Command(BaseCommand): def _process_course_fields(self, fields): """ Returns a validated list of course fields """ - all_fields = CourseFields.__dict__.keys() + all_fields = list(CourseFields.__dict__.keys()) non_course_fields = [ "__doc__", "__module__", diff --git a/cms/djangoapps/contentstore/management/commands/git_export.py b/cms/djangoapps/contentstore/management/commands/git_export.py index 27f646ea3a..dd33c3cde5 100644 --- a/cms/djangoapps/contentstore/management/commands/git_export.py +++ b/cms/djangoapps/contentstore/management/commands/git_export.py @@ -13,13 +13,15 @@ This functionality is also available as an export view in studio if the giturl attribute is set and the FEATURE['ENABLE_EXPORT_GIT'] is set. """ +from __future__ import absolute_import + import logging -from six import text_type from django.core.management.base import BaseCommand, CommandError from django.utils.translation import ugettext as _ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from six import text_type import contentstore.git_export_utils as git_export_utils from contentstore.git_export_utils import GitExportError diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 7aed4105d1..c2e9d54df4 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -1,13 +1,16 @@ """ Script for importing courseware from XML format """ +from __future__ import absolute_import + from django.core.management.base import BaseCommand + from openedx.core.djangoapps.django_comment_common.utils import are_permissions_roles_seeded, seed_permissions_roles from xmodule.contentstore.django import contentstore from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore -from xmodule.util.sandboxing import DEFAULT_PYTHON_LIB_FILENAME from xmodule.modulestore.xml_importer import import_course_from_xml +from xmodule.util.sandboxing import DEFAULT_PYTHON_LIB_FILENAME class Command(BaseCommand): diff --git a/cms/djangoapps/contentstore/management/commands/import_content_library.py b/cms/djangoapps/contentstore/management/commands/import_content_library.py index 014fa37946..20f63a1e87 100644 --- a/cms/djangoapps/contentstore/management/commands/import_content_library.py +++ b/cms/djangoapps/contentstore/management/commands/import_content_library.py @@ -1,7 +1,7 @@ """ Script for importing a content library from a tar.gz file """ -from __future__ import print_function +from __future__ import absolute_import, print_function import base64 import os @@ -14,15 +14,16 @@ from django.core.management.base import BaseCommand, CommandError from lxml import etree from opaque_keys.edx.locator import LibraryLocator from path import Path +from six.moves import input + +from cms.djangoapps.contentstore.utils import add_instructor +from openedx.core.lib.extract_tar import safetar_extractall from xmodule.contentstore.django import contentstore from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import DuplicateCourseError from xmodule.modulestore.xml_importer import import_library_from_xml -from cms.djangoapps.contentstore.utils import add_instructor -from openedx.core.lib.extract_tar import safetar_extractall - class Command(BaseCommand): """ @@ -76,7 +77,7 @@ class Command(BaseCommand): # Check if data would be overwritten ans = '' while not created and ans not in ['y', 'yes', 'n', 'no']: - inp = raw_input(u'Library "{0}" already exists, overwrite it? [y/n] '.format(courselike_key)) + inp = input(u'Library "{0}" already exists, overwrite it? [y/n] '.format(courselike_key)) ans = inp.lower() if ans.startswith('n'): print(u'Aborting import of "{0}"'.format(courselike_key)) diff --git a/cms/djangoapps/contentstore/management/commands/migrate_to_split.py b/cms/djangoapps/contentstore/management/commands/migrate_to_split.py index 3655d8b76b..2003058646 100644 --- a/cms/djangoapps/contentstore/management/commands/migrate_to_split.py +++ b/cms/djangoapps/contentstore/management/commands/migrate_to_split.py @@ -2,6 +2,8 @@ Django management command to migrate a course from the old Mongo modulestore to the new split-Mongo modulestore. """ +from __future__ import absolute_import + from django.contrib.auth.models import User from django.core.management.base import BaseCommand, CommandError from opaque_keys import InvalidKeyError diff --git a/cms/djangoapps/contentstore/management/commands/migrate_transcripts.py b/cms/djangoapps/contentstore/management/commands/migrate_transcripts.py index b869f76898..5c66732a64 100644 --- a/cms/djangoapps/contentstore/management/commands/migrate_transcripts.py +++ b/cms/djangoapps/contentstore/management/commands/migrate_transcripts.py @@ -2,19 +2,24 @@ Command to migrate transcripts to django storage. """ +from __future__ import absolute_import + 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 six.moves import map + from cms.djangoapps.contentstore.tasks import ( DEFAULT_ALL_COURSES, - DEFAULT_FORCE_UPDATE, DEFAULT_COMMIT, + DEFAULT_FORCE_UPDATE, enqueue_async_migrate_transcripts_tasks ) +from openedx.core.djangoapps.video_config.models import MigrationEnqueuedCourse, TranscriptMigrationSetting from openedx.core.lib.command_utils import get_mutually_exclusive_required_option, parse_course_keys -from openedx.core.djangoapps.video_config.models import TranscriptMigrationSetting, MigrationEnqueuedCourse from xmodule.modulestore.django import modulestore log = logging.getLogger(__name__) @@ -91,7 +96,7 @@ class Command(BaseCommand): 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']) + course_keys = list(map(self._parse_course_key, options['course_ids'])) else: migration_settings = self._latest_settings() if migration_settings.all_courses: diff --git a/cms/djangoapps/contentstore/management/commands/prompt.py b/cms/djangoapps/contentstore/management/commands/prompt.py index e658738128..927c5c916c 100644 --- a/cms/djangoapps/contentstore/management/commands/prompt.py +++ b/cms/djangoapps/contentstore/management/commands/prompt.py @@ -1,5 +1,12 @@ +""" +Takes user input. +""" +from __future__ import absolute_import + import sys +from six.moves import input + def query_yes_no(question, default="yes"): """Ask a yes/no question via raw_input() and return their answer. @@ -29,7 +36,7 @@ def query_yes_no(question, default="yes"): while True: sys.stdout.write(question + prompt) - choice = raw_input().lower() + choice = input().lower() if default is not None and choice == '': return valid[default] elif choice in valid: diff --git a/cms/djangoapps/contentstore/management/commands/reindex_course.py b/cms/djangoapps/contentstore/management/commands/reindex_course.py index 0171c80dde..0055dbd31f 100644 --- a/cms/djangoapps/contentstore/management/commands/reindex_course.py +++ b/cms/djangoapps/contentstore/management/commands/reindex_course.py @@ -1,4 +1,6 @@ """ Management command to update courses' search index """ +from __future__ import absolute_import + import logging from textwrap import dedent @@ -8,6 +10,7 @@ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import CourseLocator from search.search_engine_base import SearchEngine +from six.moves import map from contentstore.courseware_index import CoursewareSearchIndexer from xmodule.modulestore.django import modulestore @@ -101,7 +104,7 @@ class Command(BaseCommand): return else: # in case course keys are provided as arguments - course_keys = map(self._parse_course_key, course_ids) + course_keys = list(map(self._parse_course_key, course_ids)) for course_key in course_keys: CoursewareSearchIndexer.do_course_reindex(store, course_key) diff --git a/cms/djangoapps/contentstore/management/commands/reindex_library.py b/cms/djangoapps/contentstore/management/commands/reindex_library.py index 121ef45e0d..71d2f1fe13 100644 --- a/cms/djangoapps/contentstore/management/commands/reindex_library.py +++ b/cms/djangoapps/contentstore/management/commands/reindex_library.py @@ -1,10 +1,12 @@ """ Management command to update libraries' search index """ -from __future__ import print_function +from __future__ import absolute_import, print_function + from textwrap import dedent from django.core.management import BaseCommand, CommandError from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import LibraryLocator +from six.moves import map from contentstore.courseware_index import LibrarySearchIndexer from xmodule.modulestore.django import modulestore @@ -58,7 +60,7 @@ class Command(BaseCommand): else: return else: - library_keys = map(self._parse_library_key, options['library_ids']) + library_keys = list(map(self._parse_library_key, options['library_ids'])) for library_key in library_keys: print(u"Indexing library {}".format(library_key)) diff --git a/cms/djangoapps/contentstore/management/commands/restore_asset_from_trashcan.py b/cms/djangoapps/contentstore/management/commands/restore_asset_from_trashcan.py index ca8de0ceb7..72ee2c5afa 100644 --- a/cms/djangoapps/contentstore/management/commands/restore_asset_from_trashcan.py +++ b/cms/djangoapps/contentstore/management/commands/restore_asset_from_trashcan.py @@ -1,9 +1,14 @@ -from django.core.management.base import BaseCommand, CommandError +"""Management command to restore assets from trash""" + +from __future__ import absolute_import + +from django.core.management.base import BaseCommand from xmodule.contentstore.utils import restore_asset_from_trashcan class Command(BaseCommand): + """Command class to handle asset restore""" help = '''Restore a deleted asset from the trashcan back to it's original course''' def add_arguments(self, parser): diff --git a/cms/djangoapps/contentstore/management/commands/sync_courses.py b/cms/djangoapps/contentstore/management/commands/sync_courses.py new file mode 100644 index 0000000000..47cf557608 --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/sync_courses.py @@ -0,0 +1,70 @@ +""" +Sync courses from catalog service. This is used to setup a master's +integration environment. +""" +import logging +from textwrap import dedent + +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand, CommandError +from opaque_keys.edx.keys import CourseKey +from six import text_type + +from contentstore.management.commands.utils import user_from_str +from contentstore.views.course import create_new_course_in_store +from openedx.core.djangoapps.catalog.utils import get_course_runs +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.exceptions import DuplicateCourseError + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + """ + Command to populate modulestore with courses from the discovery service. + + Example: ./manage.py cms sync_courses staff@example.com + """ + help = dedent(__doc__).strip() + + def add_arguments(self, parser): + parser.add_argument('instructor') + + def get_user(self, user): + """ + Return a User object. + """ + try: + user_object = user_from_str(user) + except User.DoesNotExist: + raise CommandError(u"No user {user} found.".format(user=user)) + return user_object + + def handle(self, *args, **options): + """Execute the command""" + instructor = self.get_user(options['instructor']) + + course_runs = get_course_runs() + for course_run in course_runs: + course_key = CourseKey.from_string(course_run.get('key')) + fields = { + "display_name": course_run.get('title') + } + + try: + new_course = create_new_course_in_store( + ModuleStoreEnum.Type.split, + instructor, + course_key.org, + course_key.course, + course_key.run, + fields, + ) + logger.info(u"Created {}".format(text_type(new_course.id))) + except DuplicateCourseError: + logger.warning( + u"Course already exists for %s, %s, %s. Skipping", + course_key.org, + course_key.course, + course_key.run, + ) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_cleanup_assets.py b/cms/djangoapps/contentstore/management/commands/tests/test_cleanup_assets.py index 0930c6571d..b48d3e507e 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_cleanup_assets.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_cleanup_assets.py @@ -2,20 +2,20 @@ Test for assets cleanup of courses for Mac OS metadata files (with filename ".DS_Store" or with filename which starts with "._") """ -from django.core.management import call_command +from __future__ import absolute_import +from django.conf import settings +from django.core.management import call_command from opaque_keys.edx.keys import CourseKey + from xmodule.contentstore.content import XASSET_LOCATION_TAG from xmodule.contentstore.django import contentstore -from xmodule.modulestore.django import modulestore from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore from xmodule.modulestore.mongo.base import location_to_query from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.utils import ( - add_temp_files_from_dict, remove_temp_files_from_list, DOT_FILES_DICT -) +from xmodule.modulestore.tests.utils import DOT_FILES_DICT, add_temp_files_from_dict, remove_temp_files_from_list from xmodule.modulestore.xml_importer import import_course_from_xml -from django.conf import settings TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT @@ -32,7 +32,7 @@ class ExportAllCourses(ModuleStoreTestCase): self.content_store = contentstore() # pylint: disable=protected-access self.module_store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) - self.addCleanup(remove_temp_files_from_list, DOT_FILES_DICT.keys(), self.course_dir / "static") + self.addCleanup(remove_temp_files_from_list, list(DOT_FILES_DICT.keys()), self.course_dir / "static") add_temp_files_from_dict(DOT_FILES_DICT, self.course_dir / "static") def test_export_all_courses(self): 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 d235880691..d9e90d57ee 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_create_course.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_create_course.py @@ -1,14 +1,18 @@ """ Unittests for creating a course in an chosen modulestore """ -from StringIO import StringIO +from __future__ import absolute_import + +from six import StringIO + import ddt +import six from django.core.management import CommandError, call_command from django.test import TestCase from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase class TestArgParsing(TestCase): @@ -19,7 +23,10 @@ class TestArgParsing(TestCase): super(TestArgParsing, self).setUp() def test_no_args(self): - errstring = "Error: too few arguments" + if six.PY2: + errstring = "Error: too few arguments" + else: + errstring = "Error: the following arguments are required: modulestore, user, org, number, run" with self.assertRaisesRegexp(CommandError, errstring): call_command('create_course') @@ -108,7 +115,7 @@ class TestCreateCourse(ModuleStoreTestCase): ) course = self.store.get_course(lowercase_course_id) self.assertIsNotNone(course, 'Course not found using lowercase course key.') - self.assertEqual(unicode(course.id), unicode(lowercase_course_id)) + self.assertEqual(six.text_type(course.id), six.text_type(lowercase_course_id)) # Verify store does not return course with different case. uppercase_course_id = self.store.make_course_key(org.upper(), number.upper(), run.upper()) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py b/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py index dc72d78e36..8d0388aa14 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py @@ -1,3 +1,8 @@ +""" +Delete course tests. +""" +from __future__ import absolute_import + import mock from django.core.management import CommandError, call_command @@ -6,7 +11,7 @@ from student.tests.factories import UserFactory from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore from xmodule.modulestore.django import modulestore -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DATA_SPLIT_MODULESTORE +from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_delete_orphans.py b/cms/djangoapps/contentstore/management/commands/tests/test_delete_orphans.py index 816d579eb6..8d4dbf720e 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_delete_orphans.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_delete_orphans.py @@ -1,11 +1,14 @@ """Tests running the delete_orphan command""" -import ddt -from django.core.management import call_command, CommandError -from contentstore.tests.test_orphan import TestOrphanBase +from __future__ import absolute_import -from xmodule.modulestore.tests.factories import CourseFactory +import ddt +import six +from django.core.management import CommandError, call_command + +from contentstore.tests.test_orphan import TestOrphanBase from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.tests.factories import CourseFactory @ddt.ddt @@ -18,7 +21,11 @@ class TestDeleteOrphan(TestOrphanBase): """ Test delete_orphans command with no arguments """ - with self.assertRaisesRegexp(CommandError, 'Error: too few arguments'): + if six.PY2: + errstring = 'Error: too few arguments' + else: + errstring = 'Error: the following arguments are required: course_id' + with self.assertRaisesRegexp(CommandError, errstring): call_command('delete_orphans') @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) @@ -28,7 +35,7 @@ class TestDeleteOrphan(TestOrphanBase): results in no orphans being deleted """ course = self.create_course_with_orphans(default_store) - call_command('delete_orphans', unicode(course.id)) + call_command('delete_orphans', six.text_type(course.id)) self.assertTrue(self.store.has_item(course.id.make_usage_key('html', 'multi_parent_html'))) self.assertTrue(self.store.has_item(course.id.make_usage_key('vertical', 'OrphanVert'))) self.assertTrue(self.store.has_item(course.id.make_usage_key('chapter', 'OrphanChapter'))) @@ -42,7 +49,7 @@ class TestDeleteOrphan(TestOrphanBase): """ course = self.create_course_with_orphans(default_store) - call_command('delete_orphans', unicode(course.id), '--commit') + call_command('delete_orphans', six.text_type(course.id), '--commit') # make sure this module wasn't deleted self.assertTrue(self.store.has_item(course.id.make_usage_key('html', 'multi_parent_html'))) @@ -66,7 +73,7 @@ class TestDeleteOrphan(TestOrphanBase): # call delete orphans, specifying the published branch # of the course - call_command('delete_orphans', unicode(published_branch), '--commit') + call_command('delete_orphans', six.text_type(published_branch), '--commit') # now all orphans should be deleted self.assertOrphanCount(course.id, 0) @@ -113,6 +120,6 @@ class TestDeleteOrphan(TestOrphanBase): # there should be one in published self.assertOrphanCount(course.id, 0) self.assertOrphanCount(published_branch, 1) - self.assertIn(orphan, self.store.get_items(published_branch)) + self.assertIn(orphan.location, [x.location for x in self.store.get_items(published_branch)]) return course, orphan diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_export.py b/cms/djangoapps/contentstore/management/commands/tests/test_export.py index 32123455c0..4e02926125 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_export.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_export.py @@ -1,11 +1,14 @@ """ Tests for exporting courseware to the desired path """ +from __future__ import absolute_import + import shutil import unittest from tempfile import mkdtemp import ddt +import six from django.core.management import CommandError, call_command from xmodule.modulestore import ModuleStoreEnum @@ -22,7 +25,10 @@ class TestArgParsingCourseExport(unittest.TestCase): """ Test export command with no arguments """ - errstring = "Error: too few arguments" + if six.PY2: + errstring = "Error: too few arguments" + else: + errstring = "Error: the following arguments are required: course_id, output_path" with self.assertRaisesRegexp(CommandError, errstring): call_command('export') @@ -49,7 +55,7 @@ class TestCourseExport(ModuleStoreTestCase): Create a new course try exporting in a path specified """ course = CourseFactory.create(default_store=store) - course_id = unicode(course.id) + course_id = six.text_type(course.id) self.assertTrue( modulestore().has_course(course.id), u"Could not find course in {}".format(store) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_export_all_courses.py b/cms/djangoapps/contentstore/management/commands/tests/test_export_all_courses.py index 5013bce8cd..6d843eb6c8 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_export_all_courses.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_export_all_courses.py @@ -1,11 +1,14 @@ """ Test for export all courses. """ +from __future__ import absolute_import + import shutil from tempfile import mkdtemp -from contentstore.management.commands.export_all_courses import export_courses_to_output_path +import six +from contentstore.management.commands.export_all_courses import export_courses_to_output_path from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -47,4 +50,4 @@ class ExportAllCourses(ModuleStoreTestCase): courses, failed_export_courses = export_courses_to_output_path(self.temp_dir) self.assertEqual(len(courses), 2) self.assertEqual(len(failed_export_courses), 1) - self.assertEqual(failed_export_courses[0], unicode(second_course_id)) + self.assertEqual(failed_export_courses[0], six.text_type(second_course_id)) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_export_olx.py b/cms/djangoapps/contentstore/management/commands/tests/test_export_olx.py index ebfe27ff77..a508505fa3 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_export_olx.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_export_olx.py @@ -2,13 +2,16 @@ Tests for exporting OLX content. """ +from __future__ import absolute_import + import shutil import tarfile import unittest -from StringIO import StringIO +from six import StringIO from tempfile import mkdtemp import ddt +import six from django.core.management import CommandError, call_command from path import Path as path @@ -79,7 +82,7 @@ class TestCourseExportOlx(ModuleStoreTestCase): tmp_dir = path(mkdtemp()) self.addCleanup(shutil.rmtree, tmp_dir) filename = tmp_dir / 'test.tar.gz' - call_command('export_olx', '--output', filename, unicode(test_course_key)) + call_command('export_olx', '--output', filename, six.text_type(test_course_key)) with tarfile.open(filename) as tar_file: self.check_export_file(tar_file, test_course_key) @@ -87,7 +90,7 @@ class TestCourseExportOlx(ModuleStoreTestCase): def test_export_course_stdout(self, store_type): test_course_key = self.create_dummy_course(store_type) out = StringIO() - call_command('export_olx', unicode(test_course_key), stdout=out) + call_command('export_olx', six.text_type(test_course_key), stdout=out) out.seek(0) output = out.read() with tarfile.open(fileobj=StringIO(output)) as tar_file: diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_fix_not_found.py b/cms/djangoapps/contentstore/management/commands/tests/test_fix_not_found.py index dac5ad0393..5a0894b678 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_fix_not_found.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_fix_not_found.py @@ -2,7 +2,11 @@ Tests for the fix_not_found management command """ +from __future__ import absolute_import + +import six from django.core.management import CommandError, call_command + from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -25,7 +29,7 @@ class TestFixNotFound(ModuleStoreTestCase): """ course = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo) with self.assertRaisesRegexp(CommandError, "The owning modulestore does not support this command."): - call_command("fix_not_found", unicode(course.id)) + call_command("fix_not_found", six.text_type(course.id)) def test_fix_not_found(self): course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) @@ -45,7 +49,7 @@ class TestFixNotFound(ModuleStoreTestCase): self.assertEqual(len(course.children), 2) self.assertIn(dangling_pointer, course.children) - call_command("fix_not_found", unicode(course.id)) + call_command("fix_not_found", six.text_type(course.id)) # make sure the dangling pointer was removed from # the course block's children diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py b/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py index 5fd69318e6..600deaba86 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_force_publish.py @@ -1,13 +1,17 @@ """ Tests for the force_publish management command """ +from __future__ import absolute_import + import mock -from django.core.management import call_command, CommandError -from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase, ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +import six +from django.core.management import CommandError, call_command + from contentstore.management.commands.force_publish import Command from contentstore.management.commands.utils import get_course_versions +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory class TestForcePublish(SharedModuleStoreTestCase): @@ -25,7 +29,11 @@ class TestForcePublish(SharedModuleStoreTestCase): """ Test 'force_publish' command with no arguments """ - errstring = "Error: too few arguments" + if six.PY2: + errstring = "Error: too few arguments" + else: + errstring = "Error: the following arguments are required: course_key" + with self.assertRaisesRegexp(CommandError, errstring): call_command('force_publish') @@ -43,7 +51,7 @@ class TestForcePublish(SharedModuleStoreTestCase): """ errstring = "Error: unrecognized arguments: invalid-arg" with self.assertRaisesRegexp(CommandError, errstring): - call_command('force_publish', unicode(self.course.id), '--commit', 'invalid-arg') + call_command('force_publish', six.text_type(self.course.id), '--commit', 'invalid-arg') def test_course_key_not_found(self): """ @@ -51,7 +59,7 @@ class TestForcePublish(SharedModuleStoreTestCase): """ errstring = "Course not found." with self.assertRaisesRegexp(CommandError, errstring): - call_command('force_publish', unicode('course-v1:org+course+run')) + call_command('force_publish', six.text_type('course-v1:org+course+run')) def test_force_publish_non_split(self): """ @@ -60,7 +68,7 @@ class TestForcePublish(SharedModuleStoreTestCase): course = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo) errstring = 'The owning modulestore does not support this command.' with self.assertRaisesRegexp(CommandError, errstring): - call_command('force_publish', unicode(course.id)) + call_command('force_publish', six.text_type(course.id)) class TestForcePublishModifications(ModuleStoreTestCase): @@ -92,7 +100,7 @@ class TestForcePublishModifications(ModuleStoreTestCase): self.assertTrue(self.store.has_changes(self.store.get_item(self.course.location))) # get draft and publish branch versions - versions = get_course_versions(unicode(self.course.id)) + versions = get_course_versions(six.text_type(self.course.id)) draft_version = versions['draft-branch'] published_version = versions['published-branch'] @@ -103,13 +111,13 @@ class TestForcePublishModifications(ModuleStoreTestCase): patched_yes_no.return_value = True # force publish course - call_command('force_publish', unicode(self.course.id), '--commit') + call_command('force_publish', six.text_type(self.course.id), '--commit') # verify that course has no changes self.assertFalse(self.store.has_changes(self.store.get_item(self.course.location))) # get new draft and publish branch versions - versions = get_course_versions(unicode(self.course.id)) + versions = get_course_versions(six.text_type(self.course.id)) new_draft_version = versions['draft-branch'] new_published_version = versions['published-branch'] diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_generate_courses.py b/cms/djangoapps/contentstore/management/commands/tests/test_generate_courses.py index aa83b9d7ce..9d2e560dd4 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_generate_courses.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_generate_courses.py @@ -1,6 +1,8 @@ """ Unittest for generate a test course in an given modulestore """ +from __future__ import absolute_import + import json import ddt diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_git_export.py b/cms/djangoapps/contentstore/management/commands/tests/test_git_export.py index 9b2583b85f..ae21e494c5 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_git_export.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_git_export.py @@ -2,23 +2,26 @@ Unittests for exporting to git via management command. """ +from __future__ import absolute_import + import copy import os import shutil -import StringIO +from six import StringIO import subprocess import unittest from uuid import uuid4 +import six from django.conf import settings from django.core.management import call_command from django.core.management.base import CommandError from django.test.utils import override_settings +from opaque_keys.edx.locator import CourseLocator -from contentstore.tests.utils import CourseTestCase import contentstore.git_export_utils as git_export_utils from contentstore.git_export_utils import GitExportError -from opaque_keys.edx.locator import CourseLocator +from contentstore.tests.utils import CourseTestCase FEATURES_WITH_EXPORT_GIT = settings.FEATURES.copy() FEATURES_WITH_EXPORT_GIT['ENABLE_EXPORT_GIT'] = True @@ -57,29 +60,36 @@ class TestGitExport(CourseTestCase): test output. """ with self.assertRaisesRegexp(CommandError, 'Error: unrecognized arguments:*'): - call_command('git_export', 'blah', 'blah', 'blah', stderr=StringIO.StringIO()) + call_command('git_export', 'blah', 'blah', 'blah', stderr=StringIO()) - with self.assertRaisesMessage(CommandError, 'Error: too few arguments'): - call_command('git_export', stderr=StringIO.StringIO()) + if six.PY2: + with self.assertRaisesMessage(CommandError, 'Error: too few arguments'): + call_command('git_export', stderr=StringIO()) + else: + with self.assertRaisesMessage( + CommandError, + 'Error: the following arguments are required: course_loc, git_url' + ): + call_command('git_export', stderr=StringIO()) # Send bad url to get course not exported - with self.assertRaisesRegexp(CommandError, unicode(GitExportError.URL_BAD)): - call_command('git_export', 'foo/bar/baz', 'silly', stderr=StringIO.StringIO()) + with self.assertRaisesRegexp(CommandError, six.text_type(GitExportError.URL_BAD)): + call_command('git_export', 'foo/bar/baz', 'silly', stderr=StringIO()) # Send bad course_id to get course not exported - with self.assertRaisesRegexp(CommandError, unicode(GitExportError.BAD_COURSE)): - call_command('git_export', 'foo/bar:baz', 'silly', stderr=StringIO.StringIO()) + with self.assertRaisesRegexp(CommandError, six.text_type(GitExportError.BAD_COURSE)): + call_command('git_export', 'foo/bar:baz', 'silly', stderr=StringIO()) def test_error_output(self): """ Verify that error output is actually resolved as the correct string """ - with self.assertRaisesRegexp(CommandError, unicode(GitExportError.BAD_COURSE)): + with self.assertRaisesRegexp(CommandError, six.text_type(GitExportError.BAD_COURSE)): call_command( 'git_export', 'foo/bar:baz', 'silly' ) - with self.assertRaisesRegexp(CommandError, unicode(GitExportError.URL_BAD)): + with self.assertRaisesRegexp(CommandError, six.text_type(GitExportError.URL_BAD)): call_command( 'git_export', 'foo/bar/baz', 'silly' ) @@ -89,14 +99,14 @@ class TestGitExport(CourseTestCase): Test several bad URLs for validation """ course_key = CourseLocator('org', 'course', 'run') - with self.assertRaisesRegexp(GitExportError, unicode(GitExportError.URL_BAD)): + with self.assertRaisesRegexp(GitExportError, six.text_type(GitExportError.URL_BAD)): git_export_utils.export_to_git(course_key, 'Sillyness') - with self.assertRaisesRegexp(GitExportError, unicode(GitExportError.URL_BAD)): + with self.assertRaisesRegexp(GitExportError, six.text_type(GitExportError.URL_BAD)): git_export_utils.export_to_git(course_key, 'example.com:edx/notreal') with self.assertRaisesRegexp(GitExportError, - unicode(GitExportError.URL_NO_AUTH)): + six.text_type(GitExportError.URL_NO_AUTH)): git_export_utils.export_to_git(course_key, 'http://blah') def test_bad_git_repos(self): @@ -108,7 +118,7 @@ class TestGitExport(CourseTestCase): course_key = CourseLocator('foo', 'blah', '100-') # Test bad clones with self.assertRaisesRegexp(GitExportError, - unicode(GitExportError.CANNOT_PULL)): + six.text_type(GitExportError.CANNOT_PULL)): git_export_utils.export_to_git( course_key, 'https://user:blah@example.com/test_repo.git') @@ -116,14 +126,14 @@ class TestGitExport(CourseTestCase): # Setup good repo with bad course to test xml export with self.assertRaisesRegexp(GitExportError, - unicode(GitExportError.XML_EXPORT_FAIL)): + six.text_type(GitExportError.XML_EXPORT_FAIL)): git_export_utils.export_to_git( course_key, 'file://{0}'.format(self.bare_repo_dir)) # Test bad git remote after successful clone with self.assertRaisesRegexp(GitExportError, - unicode(GitExportError.CANNOT_PULL)): + six.text_type(GitExportError.CANNOT_PULL)): git_export_utils.export_to_git( course_key, 'https://user:blah@example.com/r.git') @@ -180,6 +190,6 @@ class TestGitExport(CourseTestCase): ) with self.assertRaisesRegexp(GitExportError, - unicode(GitExportError.CANNOT_COMMIT)): + six.text_type(GitExportError.CANNOT_COMMIT)): git_export_utils.export_to_git( self.course.id, 'file://{0}'.format(self.bare_repo_dir)) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_import.py b/cms/djangoapps/contentstore/management/commands/tests/test_import.py index c5771171f8..9c19a5474c 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_import.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_import.py @@ -2,10 +2,13 @@ Unittests for importing a course via management command """ +from __future__ import absolute_import + import os import shutil import tempfile +import six from django.core.management import call_command from path import Path as path @@ -91,4 +94,4 @@ class TestImport(ModuleStoreTestCase): course = modulestore().get_course(self.base_course_key) # With the bug, this fails because the chapter's course_key is the split mongo form, # while the course's course_key is the old mongo form. - self.assertEqual(unicode(course.location.course_key), unicode(course.children[0].course_key)) + self.assertEqual(six.text_type(course.location.course_key), six.text_type(course.children[0].course_key)) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_migrate_to_split.py b/cms/djangoapps/contentstore/management/commands/tests/test_migrate_to_split.py index 96373b6fd4..9ed7bf52e7 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_migrate_to_split.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_migrate_to_split.py @@ -1,13 +1,18 @@ """ Unittests for migrating a course to split mongo """ +from __future__ import absolute_import + +import six + from django.core.management import CommandError, call_command from django.test import TestCase + from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory class TestArgParsing(TestCase): @@ -21,7 +26,10 @@ class TestArgParsing(TestCase): """ Test the arg length error """ - errstring = "Error: too few arguments" + if six.PY2: + errstring = "Error: too few arguments" + else: + errstring = "Error: the following arguments are required: course_key, email" with self.assertRaisesRegexp(CommandError, errstring): call_command("migrate_to_split") diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py b/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py index 58e81d86e7..d2b3c77d4a 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_migrate_transcripts.py @@ -2,25 +2,27 @@ """ Tests for course transcript migration management command. """ +from __future__ import absolute_import + import itertools import logging from datetime import datetime -import pytz import ddt +import pytz +import six +from django.core.management import CommandError, call_command from django.test import TestCase -from django.core.management import call_command, CommandError +from edxval import api as api from mock import patch +from testfixtures import LogCapture -from openedx.core.djangoapps.video_config.models import ( - TranscriptMigrationSetting, MigrationEnqueuedCourse -) +from openedx.core.djangoapps.video_config.models import MigrationEnqueuedCourse, TranscriptMigrationSetting 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 import VideoBlock 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" @@ -93,7 +95,7 @@ class TestMigrateTranscripts(ModuleStoreTestCase): 'client_video_id': 'test1.mp4', 'duration': 42.0, 'status': 'upload', - 'courses': [unicode(self.course.id)], + 'courses': [six.text_type(self.course.id)], 'encoded_videos': [], 'created': datetime.now(pytz.utc) } @@ -105,9 +107,9 @@ class TestMigrateTranscripts(ModuleStoreTestCase): youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8" show_captions="false" download_track="false" - start_time="00:00:01" + start_time="1.0" download_video="false" - end_time="00:01:00"> + end_time="60.0"> @@ -122,9 +124,9 @@ class TestMigrateTranscripts(ModuleStoreTestCase): youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8" show_captions="false" download_track="false" - start_time="00:00:01" + start_time="1.0" download_video="false" - end_time="00:01:00"> + end_time="60.0"> @@ -133,11 +135,11 @@ class TestMigrateTranscripts(ModuleStoreTestCase): ''' self.video_descriptor = ItemFactory.create( parent_location=self.course.location, category='video', - data={'data': video_sample_xml} + **VideoBlock.parse_video_xml(video_sample_xml) ) self.video_descriptor_2 = ItemFactory.create( parent_location=self.course_2.location, category='video', - data={'data': video_sample_xml_2} + **VideoBlock.parse_video_xml(video_sample_xml_2) ) save_to_store(SRT_FILEDATA, 'subs_grmtran1.srt', 'text/srt', self.video_descriptor.location) @@ -154,7 +156,7 @@ class TestMigrateTranscripts(ModuleStoreTestCase): 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') + call_command('migrate_transcripts', '--course-id', six.text_type(self.course.id), '--commit') languages = api.get_available_transcript_languages(self.video_descriptor.edx_video_id) self.assertEqual(len(languages), 2) @@ -172,7 +174,7 @@ class TestMigrateTranscripts(ModuleStoreTestCase): 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)) + call_command('migrate_transcripts', '--course-id', six.text_type(self.course.id)) # check that transcripts still do not exist languages = api.get_available_transcript_languages(self.video_descriptor.edx_video_id) @@ -185,12 +187,12 @@ class TestMigrateTranscripts(ModuleStoreTestCase): Test migrating transcripts """ translations = self.video_descriptor.available_translations(self.video_descriptor.get_transcripts_info()) - self.assertItemsEqual(translations, ['hr', 'ge']) + six.assertCountEqual(self, 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') + call_command('migrate_transcripts', '--course-id', six.text_type(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')) @@ -200,24 +202,24 @@ class TestMigrateTranscripts(ModuleStoreTestCase): Test migrating transcripts multiple times """ translations = self.video_descriptor.available_translations(self.video_descriptor.get_transcripts_info()) - self.assertItemsEqual(translations, ['hr', 'ge']) + six.assertCountEqual(self, 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') + call_command('migrate_transcripts', '--course-id', six.text_type(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') + call_command('migrate_transcripts', '--course-id', six.text_type(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') + call_command('migrate_transcripts', '--course-id', six.text_type(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')) @@ -226,7 +228,7 @@ class TestMigrateTranscripts(ModuleStoreTestCase): """ Test migrate transcripts logging and output """ - course_id = unicode(self.course.id) + course_id = six.text_type(self.course.id) expected_log = ( ( 'cms.djangoapps.contentstore.tasks', 'INFO', @@ -254,7 +256,7 @@ class TestMigrateTranscripts(ModuleStoreTestCase): ) with LogCapture(LOGGER_NAME, level=logging.INFO) as logger: - call_command('migrate_transcripts', '--course-id', unicode(self.course.id)) + call_command('migrate_transcripts', '--course-id', six.text_type(self.course.id)) logger.check( *expected_log ) @@ -263,7 +265,7 @@ class TestMigrateTranscripts(ModuleStoreTestCase): """ Test migrate transcripts exception logging """ - course_id = unicode(self.course_2.id) + course_id = six.text_type(self.course_2.id) expected_log = ( ( 'cms.djangoapps.contentstore.tasks', 'INFO', @@ -291,7 +293,7 @@ class TestMigrateTranscripts(ModuleStoreTestCase): ) with LogCapture(LOGGER_NAME, level=logging.INFO) as logger: - call_command('migrate_transcripts', '--course-id', unicode(self.course_2.id), '--commit') + call_command('migrate_transcripts', '--course-id', six.text_type(self.course_2.id), '--commit') logger.check( *expected_log ) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py b/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py index ebcc04fe78..339bc5f769 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py @@ -1,17 +1,19 @@ """ Tests for course reindex command """ +from __future__ import absolute_import + import ddt -from django.core.management import call_command, CommandError import mock +from django.core.management import CommandError, call_command +import six from six import text_type +from contentstore.courseware_index import SearchIndexingError +from contentstore.management.commands.reindex_course import Command as ReindexCommand from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory -from contentstore.management.commands.reindex_course import Command as ReindexCommand -from contentstore.courseware_index import SearchIndexingError - @ddt.ddt class TestReindexCourse(ModuleStoreTestCase): @@ -103,7 +105,7 @@ class TestReindexCourse(ModuleStoreTestCase): patched_yes_no.assert_called_once_with(ReindexCommand.CONFIRMATION_PROMPT, default='no') expected_calls = self._build_calls(self.first_course, self.second_course) - self.assertItemsEqual(patched_index.mock_calls, expected_calls) + six.assertCountEqual(self, patched_index.mock_calls, expected_calls) def test_given_all_key_prompts_and_reindexes_all_courses_cancelled(self): """ Test that does not reindex anything when --all key is given and cancelled """ diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_reindex_library.py b/cms/djangoapps/contentstore/management/commands/tests/test_reindex_library.py index 0bb7a58de6..88ce8753d9 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_reindex_library.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_reindex_library.py @@ -1,18 +1,19 @@ """ Tests for library reindex command """ -import ddt -from django.core.management import call_command, CommandError -import mock +from __future__ import absolute_import +import ddt +import mock +import six +from django.core.management import CommandError, call_command +from opaque_keys import InvalidKeyError + +from contentstore.courseware_index import SearchIndexingError +from contentstore.management.commands.reindex_library import Command as ReindexCommand from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory -from opaque_keys import InvalidKeyError - -from contentstore.management.commands.reindex_library import Command as ReindexCommand -from contentstore.courseware_index import SearchIndexingError - @ddt.ddt class TestReindexLibrary(ModuleStoreTestCase): @@ -61,34 +62,34 @@ class TestReindexLibrary(ModuleStoreTestCase): def test_given_course_key_raises_command_error(self): """ Test that raises CommandError if course key is passed """ with self.assertRaisesRegexp(CommandError, ".* is not a library key"): - call_command('reindex_library', unicode(self.first_course.id)) + call_command('reindex_library', six.text_type(self.first_course.id)) with self.assertRaisesRegexp(CommandError, ".* is not a library key"): - call_command('reindex_library', unicode(self.second_course.id)) + call_command('reindex_library', six.text_type(self.second_course.id)) with self.assertRaisesRegexp(CommandError, ".* is not a library key"): call_command( 'reindex_library', - unicode(self.second_course.id), - unicode(self._get_lib_key(self.first_lib)) + six.text_type(self.second_course.id), + six.text_type(self._get_lib_key(self.first_lib)) ) def test_given_id_list_indexes_libraries(self): """ Test that reindexes libraries when given single library key or a list of library keys """ with mock.patch(self.REINDEX_PATH_LOCATION) as patched_index, \ mock.patch(self.MODULESTORE_PATCH_LOCATION, mock.Mock(return_value=self.store)): - call_command('reindex_library', unicode(self._get_lib_key(self.first_lib))) + call_command('reindex_library', six.text_type(self._get_lib_key(self.first_lib))) self.assertEqual(patched_index.mock_calls, self._build_calls(self.first_lib)) patched_index.reset_mock() - call_command('reindex_library', unicode(self._get_lib_key(self.second_lib))) + call_command('reindex_library', six.text_type(self._get_lib_key(self.second_lib))) self.assertEqual(patched_index.mock_calls, self._build_calls(self.second_lib)) patched_index.reset_mock() call_command( 'reindex_library', - unicode(self._get_lib_key(self.first_lib)), - unicode(self._get_lib_key(self.second_lib)) + six.text_type(self._get_lib_key(self.first_lib)), + six.text_type(self._get_lib_key(self.second_lib)) ) expected_calls = self._build_calls(self.first_lib, self.second_lib) self.assertEqual(patched_index.mock_calls, expected_calls) @@ -103,7 +104,7 @@ class TestReindexLibrary(ModuleStoreTestCase): patched_yes_no.assert_called_once_with(ReindexCommand.CONFIRMATION_PROMPT, default='no') expected_calls = self._build_calls(self.first_lib, self.second_lib) - self.assertItemsEqual(patched_index.mock_calls, expected_calls) + six.assertCountEqual(self, patched_index.mock_calls, expected_calls) def test_given_all_key_prompts_and_reindexes_all_libraries_cancelled(self): """ Test that does not reindex anything when --all key is given and cancelled """ @@ -122,4 +123,4 @@ class TestReindexLibrary(ModuleStoreTestCase): patched_index.side_effect = SearchIndexingError("message", []) with self.assertRaises(SearchIndexingError): - call_command('reindex_library', unicode(self._get_lib_key(self.second_lib))) + call_command('reindex_library', six.text_type(self._get_lib_key(self.second_lib))) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_sync_courses.py b/cms/djangoapps/contentstore/management/commands/tests/test_sync_courses.py new file mode 100644 index 0000000000..9a5f6af4dd --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/tests/test_sync_courses.py @@ -0,0 +1,78 @@ +""" +Tests for sync courses management command +""" +import mock + +from django.core.management import call_command +from testfixtures import LogCapture + +from contentstore.views.course import create_new_course_in_store +from opaque_keys.edx.keys import CourseKey +from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from student.tests.factories import UserFactory +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + +COMMAND_MODULE = 'contentstore.management.commands.sync_courses' + + +@mock.patch(COMMAND_MODULE + '.get_course_runs') +class TestSyncCoursesCommand(ModuleStoreTestCase): + """ Test sync_courses command """ + + def setUp(self): + super(TestSyncCoursesCommand, self).setUp() + + self.user = UserFactory(username='test', email='test@example.com') + self.catalog_course_runs = [ + CourseRunFactory(), + CourseRunFactory(), + ] + + def _validate_courses(self): + for run in self.catalog_course_runs: + course_key = CourseKey.from_string(run.get('key')) + self.assertTrue(modulestore().has_course(course_key)) + CourseOverview.objects.get(id=run.get('key')) + + def test_courses_sync(self, mock_catalog_course_runs): + mock_catalog_course_runs.return_value = self.catalog_course_runs + + call_command('sync_courses', self.user.email) + + self._validate_courses() + + def test_duplicate_courses_skipped(self, mock_catalog_course_runs): + mock_catalog_course_runs.return_value = self.catalog_course_runs + initial_display_name = "Test duplicated course" + course_run = self.catalog_course_runs[0] + course_key = CourseKey.from_string(course_run.get('key')) + + create_new_course_in_store( + ModuleStoreEnum.Type.split, + self.user, + course_key.org, + course_key.course, + course_key.run, + { + "display_name": initial_display_name + } + ) + + with LogCapture() as capture: + call_command('sync_courses', self.user.email) + expected_message = u"Course already exists for {}, {}, {}. Skipping".format( + course_key.org, + course_key.course, + course_key.run, + ) + capture.check_present( + (COMMAND_MODULE, 'WARNING', expected_message) + ) + + self._validate_courses() + + course = modulestore().get_course(course_key) + self.assertEqual(course.display_name, initial_display_name) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_video_thumbnails.py b/cms/djangoapps/contentstore/management/commands/tests/test_video_thumbnails.py index a8a184f512..617b3383b2 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_video_thumbnails.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_video_thumbnails.py @@ -2,16 +2,20 @@ """ Tests for course video thumbnails management command. """ +from __future__ import absolute_import + import logging -from mock import patch -from django.core.management import call_command, CommandError + +from django.core.management import CommandError, call_command from django.test import TestCase -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory -from openedx.core.djangoapps.video_config.models import VideoThumbnailSetting +from mock import patch from six import text_type from testfixtures import LogCapture +from openedx.core.djangoapps.video_config.models import VideoThumbnailSetting +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + LOGGER_NAME = "contentstore.management.commands.video_thumbnails" diff --git a/cms/djangoapps/contentstore/management/commands/utils.py b/cms/djangoapps/contentstore/management/commands/utils.py index 0b248ca526..99de627194 100644 --- a/cms/djangoapps/contentstore/management/commands/utils.py +++ b/cms/djangoapps/contentstore/management/commands/utils.py @@ -1,6 +1,8 @@ """ Common methods for cms commands to use """ +from __future__ import absolute_import + from django.contrib.auth.models import User from opaque_keys.edx.keys import CourseKey diff --git a/cms/djangoapps/contentstore/management/commands/video_thumbnails.py b/cms/djangoapps/contentstore/management/commands/video_thumbnails.py index e5cb570783..7bfe8c2611 100644 --- a/cms/djangoapps/contentstore/management/commands/video_thumbnails.py +++ b/cms/djangoapps/contentstore/management/commands/video_thumbnails.py @@ -1,17 +1,19 @@ """ Command to scrape thumbnails and add them to the course-videos. """ +from __future__ import absolute_import + import logging -from six import text_type import edxval.api as edxval_api from django.core.management import BaseCommand from django.core.management.base import CommandError from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from six import text_type -from openedx.core.djangoapps.video_config.models import VideoThumbnailSetting from cms.djangoapps.contentstore.tasks import enqueue_update_thumbnail_tasks +from openedx.core.djangoapps.video_config.models import VideoThumbnailSetting log = logging.getLogger(__name__) diff --git a/cms/djangoapps/contentstore/management/commands/xlint.py b/cms/djangoapps/contentstore/management/commands/xlint.py index 5bcb0b3e2c..09dd5c764d 100644 --- a/cms/djangoapps/contentstore/management/commands/xlint.py +++ b/cms/djangoapps/contentstore/management/commands/xlint.py @@ -1,7 +1,8 @@ """ Verify the structure of courseware as to it's suitability for import """ -from __future__ import print_function +from __future__ import absolute_import, print_function + from argparse import REMAINDER from django.core.management.base import BaseCommand diff --git a/cms/djangoapps/contentstore/migrations/0001_initial.py b/cms/djangoapps/contentstore/migrations/0001_initial.py index ef69433f2a..863e4ef408 100644 --- a/cms/djangoapps/contentstore/migrations/0001_initial.py +++ b/cms/djangoapps/contentstore/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals import django.db.models.deletion from django.conf import settings diff --git a/cms/djangoapps/contentstore/migrations/0002_add_assets_page_flag.py b/cms/djangoapps/contentstore/migrations/0002_add_assets_page_flag.py index aa3fbecc81..8282223c5f 100644 --- a/cms/djangoapps/contentstore/migrations/0002_add_assets_page_flag.py +++ b/cms/djangoapps/contentstore/migrations/0002_add_assets_page_flag.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from django.db import migrations, models -from django.conf import settings import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models from opaque_keys.edx.django.models import CourseKeyField diff --git a/cms/djangoapps/contentstore/migrations/0003_remove_assets_page_flag.py b/cms/djangoapps/contentstore/migrations/0003_remove_assets_page_flag.py index 1131abb39a..29c4627bc9 100644 --- a/cms/djangoapps/contentstore/migrations/0003_remove_assets_page_flag.py +++ b/cms/djangoapps/contentstore/migrations/0003_remove_assets_page_flag.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.db import migrations, models diff --git a/cms/djangoapps/contentstore/migrations/0004_remove_push_notification_configmodel_table.py b/cms/djangoapps/contentstore/migrations/0004_remove_push_notification_configmodel_table.py new file mode 100644 index 0000000000..947d4eac93 --- /dev/null +++ b/cms/djangoapps/contentstore/migrations/0004_remove_push_notification_configmodel_table.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-26 20:12 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contentstore', '0003_remove_assets_page_flag'), + ] + + operations = [ + migrations.RemoveField( + model_name='pushnotificationconfig', + name='changed_by', + ), + migrations.DeleteModel( + name='PushNotificationConfig', + ), + ] diff --git a/cms/djangoapps/contentstore/models.py b/cms/djangoapps/contentstore/models.py index fa69d45ec7..9ab2e4ad1b 100644 --- a/cms/djangoapps/contentstore/models.py +++ b/cms/djangoapps/contentstore/models.py @@ -2,6 +2,8 @@ Models for contentstore """ +from __future__ import absolute_import + from config_models.models import ConfigurationModel from django.db.models.fields import TextField @@ -21,11 +23,3 @@ class VideoUploadConfig(ConfigurationModel): def get_profile_whitelist(cls): """Get the list of profiles to include in the encoding download""" return [profile for profile in cls.current().profile_whitelist.split(",") if profile] - - -class PushNotificationConfig(ConfigurationModel): - """ - Configuration for mobile push notifications. - - .. no_pii: - """ diff --git a/cms/djangoapps/contentstore/proctoring.py b/cms/djangoapps/contentstore/proctoring.py index b5bc115b10..474180d21b 100644 --- a/cms/djangoapps/contentstore/proctoring.py +++ b/cms/djangoapps/contentstore/proctoring.py @@ -2,8 +2,11 @@ Code related to the handling of Proctored Exams in Studio """ +from __future__ import absolute_import + import logging +import six from django.conf import settings from edx_proctoring.api import ( create_exam, @@ -36,7 +39,7 @@ def register_special_exams(course_key): course = modulestore().get_course(course_key) if course is None: - raise ItemNotFoundError(u"Course {} does not exist", unicode(course_key)) + raise ItemNotFoundError(u"Course {} does not exist", six.text_type(course_key)) if not course.enable_proctored_exams and not course.enable_timed_exams: # likewise if course does not have these features turned on @@ -66,7 +69,7 @@ def register_special_exams(course_key): for timed_exam in timed_exams: msg = ( u'Found {location} as a timed-exam in course structure. Inspecting...'.format( - location=unicode(timed_exam.location) + location=six.text_type(timed_exam.location) ) ) log.info(msg) @@ -74,7 +77,7 @@ def register_special_exams(course_key): exam_metadata = { 'exam_name': timed_exam.display_name, 'time_limit_mins': timed_exam.default_time_limit_minutes, - 'due_date': timed_exam.due, + 'due_date': timed_exam.due if not course.self_paced else None, 'is_proctored': timed_exam.is_proctored_exam, # backends that support onboarding exams will treat onboarding exams as practice 'is_practice_exam': timed_exam.is_practice_exam or timed_exam.is_onboarding_exam, @@ -84,7 +87,7 @@ def register_special_exams(course_key): } try: - exam = get_exam_by_content_id(unicode(course_key), unicode(timed_exam.location)) + exam = get_exam_by_content_id(six.text_type(course_key), six.text_type(timed_exam.location)) # update case, make sure everything is synced exam_metadata['exam_id'] = exam['id'] @@ -93,8 +96,8 @@ def register_special_exams(course_key): log.info(msg) except ProctoredExamNotFoundException: - exam_metadata['course_id'] = unicode(course_key) - exam_metadata['content_id'] = unicode(timed_exam.location) + exam_metadata['course_id'] = six.text_type(course_key) + exam_metadata['content_id'] = six.text_type(timed_exam.location) exam_id = create_exam(**exam_metadata) msg = u'Created new timed exam {exam_id}'.format(exam_id=exam_id) @@ -132,7 +135,7 @@ def register_special_exams(course_key): search = [ timed_exam for timed_exam in timed_exams if - unicode(timed_exam.location) == exam['content_id'] + six.text_type(timed_exam.location) == exam['content_id'] ] if not search: # This means it was turned off in Studio, we need to mark diff --git a/cms/djangoapps/contentstore/push_notification.py b/cms/djangoapps/contentstore/push_notification.py deleted file mode 100644 index e7ee741c44..0000000000 --- a/cms/djangoapps/contentstore/push_notification.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Helper methods for push notifications from Studio. -""" - -from logging import exception as log_exception -from uuid import uuid4 - -from django.conf import settings -from six import text_type - -from contentstore.models import PushNotificationConfig -from contentstore.tasks import push_course_update_task -from parse_rest.connection import register -from parse_rest.core import ParseError -from parse_rest.installation import Push -from xmodule.modulestore.django import modulestore - - -def push_notification_enabled(): - """ - Returns whether the push notification feature is enabled. - """ - return PushNotificationConfig.is_enabled() - - -def enqueue_push_course_update(update, course_key): - """ - Enqueues a task for push notification for the given update for the given course if - (1) the feature is enabled and - (2) push_notification is selected for the update - """ - if push_notification_enabled() and update.get("push_notification_selected"): - course = modulestore().get_course(course_key) - if course: - push_course_update_task.delay( - unicode(course_key), - course.clean_id(padding_char='_'), - course.display_name - ) - - -def send_push_course_update(course_key_string, course_subscription_id, course_display_name): - """ - Sends a push notification for a course update, given the course's subscription_id and display_name. - """ - if settings.PARSE_KEYS: - try: - register( - settings.PARSE_KEYS["APPLICATION_ID"], - settings.PARSE_KEYS["REST_API_KEY"], - ) - push_payload = { - "action": "course.announcement", - "notification-id": unicode(uuid4()), - - "course-id": course_key_string, - "course-name": course_display_name, - } - push_channels = [course_subscription_id] - - # Push to all Android devices - Push.alert( - data=push_payload, - channels={"$in": push_channels}, - where={"deviceType": "android"}, - ) - - # Push to all iOS devices - # With additional payload so that - # 1. The push is displayed automatically - # 2. The app gets it even in the background. - # See http://stackoverflow.com/questions/19239737/silent-push-notification-in-ios-7-does-not-work - push_payload.update({ - "alert": "", - "content-available": 1 - }) - Push.alert( - data=push_payload, - channels={"$in": push_channels}, - where={"deviceType": "ios"}, - ) - - except ParseError as error: - log_exception(text_type(error)) diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 8f41582da0..fbf088da65 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -1,9 +1,12 @@ """ receivers of course_published and library_updated events in order to trigger indexing task """ +from __future__ import absolute_import + +import logging from datetime import datetime from functools import wraps -import logging +import six from django.core.cache import cache from django.dispatch import receiver from pytz import UTC @@ -62,7 +65,7 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded from contentstore.tasks import update_search_index - update_search_index.delay(unicode(course_key), datetime.now(UTC).isoformat()) + update_search_index.delay(six.text_type(course_key), datetime.now(UTC).isoformat()) @receiver(SignalHandler.library_updated) @@ -75,7 +78,7 @@ def listen_for_library_update(sender, library_key, **kwargs): # pylint: disable # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded from contentstore.tasks import update_library_index - update_library_index.delay(unicode(library_key), datetime.now(UTC).isoformat()) + update_library_index.delay(six.text_type(library_key), datetime.now(UTC).isoformat()) @receiver(SignalHandler.item_deleted) @@ -113,10 +116,10 @@ def handle_grading_policy_changed(sender, **kwargs): Receives signal and kicks off celery task to recalculate grades """ kwargs = { - 'course_key': unicode(kwargs.get('course_key')), - 'grading_policy_hash': unicode(kwargs.get('grading_policy_hash')), - 'event_transaction_id': unicode(get_event_transaction_id()), - 'event_transaction_type': unicode(get_event_transaction_type()), + 'course_key': six.text_type(kwargs.get('course_key')), + 'grading_policy_hash': six.text_type(kwargs.get('grading_policy_hash')), + 'event_transaction_id': six.text_type(get_event_transaction_id()), + 'event_transaction_type': six.text_type(get_event_transaction_type()), } result = task_compute_all_grades_for_course.apply_async(kwargs=kwargs, countdown=GRADING_POLICY_COUNTDOWN_SECONDS) log.info(u"Grades: Created {task_name}[{task_id}] with arguments {kwargs}".format( diff --git a/cms/djangoapps/contentstore/signals/signals.py b/cms/djangoapps/contentstore/signals/signals.py index f73b13b9ea..660f608183 100644 --- a/cms/djangoapps/contentstore/signals/signals.py +++ b/cms/djangoapps/contentstore/signals/signals.py @@ -1,6 +1,8 @@ """ Contentstore signals """ +from __future__ import absolute_import + from django.dispatch import Signal # Signal that indicates that a course grading policy has been updated. diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index f715d4e36b..2e247e1042 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -33,6 +33,7 @@ from organizations.models import OrganizationCourse from path import Path as path from pytz import UTC from six import iteritems, text_type +from six.moves import range from user_tasks.models import UserTaskArtifact, UserTaskStatus from user_tasks.tasks import UserTask @@ -45,6 +46,7 @@ from models.settings.course_metadata import CourseMetadata from openedx.core.djangoapps.embargo.models import CountryAccessRule, RestrictedCourse from openedx.core.lib.extract_tar import safetar_extractall from student.auth import has_course_author_access +from util.organizations_helpers import add_organization_course, get_organization_by_short_name from xmodule.contentstore.django import contentstore from xmodule.course_module import CourseFields from xmodule.exceptions import SerializationError @@ -101,7 +103,7 @@ def enqueue_update_thumbnail_tasks(course_videos, videos_per_task, run): start = 0 end = videos_per_task chunks_count = int(ceil(batch_size / float(videos_per_task))) - for __ in xrange(0, chunks_count): + for __ in range(0, chunks_count): # pylint: disable=C7620 course_videos_chunk = course_videos[start:end] tasks.append(task_scrape_youtube_thumbnail.s( course_videos_chunk, run @@ -197,7 +199,7 @@ def enqueue_async_migrate_transcripts_tasks(course_keys, 'command_run': command_run } group([ - async_migrate_transcript.s(unicode(course_key), **kwargs) + async_migrate_transcript.s(text_type(course_key), **kwargs) for course_key in course_keys ])() @@ -264,7 +266,7 @@ def async_migrate_transcript(self, course_key, **kwargs): # pylint: disable=un all_transcripts.update({'en': video.sub}) sub_tasks = [] - video_location = unicode(video.location) + video_location = text_type(video.location) for lang in all_transcripts: sub_tasks.append(async_migrate_transcript_subtask.s( video_location, revision, lang, force_update, **kwargs @@ -484,6 +486,8 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i for country_access_rule in country_access_rules: clone_instance(country_access_rule, {'restricted_course': new_restricted_course}) + org_data = get_organization_by_short_name(source_course_key.org) + add_organization_course(org_data, destination_course_key) return "succeeded" except DuplicateCourseError: @@ -550,16 +554,6 @@ def update_library_index(library_id, triggered_time_isoformat): LOGGER.debug(u'Search indexing successful for library %s', library_id) -@task() -def push_course_update_task(course_key_string, course_subscription_id, course_display_name): - """ - Sends a push notification for a course update. - """ - # TODO Use edx-notifications library instead (MA-638). - from .push_notification import send_push_course_update - send_push_course_update(course_key_string, course_subscription_id, course_display_name) - - class CourseExportTask(UserTask): # pylint: disable=abstract-method """ Base class for course and library export tasks. diff --git a/cms/djangoapps/contentstore/tests/test_clone_course.py b/cms/djangoapps/contentstore/tests/test_clone_course.py index 9ef484c91b..92cfded0ab 100644 --- a/cms/djangoapps/contentstore/tests/test_clone_course.py +++ b/cms/djangoapps/contentstore/tests/test_clone_course.py @@ -1,8 +1,11 @@ """ Unit tests for cloning a course between the same and different module stores. """ +from __future__ import absolute_import + import json +import six from django.conf import settings from mock import Mock, patch from opaque_keys.edx.locator import CourseLocator @@ -82,8 +85,8 @@ class CloneCourseTest(CourseTestCase): split_rerun_id = CourseLocator(org=org, course=course_number, run="2012_Q2") CourseRerunState.objects.initiated(course.id, split_rerun_id, self.user, fields['display_name']) result = rerun_course.delay( - unicode(course.id), - unicode(split_rerun_id), + six.text_type(course.id), + six.text_type(split_rerun_id), self.user.id, json.dumps(fields, cls=EdxJSONEncoder) ) @@ -106,7 +109,7 @@ class CloneCourseTest(CourseTestCase): # Mark the action as initiated fields = {'display_name': 'rerun'} CourseRerunState.objects.initiated(mongo_course1_id, split_course3_id, self.user, fields['display_name']) - result = rerun_course.delay(unicode(mongo_course1_id), unicode(split_course3_id), self.user.id, + result = rerun_course.delay(six.text_type(mongo_course1_id), six.text_type(split_course3_id), self.user.id, json.dumps(fields, cls=EdxJSONEncoder)) self.assertEqual(result.get(), "succeeded") self.assertTrue(has_course_author_access(self.user, split_course3_id), "Didn't grant access") @@ -114,7 +117,7 @@ class CloneCourseTest(CourseTestCase): self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.SUCCEEDED) # try creating rerunning again to same name and ensure it generates error - result = rerun_course.delay(unicode(mongo_course1_id), unicode(split_course3_id), self.user.id) + result = rerun_course.delay(six.text_type(mongo_course1_id), six.text_type(split_course3_id), self.user.id) self.assertEqual(result.get(), "duplicate course") # the below will raise an exception if the record doesn't exist CourseRerunState.objects.find_first( @@ -127,7 +130,7 @@ class CloneCourseTest(CourseTestCase): split_course4_id = CourseLocator(org="edx3", course="split3", run="rerun_fail") fields = {'display_name': 'total failure'} CourseRerunState.objects.initiated(split_course3_id, split_course4_id, self.user, fields['display_name']) - result = rerun_course.delay(unicode(split_course3_id), unicode(split_course4_id), self.user.id, + result = rerun_course.delay(six.text_type(split_course3_id), six.text_type(split_course4_id), self.user.id, json.dumps(fields, cls=EdxJSONEncoder)) self.assertIn("exception: ", result.get()) self.assertIsNone(self.store.get_course(split_course4_id), "Didn't delete course after error") diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 5b48c6fd9e..fb60589b53 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from __future__ import print_function +from __future__ import absolute_import, print_function import copy import shutil @@ -14,6 +14,7 @@ from uuid import uuid4 import ddt import lxml.html import mock +import six from django.conf import settings from django.contrib.auth.models import User from django.middleware.csrf import _compare_salted_tokens @@ -27,12 +28,13 @@ from opaque_keys.edx.keys import AssetKey, CourseKey, UsageKey from opaque_keys.edx.locations import CourseLocator from path import Path as path from six import text_type +from six.moves import range from waffle.testutils import override_switch +from contentstore.config import waffle from contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase, get_url, parse_json from contentstore.utils import delete_course, reverse_course_url, reverse_url from contentstore.views.component import ADVANCED_COMPONENT_TYPES -from contentstore.config import waffle from course_action_state.managers import CourseActionStateItemNotFoundError from course_action_state.models import CourseRerunState, CourseRerunUIStateManager from openedx.core.djangoapps.django_comment_common.utils import are_permissions_roles_seeded @@ -54,6 +56,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, chec from xmodule.modulestore.xml_exporter import export_course_to_xml from xmodule.modulestore.xml_importer import import_course_from_xml, perform_xlint from xmodule.seq_module import SequenceDescriptor +from xmodule.video_module import VideoBlock TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex @@ -676,7 +679,7 @@ class MiscCourseTests(ContentStoreTestCase): self.assertEqual(resp.status_code, 200) for expected in expected_types: - self.assertIn(expected, resp.content) + self.assertIn(expected, resp.content.decode('utf-8')) @ddt.data("", "alert('hi')", "") def test_container_handler_xss_prevent(self, malicious_code): @@ -1815,15 +1818,15 @@ class MetadataSaveTestCase(ContentStoreTestCase): """ self.video_descriptor = ItemFactory.create( parent_location=course.location, category='video', - data={'data': video_sample_xml} + **VideoBlock.parse_video_xml(video_sample_xml) ) def test_metadata_not_persistence(self): @@ -1840,7 +1843,6 @@ class MetadataSaveTestCase(ContentStoreTestCase): 'youtube_id_1_5', 'start_time', 'end_time', - 'source', 'html5_sources', 'track' } @@ -1930,7 +1932,7 @@ class RerunCourseTest(ContentStoreTestCase): 'course_key': destination_course_key, 'should_display': True, } - for field_name, expected_value in expected_states.iteritems(): + for field_name, expected_value in six.iteritems(expected_states): self.assertEquals(getattr(rerun_state, field_name), expected_value) # Verify that the creator is now enrolled in the course. diff --git a/cms/djangoapps/contentstore/tests/test_core_caching.py b/cms/djangoapps/contentstore/tests/test_core_caching.py index 6c606d596c..2421b8e2a0 100644 --- a/cms/djangoapps/contentstore/tests/test_core_caching.py +++ b/cms/djangoapps/contentstore/tests/test_core_caching.py @@ -2,6 +2,8 @@ Tests core caching facilities. """ +from __future__ import absolute_import + from django.test import TestCase from opaque_keys.edx.locator import AssetLocator, CourseLocator diff --git a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py index da2c19affa..d7189c016c 100644 --- a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py +++ b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py @@ -1,11 +1,14 @@ """ Test view handler for rerun (and eventually create) """ +from __future__ import absolute_import + import datetime import ddt -from django.urls import reverse +import six from django.test.client import RequestFactory +from django.urls import reverse from mock import patch from opaque_keys.edx.keys import CourseKey @@ -63,12 +66,18 @@ class TestCourseListing(ModuleStoreTestCase): self.client.logout() ModuleStoreTestCase.tearDown(self) + @patch.dict('django.conf.settings.FEATURES', {'ORGANIZATIONS_APP': True}) def test_rerun(self): """ Just testing the functionality the view handler adds over the tasks tested in test_clone_course """ + add_organization({ + 'name': 'Test Organization', + 'short_name': self.source_course_key.org, + 'description': 'Testing Organization Description', + }) response = self.client.ajax_post(self.course_create_rerun_url, { - 'source_course_key': unicode(self.source_course_key), + 'source_course_key': six.text_type(self.source_course_key), 'org': self.source_course_key.org, 'course': self.source_course_key.course, 'run': 'copy', 'display_name': 'not the same old name', }) @@ -83,6 +92,9 @@ class TestCourseListing(ModuleStoreTestCase): self.assertEqual(dest_course.end, source_course.end) self.assertEqual(dest_course.enrollment_start, None) self.assertEqual(dest_course.enrollment_end, None) + course_orgs = get_course_organizations(dest_course_key) + self.assertEqual(len(course_orgs), 1) + self.assertEqual(course_orgs[0]['short_name'], self.source_course_key.org) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_newly_created_course_has_web_certs_enabled(self, store): diff --git a/cms/djangoapps/contentstore/tests/test_course_listing.py b/cms/djangoapps/contentstore/tests/test_course_listing.py index 8cc49506fa..99dd9e92fe 100644 --- a/cms/djangoapps/contentstore/tests/test_course_listing.py +++ b/cms/djangoapps/contentstore/tests/test_course_listing.py @@ -2,6 +2,8 @@ Unit tests for getting the list of courses for a user through iterating all courses and by reversing group name formats. """ +from __future__ import absolute_import + import random import ddt @@ -10,6 +12,7 @@ from django.conf import settings from django.test import RequestFactory from mock import Mock, patch from opaque_keys.edx.locations import CourseLocator +from six.moves import range from contentstore.tests.utils import AjaxEnabledTestClient from contentstore.utils import delete_course @@ -172,7 +175,7 @@ class TestCourseListing(ModuleStoreTestCase): with self.store.default_store(default_store): # Create few courses - for num in xrange(TOTAL_COURSES_COUNT): + for num in range(TOTAL_COURSES_COUNT): course_location = self.store.make_course_key('Org', 'CreatedCourse' + str(num), 'Run') self._create_course_with_access_groups(course_location, self.user, default_store) @@ -249,7 +252,7 @@ class TestCourseListing(ModuleStoreTestCase): reversing django groups """ # create list of random course numbers which will be accessible to the user - user_course_ids = random.sample(range(TOTAL_COURSES_COUNT), USER_COURSES_COUNT) + user_course_ids = random.sample(list(range(TOTAL_COURSES_COUNT)), USER_COURSES_COUNT) # create courses and assign those to the user which have their number in user_course_ids with self.store.default_store(store): diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 3a1372fd33..e0a7bc5df6 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -1,26 +1,28 @@ """ Tests for Studio Course Settings. """ +from __future__ import absolute_import + import copy import datetime import json import unittest import ddt +import mock +import six +from crum import set_current_request from django.conf import settings from django.test import RequestFactory from django.test.utils import override_settings -from pytz import UTC -import mock -from mock import Mock, patch -from crum import set_current_request -from milestones.tests.utils import MilestonesTestCaseMixin - - -from contentstore.utils import reverse_course_url, reverse_usage_url -from contentstore.config.waffle import ENABLE_PROCTORING_PROVIDER_OVERRIDES from milestones.models import MilestoneRelationshipType -from models.settings.course_grading import CourseGradingModel, GRADING_POLICY_CHANGED_EVENT_TYPE, hash_grading_policy +from milestones.tests.utils import MilestonesTestCaseMixin +from mock import Mock, patch +from pytz import UTC + +from contentstore.config.waffle import ENABLE_PROCTORING_PROVIDER_OVERRIDES +from contentstore.utils import reverse_course_url, reverse_usage_url +from models.settings.course_grading import GRADING_POLICY_CHANGED_EVENT_TYPE, CourseGradingModel, hash_grading_policy from models.settings.course_metadata import CourseMetadata from models.settings.encoder import CourseSettingsEncoder from openedx.core.djangoapps.models.course_details import CourseDetails @@ -106,7 +108,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start) payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end) resp = self.client.ajax_post(url, payload) - self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val)) + self.compare_details_with_encoding(json.loads(resp.content.decode('utf-8')), details.__dict__, field + str(val)) MilestoneRelationshipType.objects.get_or_create(name='requires') MilestoneRelationshipType.objects.get_or_create(name='fulfills') @@ -124,7 +126,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): # resp s/b json from here on url = get_url(self.course.id) resp = self.client.get_json(url) - self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get") + self.compare_details_with_encoding(json.loads(resp.content.decode('utf-8')), details.__dict__, "virgin get") self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 12, 1, 30, tzinfo=UTC)) self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 1, 13, 30, tzinfo=UTC)) @@ -190,20 +192,20 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): url = get_url(self.course.id) resp = self.client.get_json(url) - course_detail_json = json.loads(resp.content) + course_detail_json = json.loads(resp.content.decode('utf-8')) # assert pre_requisite_courses is initialized self.assertEqual([], course_detail_json['pre_requisite_courses']) # update pre requisite courses with a new course keys pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run') pre_requisite_course2 = CourseFactory.create(org='edX', course='902', run='test_run') - pre_requisite_course_keys = [unicode(pre_requisite_course.id), unicode(pre_requisite_course2.id)] + pre_requisite_course_keys = [six.text_type(pre_requisite_course.id), six.text_type(pre_requisite_course2.id)] course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys self.client.ajax_post(url, course_detail_json) # fetch updated course to assert pre_requisite_courses has new values resp = self.client.get_json(url) - course_detail_json = json.loads(resp.content) + course_detail_json = json.loads(resp.content.decode('utf-8')) self.assertEqual(pre_requisite_course_keys, course_detail_json['pre_requisite_courses']) self.assertTrue(milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user.id), @@ -213,7 +215,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): course_detail_json['pre_requisite_courses'] = [] self.client.ajax_post(url, course_detail_json) resp = self.client.get_json(url) - course_detail_json = json.loads(resp.content) + course_detail_json = json.loads(resp.content.decode('utf-8')) self.assertEqual([], course_detail_json['pre_requisite_courses']) self.assertFalse(milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user.id), @@ -223,11 +225,11 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): def test_invalid_pre_requisite_course(self): url = get_url(self.course.id) resp = self.client.get_json(url) - course_detail_json = json.loads(resp.content) + course_detail_json = json.loads(resp.content.decode('utf-8')) # update pre requisite courses one valid and one invalid key pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run') - pre_requisite_course_keys = [unicode(pre_requisite_course.id), 'invalid_key'] + pre_requisite_course_keys = [six.text_type(pre_requisite_course.id), 'invalid_key'] course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys response = self.client.ajax_post(url, course_detail_json) self.assertEqual(400, response.status_code) @@ -252,7 +254,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): resp = self.client.get_html(course_details_url) self.assertEqual( feature_flags[2], - '

' in resp.content + b'

' in resp.content ) @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) @@ -513,10 +515,10 @@ class CourseGradingTest(CourseTestCase): mock.call( GRADING_POLICY_CHANGED_EVENT_TYPE, { - 'course_id': unicode(self.course.id), + 'course_id': six.text_type(self.course.id), 'event_transaction_type': 'edx.grades.grading_policy_changed', 'grading_policy_hash': policy_hash, - 'user_id': unicode(self.user.id), + 'user_id': six.text_type(self.user.id), 'event_transaction_id': 'mockUUID', } ) for policy_hash in ( @@ -562,10 +564,10 @@ class CourseGradingTest(CourseTestCase): mock.call( GRADING_POLICY_CHANGED_EVENT_TYPE, { - 'course_id': unicode(self.course.id), + 'course_id': six.text_type(self.course.id), 'event_transaction_type': 'edx.grades.grading_policy_changed', 'grading_policy_hash': policy_hash, - 'user_id': unicode(self.user.id), + 'user_id': six.text_type(self.user.id), 'event_transaction_id': 'mockUUID', } ) for policy_hash in {grading_policy_1, grading_policy_2, grading_policy_3} @@ -600,10 +602,10 @@ class CourseGradingTest(CourseTestCase): mock.call( GRADING_POLICY_CHANGED_EVENT_TYPE, { - 'course_id': unicode(self.course.id), + 'course_id': six.text_type(self.course.id), 'event_transaction_type': 'edx.grades.grading_policy_changed', 'grading_policy_hash': policy_hash, - 'user_id': unicode(self.user.id), + 'user_id': six.text_type(self.user.id), 'event_transaction_id': 'mockUUID', } ) for policy_hash in (grading_policy_1, grading_policy_2, grading_policy_3) @@ -677,10 +679,10 @@ class CourseGradingTest(CourseTestCase): mock.call( GRADING_POLICY_CHANGED_EVENT_TYPE, { - 'course_id': unicode(self.course.id), + 'course_id': six.text_type(self.course.id), 'event_transaction_type': 'edx.grades.grading_policy_changed', 'grading_policy_hash': policy_hash, - 'user_id': unicode(self.user.id), + 'user_id': six.text_type(self.user.id), 'event_transaction_id': 'mockUUID', } ) for policy_hash in (grading_policy_1, grading_policy_2) @@ -688,7 +690,7 @@ class CourseGradingTest(CourseTestCase): def _model_from_url(self, url_base): response = self.client.get_json(url_base) - return json.loads(response.content) + return json.loads(response.content.decode('utf-8')) def test_get_set_grader_types_ajax(self): """ @@ -733,7 +735,7 @@ class CourseGradingTest(CourseTestCase): ) grading_policy_hash1 = self._grading_policy_hash_for_course() self.assertEqual(200, response.status_code) - grader_sample = json.loads(response.content) + grader_sample = json.loads(response.content.decode('utf-8')) new_grader['id'] = len(original_model['graders']) self.assertEqual(new_grader, grader_sample) @@ -773,12 +775,12 @@ class CourseGradingTest(CourseTestCase): response = self.client.ajax_post(grade_type_url, {'graderType': u'Homework'}) self.assertEqual(200, response.status_code) response = self.client.get_json(grade_type_url + '?fields=graderType') - self.assertEqual(json.loads(response.content).get('graderType'), u'Homework') + self.assertEqual(json.loads(response.content.decode('utf-8')).get('graderType'), u'Homework') # and unset response = self.client.ajax_post(grade_type_url, {'graderType': u'notgraded'}) self.assertEqual(200, response.status_code) response = self.client.get_json(grade_type_url + '?fields=graderType') - self.assertEqual(json.loads(response.content).get('graderType'), u'notgraded') + self.assertEqual(json.loads(response.content.decode('utf-8')).get('graderType'), u'notgraded') def _grading_policy_hash_for_course(self): return hash_grading_policy(modulestore().get_course(self.course.id).grading_policy) @@ -1090,12 +1092,12 @@ class CourseMetadataEditingTest(CourseTestCase): def test_http_fetch_initial_fields(self): response = self.client.get_json(self.course_setting_url) - test_model = json.loads(response.content) + test_model = json.loads(response.content.decode('utf-8')) self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertEqual(test_model['display_name']['value'], self.course.display_name) response = self.client.get_json(self.fullcourse_setting_url) - test_model = json.loads(response.content) + test_model = json.loads(response.content.decode('utf-8')) self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in') self.assertIn('display_name', test_model, 'full missing editable metadata field') self.assertEqual(test_model['display_name']['value'], self.fullcourse.display_name) @@ -1108,18 +1110,18 @@ class CourseMetadataEditingTest(CourseTestCase): "advertised_start": {"value": "start A"}, "days_early_for_beta": {"value": 2}, }) - test_model = json.loads(response.content) + test_model = json.loads(response.content.decode('utf-8')) self.update_check(test_model) response = self.client.get_json(self.course_setting_url) - test_model = json.loads(response.content) + test_model = json.loads(response.content.decode('utf-8')) self.update_check(test_model) # now change some of the existing metadata response = self.client.ajax_post(self.course_setting_url, { "advertised_start": {"value": "start B"}, "display_name": {"value": "jolly roger"} }) - test_model = json.loads(response.content) + test_model = json.loads(response.content.decode('utf-8')) self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertEqual(test_model['display_name']['value'], 'jolly roger', "not expected value") self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field') @@ -1159,7 +1161,7 @@ class CourseMetadataEditingTest(CourseTestCase): 'model': {'display_name': 'Tabs Exception'} } ] - self.assertEqual(json.loads(resp.content), error_msg) + self.assertEqual(json.loads(resp.content.decode('utf-8')), error_msg) # verify that the course wasn't saved into the modulestore course = modulestore().get_course(self.course.id) @@ -1399,7 +1401,7 @@ class CourseGraderUpdatesTest(CourseTestCase): """Test getting a specific grading type record.""" resp = self.client.get_json(self.url + '/0') self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content) + obj = json.loads(resp.content.decode('utf-8')) self.assertEqual(self.starting_graders[0], obj) def test_delete(self): @@ -1422,7 +1424,7 @@ class CourseGraderUpdatesTest(CourseTestCase): } resp = self.client.ajax_post(self.url + '/0', grader) self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content) + obj = json.loads(resp.content.decode('utf-8')) self.assertEqual(obj, grader) current_graders = CourseGradingModel.fetch(self.course.id).graders self.assertEqual(len(self.starting_graders), len(current_graders)) @@ -1441,7 +1443,7 @@ class CourseGraderUpdatesTest(CourseTestCase): } resp = self.client.ajax_post('{}/{}'.format(self.url, len(self.starting_graders) + 1), grader) self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content) + obj = json.loads(resp.content.decode('utf-8')) self.assertEqual(obj['id'], len(self.starting_graders)) del obj['id'] self.assertEqual(obj, grader) @@ -1491,7 +1493,7 @@ id=\"course-enrollment-end-time\" value=\"\" placeholder=\"HH:MM\" autocomplete= """ super(CourseEnrollmentEndFieldTest, self).setUp() self.course = CourseFactory.create(org='edX', number='dummy', display_name='Marketing Site Course') - self.course_details_url = reverse_course_url('settings_handler', unicode(self.course.id)) + self.course_details_url = reverse_course_url('settings_handler', six.text_type(self.course.id)) def _get_course_details_response(self, global_staff): """ diff --git a/cms/djangoapps/contentstore/tests/test_courseware_index.py b/cms/djangoapps/contentstore/tests/test_courseware_index.py index 880f160bc9..89f0357868 100644 --- a/cms/djangoapps/contentstore/tests/test_courseware_index.py +++ b/cms/djangoapps/contentstore/tests/test_courseware_index.py @@ -1,7 +1,8 @@ """ Testing indexing of the courseware as it is changed """ -from __future__ import print_function +from __future__ import absolute_import, print_function + import json import time from datetime import datetime @@ -10,11 +11,13 @@ from uuid import uuid4 import ddt import pytest +import six from django.conf import settings from lazy.lazy import lazy from mock import patch from pytz import UTC from search.search_engine_base import SearchEngine +from six.moves import range from contentstore.courseware_index import ( CourseAboutSearchIndexer, @@ -259,7 +262,7 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase): ) def _get_default_search(self): - return {"course": unicode(self.course.id)} + return {"course": six.text_type(self.course.id)} def _test_indexing_course(self, store): """ indexing course tests """ @@ -383,10 +386,10 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase): results = response["results"] date_map = { - unicode(self.chapter.location): early_date, - unicode(self.sequential.location): early_date, - unicode(self.vertical.location): later_date, - unicode(self.html_unit.location): later_date, + six.text_type(self.chapter.location): early_date, + six.text_type(self.sequential.location): early_date, + six.text_type(self.vertical.location): later_date, + six.text_type(self.html_unit.location): later_date, } for result in results: self.assertEqual(result["data"]["start_date"], date_map[result["data"]["id"]]) @@ -449,7 +452,7 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase): self.reindex_course(store) response = self.searcher.search( doc_type=CourseAboutSearchIndexer.DISCOVERY_DOCUMENT_TYPE, - field_dictionary={"course": unicode(self.course.id)} + field_dictionary={"course": six.text_type(self.course.id)} ) self.assertEqual(response["total"], 1) self.assertEqual(response["results"][0]["data"]["content"]["display_name"], display_name) @@ -463,7 +466,7 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase): self.reindex_course(store) response = self.searcher.search( doc_type=CourseAboutSearchIndexer.DISCOVERY_DOCUMENT_TYPE, - field_dictionary={"course": unicode(self.course.id)} + field_dictionary={"course": six.text_type(self.course.id)} ) self.assertEqual(response["total"], 1) self.assertEqual(response["results"][0]["data"]["content"]["short_description"], short_description) @@ -471,13 +474,13 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase): def _test_course_about_mode_index(self, store): """ Test that informational properties in the course modes store end up in the course_info index """ honour_mode = CourseMode( - course_id=unicode(self.course.id), + course_id=six.text_type(self.course.id), mode_slug=CourseMode.HONOR, mode_display_name=CourseMode.HONOR ) honour_mode.save() verified_mode = CourseMode( - course_id=unicode(self.course.id), + course_id=six.text_type(self.course.id), mode_slug=CourseMode.VERIFIED, mode_display_name=CourseMode.VERIFIED, min_price=1 @@ -487,7 +490,7 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase): response = self.searcher.search( doc_type=CourseAboutSearchIndexer.DISCOVERY_DOCUMENT_TYPE, - field_dictionary={"course": unicode(self.course.id)} + field_dictionary={"course": six.text_type(self.course.id)} ) self.assertEqual(response["total"], 1) self.assertIn(CourseMode.HONOR, response["results"][0]["data"]["modes"]) @@ -638,13 +641,13 @@ class TestLargeCourseDeletions(MixedWithOptionsTestCase): """ Test that deleting items from a course works even when present within a very large course """ def id_list(top_parent_object): """ private function to get ids from object down the tree """ - list_of_ids = [unicode(top_parent_object.location)] + list_of_ids = [six.text_type(top_parent_object.location)] for child in top_parent_object.get_children(): list_of_ids.extend(id_list(child)) return list_of_ids course, course_size = create_large_course(store, load_factor) - self.course_id = unicode(course.id) + self.course_id = six.text_type(course.id) # index full course CoursewareSearchIndexer.do_course_reindex(store, course.id) @@ -753,7 +756,7 @@ class TestTaskExecution(SharedModuleStoreTestCase): searcher = SearchEngine.get_search_engine(CoursewareSearchIndexer.INDEX_NAME) response = searcher.search( doc_type=CoursewareSearchIndexer.DOCUMENT_TYPE, - field_dictionary={"course": unicode(self.course.id)} + field_dictionary={"course": six.text_type(self.course.id)} ) self.assertEqual(response["total"], 0) @@ -762,14 +765,14 @@ class TestTaskExecution(SharedModuleStoreTestCase): # Note that this test will only succeed if celery is working in inline mode response = searcher.search( doc_type=CoursewareSearchIndexer.DOCUMENT_TYPE, - field_dictionary={"course": unicode(self.course.id)} + field_dictionary={"course": six.text_type(self.course.id)} ) self.assertEqual(response["total"], 3) def test_task_library_update(self): """ Making sure that the receiver correctly fires off the task when invoked by signal """ searcher = SearchEngine.get_search_engine(LibrarySearchIndexer.INDEX_NAME) - library_search_key = unicode(normalize_key_for_search(self.library.location.library_key)) + library_search_key = six.text_type(normalize_key_for_search(self.library.location.library_key)) response = searcher.search(field_dictionary={"library": library_search_key}) self.assertEqual(response["total"], 0) @@ -821,7 +824,7 @@ class TestLibrarySearchIndexer(MixedWithOptionsTestCase): def _get_default_search(self): """ Returns field_dictionary for default search """ - return {"library": unicode(self.library.location.library_key.replace(version_guid=None, branch=None))} + return {"library": six.text_type(self.library.location.library_key.replace(version_guid=None, branch=None))} def reindex_library(self, store): """ kick off complete reindex of the course """ @@ -1177,9 +1180,9 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): """ return { 'course_name': self.course.display_name, - 'id': unicode(html_unit.location), + 'id': six.text_type(html_unit.location), 'content': {'html_content': '', 'display_name': html_unit.display_name}, - 'course': unicode(self.course.id), + 'course': six.text_type(self.course.id), 'location': [ self.chapter.display_name, self.sequential.display_name, @@ -1197,9 +1200,9 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): """ return { 'course_name': self.course.display_name, - 'id': unicode(html_unit.location), + 'id': six.text_type(html_unit.location), 'content': {'html_content': '', 'display_name': html_unit.display_name}, - 'course': unicode(self.course.id), + 'course': six.text_type(self.course.id), 'location': [ self.chapter.display_name, self.sequential2.display_name, @@ -1218,7 +1221,7 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): return { 'start_date': datetime(2015, 4, 1, 0, 0, tzinfo=UTC), 'content': {'display_name': vertical.display_name}, - 'course': unicode(self.course.id), + 'course': six.text_type(self.course.id), 'location': [ self.chapter.display_name, self.sequential2.display_name, @@ -1226,7 +1229,7 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): ], 'content_type': 'Sequence', 'content_groups': content_groups, - 'id': unicode(vertical.location), + 'id': six.text_type(vertical.location), 'course_name': self.course.display_name, 'org': self.course.org } @@ -1237,9 +1240,9 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): """ return { 'course_name': self.course.display_name, - 'id': unicode(html_unit.location), + 'id': six.text_type(html_unit.location), 'content': {'html_content': '', 'display_name': html_unit.display_name}, - 'course': unicode(self.course.id), + 'course': six.text_type(self.course.id), 'location': [ self.chapter.display_name, self.sequential.display_name, @@ -1269,7 +1272,7 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): # Only published modules should be in the index added_to_index = self.reindex_course(self.store) self.assertEqual(added_to_index, 16) - response = self.searcher.search(field_dictionary={"course": unicode(self.course.id)}) + response = self.searcher.search(field_dictionary={"course": six.text_type(self.course.id)}) self.assertEqual(response["total"], 17) group_access_content = {'group_access': {666: [1]}} @@ -1287,44 +1290,44 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): self.assertTrue(mock_index.called) indexed_content = self._get_index_values_from_call_args(mock_index) self.assertIn(self._html_group_result(self.html_unit1, [1]), indexed_content) - self.assertIn(self._html_experiment_group_result(self.html_unit4, [unicode(2)]), indexed_content) - self.assertIn(self._html_experiment_group_result(self.html_unit5, [unicode(3)]), indexed_content) - self.assertIn(self._html_experiment_group_result(self.html_unit6, [unicode(4)]), indexed_content) - self.assertNotIn(self._html_experiment_group_result(self.html_unit6, [unicode(5)]), indexed_content) + self.assertIn(self._html_experiment_group_result(self.html_unit4, [six.text_type(2)]), indexed_content) + self.assertIn(self._html_experiment_group_result(self.html_unit5, [six.text_type(3)]), indexed_content) + self.assertIn(self._html_experiment_group_result(self.html_unit6, [six.text_type(4)]), indexed_content) + self.assertNotIn(self._html_experiment_group_result(self.html_unit6, [six.text_type(5)]), indexed_content) self.assertIn( - self._vertical_experiment_group_result(self.condition_0_vertical, [unicode(2)]), + self._vertical_experiment_group_result(self.condition_0_vertical, [six.text_type(2)]), indexed_content ) self.assertNotIn( - self._vertical_experiment_group_result(self.condition_1_vertical, [unicode(2)]), + self._vertical_experiment_group_result(self.condition_1_vertical, [six.text_type(2)]), indexed_content ) self.assertNotIn( - self._vertical_experiment_group_result(self.condition_2_vertical, [unicode(2)]), + self._vertical_experiment_group_result(self.condition_2_vertical, [six.text_type(2)]), indexed_content ) self.assertNotIn( - self._vertical_experiment_group_result(self.condition_0_vertical, [unicode(3)]), + self._vertical_experiment_group_result(self.condition_0_vertical, [six.text_type(3)]), indexed_content ) self.assertIn( - self._vertical_experiment_group_result(self.condition_1_vertical, [unicode(3)]), + self._vertical_experiment_group_result(self.condition_1_vertical, [six.text_type(3)]), indexed_content ) self.assertNotIn( - self._vertical_experiment_group_result(self.condition_2_vertical, [unicode(3)]), + self._vertical_experiment_group_result(self.condition_2_vertical, [six.text_type(3)]), indexed_content ) self.assertNotIn( - self._vertical_experiment_group_result(self.condition_0_vertical, [unicode(4)]), + self._vertical_experiment_group_result(self.condition_0_vertical, [six.text_type(4)]), indexed_content ) self.assertNotIn( - self._vertical_experiment_group_result(self.condition_1_vertical, [unicode(4)]), + self._vertical_experiment_group_result(self.condition_1_vertical, [six.text_type(4)]), indexed_content ) self.assertIn( - self._vertical_experiment_group_result(self.condition_2_vertical, [unicode(4)]), + self._vertical_experiment_group_result(self.condition_2_vertical, [six.text_type(4)]), indexed_content ) mock_index.reset_mock() diff --git a/cms/djangoapps/contentstore/tests/test_crud.py b/cms/djangoapps/contentstore/tests/test_crud.py index 700c3a6c4c..86db5dd30c 100644 --- a/cms/djangoapps/contentstore/tests/test_crud.py +++ b/cms/djangoapps/contentstore/tests/test_crud.py @@ -1,7 +1,11 @@ +"""Tests for CRUD Operations""" + +from __future__ import absolute_import + from xmodule import templates from xmodule.capa_module import ProblemBlock from xmodule.course_module import CourseDescriptor -from xmodule.html_module import HtmlDescriptor +from xmodule.html_module import HtmlBlock from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.exceptions import DuplicateCourseError from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase @@ -40,10 +44,10 @@ class TemplateTests(ModuleStoreTestCase): def test_get_some_templates(self): self.assertEqual(len(SequenceDescriptor.templates()), 0) - self.assertGreater(len(HtmlDescriptor.templates()), 0) + self.assertGreater(len(HtmlBlock.templates()), 0) self.assertIsNone(SequenceDescriptor.get_template('doesntexist.yaml')) - self.assertIsNone(HtmlDescriptor.get_template('doesntexist.yaml')) - self.assertIsNotNone(HtmlDescriptor.get_template('announcement.yaml')) + self.assertIsNone(HtmlBlock.get_template('doesntexist.yaml')) + self.assertIsNotNone(HtmlBlock.get_template('announcement.yaml')) def test_factories(self): test_course = CourseFactory.create( diff --git a/cms/djangoapps/contentstore/tests/test_export_git.py b/cms/djangoapps/contentstore/tests/test_export_git.py index 29d9276c45..27ae16da16 100644 --- a/cms/djangoapps/contentstore/tests/test_export_git.py +++ b/cms/djangoapps/contentstore/tests/test_export_git.py @@ -2,6 +2,8 @@ Test the ability to export courses to xml from studio """ +from __future__ import absolute_import + import copy import os import shutil diff --git a/cms/djangoapps/contentstore/tests/test_gating.py b/cms/djangoapps/contentstore/tests/test_gating.py index e61da3ace5..e39f78564b 100644 --- a/cms/djangoapps/contentstore/tests/test_gating.py +++ b/cms/djangoapps/contentstore/tests/test_gating.py @@ -1,6 +1,8 @@ """ Unit tests for the gating feature in Studio """ +from __future__ import absolute_import + from milestones.tests.utils import MilestonesTestCaseMixin from mock import patch diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py index 45034e3839..e083a33c37 100644 --- a/cms/djangoapps/contentstore/tests/test_i18n.py +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -2,6 +2,8 @@ """ Tests for validate Internationalization and Module i18n service. """ +from __future__ import absolute_import + import gettext from unittest import skip diff --git a/cms/djangoapps/contentstore/tests/test_import.py b/cms/djangoapps/contentstore/tests/test_import.py index de79fda8f2..1aeab0618c 100644 --- a/cms/djangoapps/contentstore/tests/test_import.py +++ b/cms/djangoapps/contentstore/tests/test_import.py @@ -3,12 +3,13 @@ """ Tests for import_course_from_xml using the mongo modulestore. """ -from __future__ import print_function +from __future__ import absolute_import, print_function import copy from uuid import uuid4 import ddt +import six from django.conf import settings from django.test.client import Client from django.test.utils import override_settings @@ -268,7 +269,7 @@ class ContentStoreImportTest(ModuleStoreTestCase): self.assertIsNotNone(split_test_module) remapped_verticals = { - key: target_id.make_usage_key('vertical', value) for key, value in groups_to_verticals.iteritems() + key: target_id.make_usage_key('vertical', value) for key, value in six.iteritems(groups_to_verticals) } self.assertEqual(remapped_verticals, split_test_module.group_id_to_child) diff --git a/cms/djangoapps/contentstore/tests/test_import_draft_order.py b/cms/djangoapps/contentstore/tests/test_import_draft_order.py index 68d6488cf9..c81016898f 100644 --- a/cms/djangoapps/contentstore/tests/test_import_draft_order.py +++ b/cms/djangoapps/contentstore/tests/test_import_draft_order.py @@ -1,6 +1,8 @@ """ Tests Draft import order. """ +from __future__ import absolute_import + from django.conf import settings from xmodule.modulestore.django import modulestore diff --git a/cms/djangoapps/contentstore/tests/test_import_pure_xblock.py b/cms/djangoapps/contentstore/tests/test_import_pure_xblock.py index 05d2079fd3..72a1ba661c 100644 --- a/cms/djangoapps/contentstore/tests/test_import_pure_xblock.py +++ b/cms/djangoapps/contentstore/tests/test_import_pure_xblock.py @@ -2,6 +2,8 @@ Integration tests for importing courses containing pure XBlocks. """ +from __future__ import absolute_import + from django.conf import settings from xblock.core import XBlock from xblock.fields import String @@ -31,6 +33,7 @@ class StubXBlock(XBlock): class XBlockImportTest(ModuleStoreTestCase): + """Test class to verify xblock import operations""" @XBlock.register_temp_plugin(StubXBlock) def test_import_public(self): diff --git a/cms/djangoapps/contentstore/tests/test_libraries.py b/cms/djangoapps/contentstore/tests/test_libraries.py index 17ce84d88a..5030deccbf 100644 --- a/cms/djangoapps/contentstore/tests/test_libraries.py +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -1,10 +1,14 @@ """ Content library unit tests that require the CMS runtime. """ +from __future__ import absolute_import + import ddt +import six from django.test.utils import override_settings from mock import Mock, patch from opaque_keys.edx.locator import CourseKey, LibraryLocator +from six.moves import range from contentstore.tests.utils import AjaxEnabledTestClient, parse_json from contentstore.utils import reverse_library_url, reverse_url, reverse_usage_url @@ -81,7 +85,7 @@ class LibraryTestCase(ModuleStoreTestCase): parent_location=course.location, user_id=self.user.id, publish_item=publish_item, - source_library_id=unicode(library_key), + source_library_id=six.text_type(library_key), **(other_settings or {}) ) @@ -511,11 +515,11 @@ class TestLibraryAccess(LibraryTestCase): `library` can be a LibraryLocator or the library's root XBlock """ - if isinstance(library, (basestring, LibraryLocator)): + if isinstance(library, (six.string_types, LibraryLocator)): lib_key = library else: lib_key = library.location.library_key - response = self.client.get(reverse_library_url('library_handler', unicode(lib_key))) + response = self.client.get(reverse_library_url('library_handler', six.text_type(lib_key))) self.assertIn(response.status_code, (200, 302, 403)) return response.status_code == 200 @@ -579,7 +583,7 @@ class TestLibraryAccess(LibraryTestCase): # Now non_staff_user should be able to access library2_key only: lib_list = self._list_libraries() self.assertEqual(len(lib_list), 1) - self.assertEqual(lib_list[0]["library_key"], unicode(library2_key)) + self.assertEqual(lib_list[0]["library_key"], six.text_type(library2_key)) self.assertTrue(self._can_access_library(library2_key)) self.assertFalse(self._can_access_library(self.library)) @@ -606,7 +610,7 @@ class TestLibraryAccess(LibraryTestCase): # Now non_staff_user should be able to access lib_key_pacific only: lib_list = self._list_libraries() self.assertEqual(len(lib_list), 1) - self.assertEqual(lib_list[0]["library_key"], unicode(lib_key_pacific)) + self.assertEqual(lib_list[0]["library_key"], six.text_type(lib_key_pacific)) self.assertTrue(self._can_access_library(lib_key_pacific)) self.assertFalse(self._can_access_library(lib_key_atlantic)) self.assertFalse(self._can_access_library(self.lib_key)) @@ -646,8 +650,8 @@ class TestLibraryAccess(LibraryTestCase): def can_copy_block(): """ Check if studio lets us duplicate the XBlock in the library """ response = self.client.ajax_post(reverse_url('xblock_handler'), { - 'parent_locator': unicode(self.library.location), - 'duplicate_source_locator': unicode(block.location), + 'parent_locator': six.text_type(self.library.location), + 'duplicate_source_locator': six.text_type(block.location), }) self.assertIn(response.status_code, (200, 403)) # 400 would be ambiguous return response.status_code == 200 @@ -655,7 +659,7 @@ class TestLibraryAccess(LibraryTestCase): def can_create_block(): """ Check if studio lets us make a new XBlock in the library """ response = self.client.ajax_post(reverse_url('xblock_handler'), { - 'parent_locator': unicode(self.library.location), 'category': 'html', + 'parent_locator': six.text_type(self.library.location), 'category': 'html', }) self.assertIn(response.status_code, (200, 403)) # 400 would be ambiguous return response.status_code == 200 @@ -708,8 +712,8 @@ class TestLibraryAccess(LibraryTestCase): # Copy block to the course: response = self.client.ajax_post(reverse_url('xblock_handler'), { - 'parent_locator': unicode(course.location), - 'duplicate_source_locator': unicode(block.location), + 'parent_locator': six.text_type(course.location), + 'duplicate_source_locator': six.text_type(block.location), }) self.assertIn(response.status_code, (200, 403)) # 400 would be ambiguous duplicate_action_allowed = (response.status_code == 200) diff --git a/cms/djangoapps/contentstore/tests/test_orphan.py b/cms/djangoapps/contentstore/tests/test_orphan.py index f6919d8bdd..2e73911451 100644 --- a/cms/djangoapps/contentstore/tests/test_orphan.py +++ b/cms/djangoapps/contentstore/tests/test_orphan.py @@ -1,9 +1,12 @@ """ Test finding orphans via the view and django config """ +from __future__ import absolute_import + import json import ddt +import six from opaque_keys.edx.locator import BlockUsageLocator from contentstore.tests.utils import CourseTestCase @@ -94,11 +97,11 @@ class TestOrphan(TestOrphanBase): ) self.assertEqual(len(orphans), 3, u"Wrong # {}".format(orphans)) location = course.location.replace(category='chapter', name='OrphanChapter') - self.assertIn(unicode(location), orphans) + self.assertIn(six.text_type(location), orphans) location = course.location.replace(category='vertical', name='OrphanVert') - self.assertIn(unicode(location), orphans) + self.assertIn(six.text_type(location), orphans) location = course.location.replace(category='html', name='OrphanHtml') - self.assertIn(unicode(location), orphans) + self.assertIn(six.text_type(location), orphans) @ddt.data( (ModuleStoreEnum.Type.split, 9, 5), @@ -171,8 +174,8 @@ class TestOrphan(TestOrphanBase): # HTML component has `vertical1` as its parent. html_parent = self.store.get_parent_location(multi_parent_html.location) - self.assertNotEqual(unicode(html_parent), unicode(orphan_vertical.location)) - self.assertEqual(unicode(html_parent), unicode(vertical1.location)) + self.assertNotEqual(six.text_type(html_parent), six.text_type(orphan_vertical.location)) + self.assertEqual(six.text_type(html_parent), six.text_type(vertical1.location)) # Get path of the `multi_parent_html` & verify path_to_location returns a expected path path = path_to_location(self.store, multi_parent_html.location) @@ -224,7 +227,7 @@ class TestOrphan(TestOrphanBase): # Verify chapter1 is parent of vertical1. vertical1_parent = self.store.get_parent_location(vertical1.location) - self.assertEqual(unicode(vertical1_parent), unicode(chapter1.location)) + self.assertEqual(six.text_type(vertical1_parent), six.text_type(chapter1.location)) # Make `Vertical1` the parent of `HTML0`. So `HTML0` will have to parents (`Vertical0` & `Vertical1`) vertical1.children.append(html.location) @@ -233,7 +236,7 @@ class TestOrphan(TestOrphanBase): # Get parent location & verify its either of the two verticals. As both parents are non-orphan, # alphabetically least is returned html_parent = self.store.get_parent_location(html.location) - self.assertEquals(unicode(html_parent), unicode(vertical1.location)) + self.assertEquals(six.text_type(html_parent), six.text_type(vertical1.location)) # verify path_to_location returns a expected path path = path_to_location(self.store, html.location) diff --git a/cms/djangoapps/contentstore/tests/test_permissions.py b/cms/djangoapps/contentstore/tests/test_permissions.py index 1f43a20532..30f7a61242 100644 --- a/cms/djangoapps/contentstore/tests/test_permissions.py +++ b/cms/djangoapps/contentstore/tests/test_permissions.py @@ -1,9 +1,12 @@ """ Test CRUD for authorization. """ +from __future__ import absolute_import + import copy from django.contrib.auth.models import User +from six.moves import range from contentstore.tests.utils import AjaxEnabledTestClient from contentstore.utils import reverse_course_url, reverse_url diff --git a/cms/djangoapps/contentstore/tests/test_proctoring.py b/cms/djangoapps/contentstore/tests/test_proctoring.py index ef2ad670c4..e842259a50 100644 --- a/cms/djangoapps/contentstore/tests/test_proctoring.py +++ b/cms/djangoapps/contentstore/tests/test_proctoring.py @@ -2,15 +2,18 @@ Tests for the edx_proctoring integration into Studio """ +from __future__ import absolute_import + from datetime import datetime, timedelta import ddt +import six +from django.conf import settings from edx_proctoring.api import get_all_exams_for_course, get_review_policy_by_exam_id from mock import patch from pytz import UTC from contentstore.signals.handlers import listen_for_course_publish -from django.conf import settings from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -41,7 +44,7 @@ class TestProctoredExams(ModuleStoreTestCase): Helper method to compare the sequence with the stored exam, which should just be a single one """ - exams = get_all_exams_for_course(unicode(self.course.id)) + exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), 1) @@ -56,8 +59,8 @@ class TestProctoredExams(ModuleStoreTestCase): # the hide after due value only applies to timed exams self.assertEqual(exam['hide_after_due'], sequence.hide_after_due) - self.assertEqual(exam['course_id'], unicode(self.course.id)) - self.assertEqual(exam['content_id'], unicode(sequence.location)) + self.assertEqual(exam['course_id'], six.text_type(self.course.id)) + self.assertEqual(exam['content_id'], six.text_type(sequence.location)) self.assertEqual(exam['exam_name'], sequence.display_name) self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes) self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam) @@ -164,7 +167,7 @@ class TestProctoredExams(ModuleStoreTestCase): listen_for_course_publish(self, self.course.id) - exams = get_all_exams_for_course(unicode(self.course.id)) + exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), 1) sequence.is_time_limited = False @@ -195,7 +198,7 @@ class TestProctoredExams(ModuleStoreTestCase): listen_for_course_publish(self, self.course.id) - exams = get_all_exams_for_course(unicode(self.course.id)) + exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), 1) self.store.delete_item(chapter.location, self.user.id) @@ -205,7 +208,7 @@ class TestProctoredExams(ModuleStoreTestCase): # look through exam table, the dangling exam # should be disabled - exams = get_all_exams_for_course(unicode(self.course.id)) + exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), 1) exam = exams[0] @@ -230,7 +233,7 @@ class TestProctoredExams(ModuleStoreTestCase): listen_for_course_publish(self, self.course.id) - exams = get_all_exams_for_course(unicode(self.course.id)) + exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), 0) @ddt.data( @@ -269,5 +272,42 @@ class TestProctoredExams(ModuleStoreTestCase): # there shouldn't be any exams because we haven't enabled that # advanced setting flag - exams = get_all_exams_for_course(unicode(self.course.id)) + exams = get_all_exams_for_course(six.text_type(self.course.id)) self.assertEqual(len(exams), expected_count) + + def test_self_paced_no_due_dates(self): + self.course = CourseFactory.create( + org='edX', + course='901', + run='test_run2', + enable_proctored_exams=True, + enable_timed_exams=True, + self_paced=True, + ) + chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') + ItemFactory.create( + parent=chapter, + category='sequential', + display_name='Test Proctored Exam', + graded=True, + is_time_limited=True, + default_time_limit_minutes=60, + is_proctored_exam=False, + is_practice_exam=False, + due=datetime.now(UTC) + timedelta(minutes=60), + exam_review_rules="allow_use_of_paper", + hide_after_due=True, + is_onboarding_exam=False, + ) + listen_for_course_publish(self, self.course.id) + exams = get_all_exams_for_course(six.text_type(self.course.id)) + # self-paced courses should ignore due dates + assert exams[0]['due_date'] is None + + # now switch to instructor paced + # the exam will be updated with a due date + self.course.self_paced = False + self.course = self.update_course(self.course, 1) + listen_for_course_publish(self, self.course.id) + exams = get_all_exams_for_course(six.text_type(self.course.id)) + assert exams[0]['due_date'] is not None diff --git a/cms/djangoapps/contentstore/tests/test_request_event.py b/cms/djangoapps/contentstore/tests/test_request_event.py index bf8821ef4a..a9b84d14d9 100644 --- a/cms/djangoapps/contentstore/tests/test_request_event.py +++ b/cms/djangoapps/contentstore/tests/test_request_event.py @@ -1,7 +1,10 @@ """Tests for CMS's requests to logs""" +from __future__ import absolute_import + import mock -from django.urls import reverse from django.test import TestCase +from django.urls import reverse +from six import unichr # pylint: disable=W0622 from contentstore.views.helpers import event as cms_user_track diff --git a/cms/djangoapps/contentstore/tests/test_signals.py b/cms/djangoapps/contentstore/tests/test_signals.py index 7126fb9ea6..c9c1f724a1 100644 --- a/cms/djangoapps/contentstore/tests/test_signals.py +++ b/cms/djangoapps/contentstore/tests/test_signals.py @@ -1,10 +1,12 @@ -import ddt -from mock import patch, Mock +"""Tests for verifying availability of resources for locking""" -from cms.djangoapps.contentstore.signals.handlers import ( - GRADING_POLICY_COUNTDOWN_SECONDS, - handle_grading_policy_changed -) +from __future__ import absolute_import + +import ddt +import six +from mock import Mock, patch + +from cms.djangoapps.contentstore.signals.handlers import GRADING_POLICY_COUNTDOWN_SECONDS, handle_grading_policy_changed from student.models import CourseEnrollment from student.tests.factories import UserFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -13,6 +15,8 @@ from xmodule.modulestore.tests.factories import CourseFactory @ddt.ddt class LockedTest(ModuleStoreTestCase): + """Test class to verify locking of mocked resources""" + def setUp(self): super(LockedTest, self).setUp() self.course = CourseFactory.create( @@ -24,16 +28,15 @@ class LockedTest(ModuleStoreTestCase): CourseEnrollment.enroll(self.user, self.course.id) @patch('cms.djangoapps.contentstore.signals.handlers.cache.add') - @patch('cms.djangoapps.contentstore.signals.handlers.cache.delete') @patch('cms.djangoapps.contentstore.signals.handlers.task_compute_all_grades_for_course.apply_async') @ddt.data(True, False) - def test_locked(self, lock_available, compute_grades_async_mock, delete_mock, add_mock): + def test_locked(self, lock_available, compute_grades_async_mock, add_mock): add_mock.return_value = lock_available sender = Mock() - handle_grading_policy_changed(sender, course_key=unicode(self.course.id)) + handle_grading_policy_changed(sender, course_key=six.text_type(self.course.id)) - cache_key = 'handle_grading_policy_changed-{}'.format(unicode(self.course.id)) + cache_key = 'handle_grading_policy_changed-{}'.format(six.text_type(self.course.id)) self.assertEqual(lock_available, compute_grades_async_mock.called) if lock_available: add_mock.assert_called_once_with(cache_key, "true", GRADING_POLICY_COUNTDOWN_SECONDS) diff --git a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py index c1323126be..bba9984e76 100644 --- a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py +++ b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- """ Tests for transcripts_utils. """ +from __future__ import absolute_import + import copy import json import tempfile diff --git a/cms/djangoapps/contentstore/tests/test_users_default_role.py b/cms/djangoapps/contentstore/tests/test_users_default_role.py index 32648657ca..bcadfde520 100644 --- a/cms/djangoapps/contentstore/tests/test_users_default_role.py +++ b/cms/djangoapps/contentstore/tests/test_users_default_role.py @@ -2,6 +2,8 @@ Unit tests for checking default forum role "Student" of a user when he creates a course or after deleting it creates same course again """ +from __future__ import absolute_import + from contentstore.tests.utils import AjaxEnabledTestClient from contentstore.utils import delete_course, reverse_url from courseware.tests.factories import UserFactory diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index ef769339ad..487ab0afec 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -1,7 +1,10 @@ """ Tests for utils. """ +from __future__ import absolute_import + import collections from datetime import datetime, timedelta +import six from django.test import TestCase from opaque_keys.edx.locator import CourseLocator from pytz import UTC @@ -24,19 +27,22 @@ class LMSLinksTestCase(TestCase): course_key = CourseLocator('mitX', '101', 'test') location = course_key.make_usage_key('vertical', 'contacting_us') link = utils.get_lms_link_for_item(location, False) - self.assertEquals(link, "//localhost:8000/courses/course-v1:mitX+101+test/jump_to/block-v1:mitX+101+test+type@vertical+block@contacting_us") + self.assertEquals(link, "//localhost:8000/courses/course-v1:mitX+101+test/jump_to/block-v1:mitX+101+test+type" + "@vertical+block@contacting_us") # test preview link = utils.get_lms_link_for_item(location, True) self.assertEquals( link, - "//preview.localhost/courses/course-v1:mitX+101+test/jump_to/block-v1:mitX+101+test+type@vertical+block@contacting_us" + "//preview.localhost/courses/course-v1:mitX+101+test/jump_to/block-v1:mitX+101+test+type@vertical+block" + "@contacting_us " ) # now test with the course' location location = course_key.make_usage_key('course', 'test') link = utils.get_lms_link_for_item(location) - self.assertEquals(link, "//localhost:8000/courses/course-v1:mitX+101+test/jump_to/block-v1:mitX+101+test+type@course+block@test") + self.assertEquals(link, "//localhost:8000/courses/course-v1:mitX+101+test/jump_to/block-v1:mitX+101+test+type" + "@course+block@test") def lms_link_for_certificate_web_view_test(self): """ Tests get_lms_link_for_certificate_web_view. """ @@ -79,7 +85,7 @@ class ExtraPanelTabTestCase(TestCase): if tabs is None: tabs = [] course = collections.namedtuple('MockCourse', ['tabs']) - if isinstance(tabs, basestring): + if isinstance(tabs, six.string_types): course.tabs = self.get_tab_type_dicts(tabs) else: course.tabs = tabs diff --git a/cms/djangoapps/contentstore/tests/test_video_utils.py b/cms/djangoapps/contentstore/tests/test_video_utils.py index 0783f0fd2f..76ef0e49aa 100644 --- a/cms/djangoapps/contentstore/tests/test_video_utils.py +++ b/cms/djangoapps/contentstore/tests/test_video_utils.py @@ -3,31 +3,29 @@ Unit tests for video utils. """ -from unittest import TestCase +from __future__ import absolute_import + from datetime import datetime +from unittest import TestCase + import ddt import pytz import requests +import six from django.conf import settings from django.core.files.uploadedfile import UploadedFile from django.test.utils import override_settings -from edxval.api import ( - create_profile, - create_video, - get_course_video_image_url, - update_video_image -) -from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file - +from edxval.api import create_profile, create_video, get_course_video_image_url, update_video_image from mock import patch from contentstore.tests.utils import CourseTestCase from contentstore.video_utils import ( + YOUTUBE_THUMBNAIL_SIZES, download_youtube_video_thumbnail, scrape_youtube_thumbnail, - validate_video_image, - YOUTUBE_THUMBNAIL_SIZES + validate_video_image ) +from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file class ValidateVideoImageTestCase(TestCase): @@ -63,7 +61,7 @@ class ScrapeVideoThumbnailsTestCase(CourseTestCase): def setUp(self): super(ScrapeVideoThumbnailsTestCase, self).setUp() - course_ids = [unicode(self.course.id)] + course_ids = [six.text_type(self.course.id)] profiles = ['youtube'] created = datetime.now(pytz.utc) previous_uploads = [ @@ -119,7 +117,7 @@ class ScrapeVideoThumbnailsTestCase(CourseTestCase): # Create video images. with make_image_file() as image_file: update_video_image( - 'test-youtube-video-2', unicode(self.course.id), image_file, 'image.jpg' + 'test-youtube-video-2', six.text_type(self.course.id), image_file, 'image.jpg' ) def mocked_youtube_thumbnail_response( @@ -235,7 +233,7 @@ class ScrapeVideoThumbnailsTestCase(CourseTestCase): """ Test that youtube thumbnails are correctly scrapped. """ - course_id = unicode(self.course.id) + course_id = six.text_type(self.course.id) video1_edx_video_id = 'test-youtube-video-1' video2_edx_video_id = 'test-youtube-video-2' @@ -289,7 +287,7 @@ class ScrapeVideoThumbnailsTestCase(CourseTestCase): """ Test that we get correct logs in case of failure as well as success. """ - course_id = unicode(self.course.id) + course_id = six.text_type(self.course.id) video1_edx_video_id = 'test-youtube-video-1' mocked_request.side_effect = [ self.mocked_youtube_thumbnail_response( @@ -324,14 +322,14 @@ class ScrapeVideoThumbnailsTestCase(CourseTestCase): 'dummy-content', None, u'This image file type is not supported. Supported file types are {supported_file_formats}.'.format( - supported_file_formats=settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys() + supported_file_formats=list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys()) ) ), ( None, None, u'This image file type is not supported. Supported file types are {supported_file_formats}.'.format( - supported_file_formats=settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys() + supported_file_formats=list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys()) ) ), ) @@ -350,7 +348,7 @@ class ScrapeVideoThumbnailsTestCase(CourseTestCase): Test that when no thumbnail is downloaded, video image is not updated. """ mock_download_youtube_thumbnail.return_value = image_content, image_content_type - course_id = unicode(self.course.id) + course_id = six.text_type(self.course.id) video1_edx_video_id = 'test-youtube-video-1' # Verify that video1 has no image attached. diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index c33a60ff38..128672b93b 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -1,31 +1,32 @@ """ This test file will test registration, login, activation, and session activity timeouts """ -from __future__ import print_function +from __future__ import absolute_import, print_function + import datetime import time import mock import pytest +from contentstore.tests.test_course_settings import CourseTestCase +from contentstore.tests.utils import AjaxEnabledTestClient, parse_json, registration, user from ddt import data, ddt, unpack from django.conf import settings from django.contrib.auth.models import User from django.core.cache import cache -from django.urls import reverse from django.test import TestCase from django.test.utils import override_settings +from django.urls import reverse from freezegun import freeze_time from pytz import UTC -from six.moves import xrange - -from contentstore.models import PushNotificationConfig -from contentstore.tests.test_course_settings import CourseTestCase -from contentstore.tests.utils import AjaxEnabledTestClient, parse_json, registration, user +from six.moves import range from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory class ContentStoreTestCase(ModuleStoreTestCase): + """Test class to verify user account operations""" + def _login(self, email, password): """ Login. View should always return 200. The success/fail is in the @@ -62,8 +63,8 @@ class ContentStoreTestCase(ModuleStoreTestCase): """Create the account and check that it worked""" resp = self._create_account(username, email, password) self.assertEqual(resp.status_code, 200) - data = parse_json(resp) - self.assertEqual(data['success'], True) + json_data = parse_json(resp) + self.assertEqual(json_data['success'], True) # Check both that the user is created, and inactive self.assertFalse(user(email).is_active) @@ -173,7 +174,7 @@ class AuthTestCase(ContentStoreTestCase): self.create_account(self.username, self.email, self.pw) # Not activated yet. Login should fail. - resp = self._login(self.email, self.pw) + self._login(self.email, self.pw) self.activate_user(self.email) @@ -183,7 +184,7 @@ class AuthTestCase(ContentStoreTestCase): def test_login_ratelimited(self): # try logging in 30 times, the default limit in the number of failed # login attempts in one 5 minute period before the rate gets limited - for i in xrange(30): + for i in range(30): resp = self._login(self.email, 'wrong_password{0}'.format(i)) self.assertEqual(resp.status_code, 403) resp = self._login(self.email, 'wrong_password') @@ -200,7 +201,7 @@ class AuthTestCase(ContentStoreTestCase): self.create_account(self.username, self.email, self.pw) self.activate_user(self.email) - for i in xrange(3): + for i in range(3): resp = self._login(self.email, 'wrong_password{0}'.format(i)) self.assertEqual(resp.status_code, 403) self.assertIn( @@ -341,6 +342,8 @@ class AuthTestCase(ContentStoreTestCase): class ForumTestCase(CourseTestCase): + """Tests class to verify course to forum operations""" + def setUp(self): """ Creates the test course. """ super(ForumTestCase, self).setUp() @@ -388,6 +391,8 @@ class ForumTestCase(CourseTestCase): @ddt class CourseKeyVerificationTestCase(CourseTestCase): + """Test class to verify course decorator operations""" + def setUp(self): """ Create test course. @@ -411,15 +416,3 @@ class CourseKeyVerificationTestCase(CourseTestCase): ) resp = self.client.get_html(url) self.assertEqual(resp.status_code, status_code) - - -class PushNotificationConfigTestCase(TestCase): - """ - Tests PushNotificationConfig. - """ - def test_notifications_defaults(self): - self.assertFalse(PushNotificationConfig.is_enabled()) - - def test_notifications_enabled(self): - PushNotificationConfig(enabled=True).save() - self.assertTrue(PushNotificationConfig.is_enabled()) diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index a22e444fdd..53fb93693e 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -1,9 +1,12 @@ ''' Utilities for contentstore tests ''' +from __future__ import absolute_import + import json import textwrap +import six from django.conf import settings from django.contrib.auth.models import User from django.test.client import Client @@ -26,7 +29,7 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT def parse_json(response): """Parse response, which is assumed to be json""" - return json.loads(response.content) + return json.loads(response.content.decode('utf-8')) def user(email): @@ -48,7 +51,7 @@ class AjaxEnabledTestClient(Client): Convenience method for client post which serializes the data into json and sets the accept type to json """ - if not isinstance(data, basestring): + if not isinstance(data, six.string_types): data = json.dumps(data or {}) kwargs.setdefault("HTTP_X_REQUESTED_WITH", "XMLHttpRequest") kwargs.setdefault("HTTP_ACCEPT", "application/json") @@ -354,7 +357,7 @@ class CourseTestCase(ProceduralCourseTestMixin, ModuleStoreTestCase): course1_asset_attrs = content_store.get_attrs(course1_id.make_asset_key(category, filename)) course2_asset_attrs = content_store.get_attrs(course2_id.make_asset_key(category, filename)) self.assertEqual(len(course1_asset_attrs), len(course2_asset_attrs)) - for key, value in course1_asset_attrs.iteritems(): + for key, value in six.iteritems(course1_asset_attrs): if key in ['_id', 'filename', 'uploadDate', 'content_son', 'thumbnail_location']: pass else: diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 237dd34afc..c6f6ce48ab 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1,11 +1,12 @@ """ Common utility functions useful throughout the contentstore """ -from __future__ import print_function +from __future__ import absolute_import, print_function import logging from datetime import datetime +import six from django.conf import settings from django.urls import reverse from django.utils.translation import ugettext as _ @@ -150,7 +151,7 @@ def get_lms_link_for_certificate_web_view(user_id, course_key, mode): return u"//{certificate_web_base}/certificates/user/{user_id}/course/{course_id}?preview={mode}".format( certificate_web_base=lms_base, user_id=user_id, - course_id=unicode(course_key), + course_id=six.text_type(course_key), mode=mode ) @@ -274,7 +275,7 @@ def reverse_url(handler_name, key_name=None, key_value=None, kwargs=None): Creates the URL for the given handler. The optional key_name and key_value are passed in as kwargs to the handler. """ - kwargs_for_reverse = {key_name: unicode(key_value)} if key_name else None + kwargs_for_reverse = {key_name: six.text_type(key_value)} if key_name else None if kwargs: kwargs_for_reverse.update(kwargs) return reverse(handler_name, kwargs=kwargs_for_reverse) @@ -427,7 +428,7 @@ def get_user_partition_info(xblock, schemes=None, course=None): # Put together the entire partition dictionary partitions.append({ "id": p.id, - "name": unicode(p.name), # Convert into a string in case ugettext_lazy was used + "name": six.text_type(p.name), # Convert into a string in case ugettext_lazy was used "scheme": p.scheme.name, "groups": groups, }) diff --git a/cms/djangoapps/contentstore/video_utils.py b/cms/djangoapps/contentstore/video_utils.py index df64c5c281..39260b1385 100644 --- a/cms/djangoapps/contentstore/video_utils.py +++ b/cms/djangoapps/contentstore/video_utils.py @@ -2,16 +2,18 @@ """ Utils related to the videos. """ -import logging -from urlparse import urljoin -import requests +from __future__ import absolute_import +import logging + +import requests +import six from django.conf import settings from django.core.files.images import get_image_dimensions from django.core.files.uploadedfile import SimpleUploadedFile from django.utils.translation import ugettext as _ from edxval.api import get_course_video_image_url, update_video_image - +from six.moves.urllib.parse import urljoin # pylint: disable=import-error # Youtube thumbnail sizes. # https://img.youtube.com/vi/{youtube_id}/{thumbnail_quality}.jpg @@ -40,9 +42,9 @@ def validate_video_image(image_file, skip_aspect_ratio=False): if not all(hasattr(image_file, attr) for attr in ['name', 'content_type', 'size']): error = _('The image must have name, content type, and size information.') - elif image_file.content_type not in settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.values(): + elif image_file.content_type not in list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.values()): error = _(u'This image file type is not supported. Supported file types are {supported_file_formats}.').format( - supported_file_formats=settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys() + supported_file_formats=list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys()) ) elif image_file.size > settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES']: error = _(u'This image file must be smaller than {image_max_size}.').format( @@ -126,7 +128,7 @@ def scrape_youtube_thumbnail(course_id, edx_video_id, youtube_id): # Scrape when course video image does not exist for edx_video_id. if not get_course_video_image_url(course_id, edx_video_id): thumbnail_content, thumbnail_content_type = download_youtube_video_thumbnail(youtube_id) - supported_content_types = {v: k for k, v in settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.iteritems()} + supported_content_types = {v: k for k, v in six.iteritems(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS)} image_filename = '{youtube_id}{image_extention}'.format( youtube_id=youtube_id, image_extention=supported_content_types.get( diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py index e90628aef4..3f81e7f8f9 100644 --- a/cms/djangoapps/contentstore/views/__init__.py +++ b/cms/djangoapps/contentstore/views/__init__.py @@ -2,26 +2,25 @@ "All view functions for contentstore, broken out into submodules" -# Disable warnings about import from wildcard -# All files below declare exports with __all__ from .assets import * +from .checklists import * from .component import * from .course import * -from .checklists import * from .entrance_exam import * from .error import * +from .export_git import * from .helpers import * -from .item import * from .import_export import * +from .item import * from .library import * from .preview import * from .public import * -from .export_git import * -from .user import * from .tabs import * -from .videos import * from .transcript_settings import * from .transcripts_ajax import * +from .user import * +from .videos import * + try: from .dev import * except ImportError: diff --git a/cms/djangoapps/contentstore/views/access.py b/cms/djangoapps/contentstore/views/access.py index 50fb74e55c..2ed9de9453 100644 --- a/cms/djangoapps/contentstore/views/access.py +++ b/cms/djangoapps/contentstore/views/access.py @@ -1,5 +1,7 @@ """ Helper methods for determining user access permissions in Studio """ +from __future__ import absolute_import + from student import auth from student.roles import CourseInstructorRole diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 9a08794a0d..c914063283 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -1,32 +1,36 @@ +"""Views for assets""" +from __future__ import absolute_import + import json import logging import math -from functools import partial import re +from functools import partial +import six from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.utils.translation import ugettext as _ from django.views.decorators.csrf import ensure_csrf_cookie -from django.views.decorators.http import require_POST, require_http_methods +from django.views.decorators.http import require_http_methods, require_POST from opaque_keys.edx.keys import AssetKey, CourseKey from pymongo import ASCENDING, DESCENDING from six import text_type -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.modulestore.exceptions import ItemNotFoundError -from contentstore.utils import reverse_course_url +from ..utils import reverse_course_url from contentstore.views.exception import AssetNotFoundException, AssetSizeTooLargeException from edxmako.shortcuts import render_to_response from openedx.core.djangoapps.contentserver.caching import del_cached_content from student.auth import has_course_author_access from util.date_utils import get_default_time_display from util.json_request import JsonResponse +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.modulestore.exceptions import ItemNotFoundError __all__ = ['assets_handler'] @@ -186,6 +190,7 @@ def _get_requested_attribute(request, attribute): def _get_error_if_invalid_parameters(requested_filter): + """Function for returning error messages on filters""" requested_file_types = _get_requested_file_types_from_requested_filter(requested_filter) invalid_filters = [] @@ -295,6 +300,7 @@ def _get_sort_type_and_direction(request_options): def _get_mongo_sort_from_requested_sort(requested_sort): + """Function returns sorts dataset based on the key provided""" if requested_sort == 'date_added': sort = 'uploadDate' elif requested_sort == 'display_name': @@ -320,6 +326,7 @@ def _get_first_asset_index(current_page, page_size): def _get_assets_for_page(course_key, options): + """returns course content for given course and options""" current_page = options['current_page'] page_size = options['page_size'] sort = options['sort'] @@ -331,10 +338,12 @@ def _get_assets_for_page(course_key, options): def _update_options_to_requery_final_page(query_options, total_asset_count): + """sets current_page value based on asset count and page_size""" query_options['current_page'] = int(math.floor((total_asset_count - 1) / query_options['page_size'])) def _get_assets_in_json_format(assets, course_key): + """returns assets information in JSON Format""" assets_in_json_format = [] for asset in assets: thumbnail_asset_key = _get_thumbnail_asset_key(asset, course_key) @@ -355,6 +364,7 @@ def _get_assets_in_json_format(assets, course_key): def update_course_run_asset(course_key, upload_file): + """returns contents of the uploaded file""" course_exists_response = _get_error_if_course_does_not_exist(course_key) if course_exists_response is not None: @@ -388,6 +398,7 @@ def update_course_run_asset(course_key, upload_file): @ensure_csrf_cookie @login_required def _upload_asset(request, course_key): + """uploads the file in request and returns JSON response""" course_exists_error = _get_error_if_course_does_not_exist(course_key) if course_exists_error is not None: @@ -442,11 +453,13 @@ def _get_file_metadata_as_dictionary(upload_file): def get_file_size(upload_file): + """returns the size of the uploaded file""" # can be used for mocking test file sizes. return upload_file.size def _check_file_size_is_too_large(file_metadata): + """verifies whether file size is greater than allowed file size""" upload_file_size = file_metadata['upload_file_size'] maximum_file_size_in_megabytes = settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB maximum_file_size_in_bytes = maximum_file_size_in_megabytes * 1000 ** 2 @@ -455,6 +468,8 @@ def _check_file_size_is_too_large(file_metadata): def _get_file_too_large_error_message(filename): + """returns formatted error message for large files""" + return _( u'File {filename} exceeds maximum size of ' u'{maximum_size_in_megabytes} MB.' @@ -465,6 +480,7 @@ def _get_file_too_large_error_message(filename): def _get_file_content_and_path(file_metadata, course_key): + """returns contents of the uploaded file and path for temporary uploaded file""" content_location = StaticContent.compute_location(course_key, file_metadata['filename']) upload_file = file_metadata['upload_file'] @@ -483,10 +499,12 @@ def _get_file_content_and_path(file_metadata, course_key): def _check_thumbnail_uploaded(thumbnail_content): + """returns whether thumbnail is None""" return thumbnail_content is not None def _get_thumbnail_asset_key(asset, course_key): + """returns thumbnail asset key""" # note, due to the schema change we may not have a 'thumbnail_location' in the result set thumbnail_location = asset.get('thumbnail_location', None) thumbnail_asset_key = None @@ -501,12 +519,12 @@ def _get_thumbnail_asset_key(asset, course_key): @login_required @ensure_csrf_cookie def _update_asset(request, course_key, asset_key): - ''' + """ restful CRUD operations for a course asset. Currently only DELETE, POST, and PUT methods are implemented. asset_path_encoding: the odd /c4x/org/course/category/name repr of the asset (used by Backbone as the id) - ''' + """ if request.method == 'DELETE': try: delete_asset(course_key, asset_key) @@ -520,7 +538,7 @@ def _update_asset(request, course_key, asset_key): # update existing asset try: - modified_asset = json.loads(request.body) + modified_asset = json.loads(request.body.decode('utf8')) except ValueError: return HttpResponseBadRequest() contentstore().set_attr(asset_key, 'locked', modified_asset['locked']) @@ -530,10 +548,12 @@ def _update_asset(request, course_key, asset_key): def _save_content_to_trash(content): + """saves the content to trash""" contentstore('trashcan').save(content) def delete_asset(course_key, asset_key): + """deletes the cached content based on asset key""" content = _check_existence_and_get_asset_content(asset_key) _save_content_to_trash(content) @@ -583,5 +603,5 @@ def _get_asset_json(display_name, content_type, date, location, thumbnail_locati 'thumbnail': StaticContent.serialize_asset_key_with_slash(thumbnail_location) if thumbnail_location else None, 'locked': locked, # needed for Backbone delete/update. - 'id': unicode(location) + 'id': six.text_type(location) } diff --git a/cms/djangoapps/contentstore/views/certificates.py b/cms/djangoapps/contentstore/views/certificates.py index acef5a41d1..69a208c8b9 100644 --- a/cms/djangoapps/contentstore/views/certificates.py +++ b/cms/djangoapps/contentstore/views/certificates.py @@ -21,9 +21,12 @@ course.certificates: { ] } """ +from __future__ import absolute_import + import json import logging +import six from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied @@ -31,6 +34,7 @@ from django.http import HttpResponse from django.utils.translation import ugettext as _ from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_http_methods +from eventtracking import tracker from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import AssetKey, CourseKey from six import text_type @@ -40,7 +44,6 @@ from contentstore.views.assets import delete_asset from contentstore.views.exception import AssetNotFoundException from course_modes.models import CourseMode from edxmako.shortcuts import render_to_response -from eventtracking import tracker from student.auth import has_studio_write_access from student.roles import GlobalStaff from util.db import MYSQL_MAX_INT, generate_int_id @@ -346,7 +349,7 @@ def certificate_activation_handler(request, course_key_string): msg = _(u'PermissionDenied: Failed in authenticating {user}').format(user=request.user) return JsonResponse({"error": msg}, status=403) - data = json.loads(request.body) + data = json.loads(request.body.decode('utf8')) is_active = data.get('is_active', False) certificates = CertificateManager.get_certificates(course) @@ -358,7 +361,7 @@ def certificate_activation_handler(request, course_key_string): store.update_item(course, request.user.id) cert_event_type = 'activated' if is_active else 'deactivated' CertificateManager.track_event(cert_event_type, { - 'course_id': unicode(course.id), + 'course_id': six.text_type(course.id), }) return HttpResponse(status=200) @@ -446,7 +449,7 @@ def certificates_list_handler(request, course_key_string): ) store.update_item(course, request.user.id) CertificateManager.track_event('created', { - 'course_id': unicode(course.id), + 'course_id': six.text_type(course.id), 'configuration_id': new_certificate.id }) course = _get_course_and_check_access(course_key, request.user) @@ -503,7 +506,7 @@ def certificates_detail_handler(request, course_key_string, certificate_id): store.update_item(course, request.user.id) CertificateManager.track_event(cert_event_type, { - 'course_id': unicode(course.id), + 'course_id': six.text_type(course.id), 'configuration_id': serialized_certificate["id"] }) return JsonResponse(serialized_certificate, status=201) @@ -525,7 +528,7 @@ def certificates_detail_handler(request, course_key_string, certificate_id): certificate_id=certificate_id ) CertificateManager.track_event('deleted', { - 'course_id': unicode(course.id), + 'course_id': six.text_type(course.id), 'configuration_id': certificate_id }) return JsonResponse(status=204) diff --git a/cms/djangoapps/contentstore/views/checklists.py b/cms/djangoapps/contentstore/views/checklists.py index f3fb7cffba..ebea256a50 100644 --- a/cms/djangoapps/contentstore/views/checklists.py +++ b/cms/djangoapps/contentstore/views/checklists.py @@ -1,12 +1,14 @@ # pylint: disable=missing-docstring +from __future__ import absolute_import + from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.views.decorators.csrf import ensure_csrf_cookie from opaque_keys.edx.keys import CourseKey -from xmodule.modulestore.django import modulestore from edxmako.shortcuts import render_to_response from student.auth import has_course_author_access +from xmodule.modulestore.django import modulestore __all__ = ['checklists_handler'] diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 1228b724e2..ed75ccd25b 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import logging +import six from django.conf import settings from django.contrib.auth.decorators import login_required @@ -130,9 +131,10 @@ def container_handler(request, usage_key_string): assert unit is not None, "Could not determine unit page" subsection = get_parent_xblock(unit) - assert subsection is not None, "Could not determine parent subsection from unit " + unicode(unit.location) + assert subsection is not None, "Could not determine parent subsection from unit " + six.text_type( + unit.location) section = get_parent_xblock(subsection) - assert section is not None, "Could not determine ancestor section from unit " + unicode(unit.location) + assert section is not None, "Could not determine ancestor section from unit " + six.text_type(unit.location) # Fetch the XBlock info for use by the container page. Note that it includes information # about the block's ancestors and siblings for use by the Unit Outline. diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 0aa32a6647..5b5aa03264 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -1,13 +1,15 @@ """ Views related to operations on course objects """ -from collections import defaultdict +from __future__ import absolute_import + import copy import json import logging import random import re import string +from collections import defaultdict import django.utils import six @@ -15,20 +17,18 @@ from ccx_keys.locator import CCXLocator from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied, ValidationError -from django.urls import reverse from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotFound from django.shortcuts import redirect +from django.urls import reverse from django.utils.translation import ugettext as _ from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_GET, require_http_methods +from milestones import api as milestones_api from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import BlockUsageLocator -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace -from openedx.features.course_experience.waffle import waffle as course_experience_waffle -from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML from six import text_type +from six.moves import filter from contentstore.course_group_config import ( COHORT_SCHEME, @@ -39,7 +39,6 @@ from contentstore.course_group_config import ( ) from contentstore.course_info_model import delete_course_update, get_course_updates, update_course_updates from contentstore.courseware_index import CoursewareSearchIndexer, SearchIndexingError -from contentstore.push_notification import push_notification_enabled from contentstore.tasks import rerun_course as rerun_course_task from contentstore.utils import ( add_instructor, @@ -56,19 +55,22 @@ from course_action_state.managers import CourseActionStateItemNotFoundError from course_action_state.models import CourseRerunState, CourseRerunUIStateManager from course_creators.views import add_user_with_status_unrequested, get_course_creator_status from edxmako.shortcuts import render_to_response -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_overviews.models import CourseOverview 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 from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from openedx.core.djangolib.js_utils import dump_js_escaped_json from openedx.core.lib.course_tabs import CourseTabPluginManager from openedx.core.lib.courses import course_image_url -from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME from openedx.features.content_type_gating.models import ContentTypeGatingConfig +from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME +from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML +from openedx.features.course_experience.waffle import waffle as course_experience_waffle from student import auth from student.auth import has_course_author_access, has_studio_read_access, has_studio_write_access from student.roles import CourseCreatorRole, CourseInstructorRole, CourseStaffRole, GlobalStaff, UserBasedRole @@ -460,7 +462,7 @@ def _accessible_courses_list_from_groups(request): instructor_courses = UserBasedRole(request.user, CourseInstructorRole.ROLE).courses_with_role() staff_courses = UserBasedRole(request.user, CourseStaffRole.ROLE).courses_with_role() - all_courses = filter(filter_ccx, instructor_courses | staff_courses) + all_courses = list(filter(filter_ccx, instructor_courses | staff_courses)) courses_list = [] course_keys = {} @@ -469,7 +471,7 @@ def _accessible_courses_list_from_groups(request): raise AccessListFallback course_keys[course_access.course_id] = course_access.course_id - course_keys = course_keys.values() + course_keys = list(course_keys.values()) if course_keys: courses_list = modulestore().get_course_summaries(course_keys=course_keys) @@ -514,7 +516,7 @@ def course_listing(request): """ return { u'display_name': uca.display_name, - u'course_key': unicode(uca.course_key), + u'course_key': six.text_type(uca.course_key), u'org': uca.course_key.org, u'number': uca.course_key.course, u'run': uca.course_key.run, @@ -536,8 +538,8 @@ def course_listing(request): return { u'display_name': library.display_name, - u'library_key': unicode(library.location.library_key), - u'url': reverse_library_url(u'library_handler', unicode(library.location.library_key)), + u'library_key': six.text_type(library.location.library_key), + u'url': reverse_library_url(u'library_handler', six.text_type(library.location.library_key)), u'org': library.display_org_with_default, u'number': library.display_number_with_default, u'can_edit': has_studio_write_access(request.user, library.location.library_key), @@ -624,7 +626,7 @@ def course_index(request, course_key): lms_link = get_lms_link_for_item(course_module.location) reindex_link = None if settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', False): - reindex_link = "/course/{course_id}/search_reindex".format(course_id=unicode(course_key)) + reindex_link = "/course/{course_id}/search_reindex".format(course_id=six.text_type(course_key)) sections = course_module.get_children() course_structure = _course_outline_json(request, course_module) locator_to_show = request.GET.get('show', None) @@ -708,7 +710,7 @@ def _process_courses_list(courses_iter, in_process_course_actions, split_archive """ return { 'display_name': course.display_name, - 'course_key': unicode(course.location.course_key), + 'course_key': six.text_type(course.location.course_key), 'url': reverse_course_url('course_handler', course.id), 'lms_link': get_lms_link_for_item(course.location), 'rerun_link': _get_rerun_link_for_item(course.id), @@ -819,14 +821,14 @@ def _create_or_rerun_course(request): destination_course_key = rerun_course(request.user, source_course_key, org, course, run, fields) return JsonResponse({ 'url': reverse_url('course_handler'), - 'destination_course_key': unicode(destination_course_key) + 'destination_course_key': six.text_type(destination_course_key) }) else: try: new_course = create_new_course(request.user, org, course, run, fields) return JsonResponse({ 'url': reverse_course_url('course_handler', new_course.id), - 'course_key': unicode(new_course.id), + 'course_key': six.text_type(new_course.id), }) except ValidationError as ex: return JsonResponse({'error': text_type(ex)}, status=400) @@ -928,7 +930,7 @@ def rerun_course(user, source_course_key, org, number, run, fields, background=T fields['video_upload_pipeline'] = {} json_fields = json.dumps(fields, cls=EdxJSONEncoder) - args = [unicode(source_course_key), unicode(destination_course_key), user.id, json_fields] + args = [six.text_type(source_course_key), six.text_type(destination_course_key), user.id, json_fields] if background: rerun_course_task.delay(*args) @@ -963,7 +965,6 @@ def course_info_handler(request, course_key_string): 'updates_url': reverse_course_url('course_info_update_handler', course_key), 'handouts_locator': course_key.make_usage_key('course_info', 'handouts'), 'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_module.id), - 'push_notification_enabled': push_notification_enabled() } ) else: @@ -1221,7 +1222,7 @@ def grading_handler(request, course_key_string, grader_index=None): # update credit course requirements if 'minimum_grade_credit' # field value is changed if 'minimum_grade_credit' in request.json: - update_credit_course_requirements.delay(unicode(course_key)) + update_credit_course_requirements.delay(six.text_type(course_key)) # None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader if grader_index is None: @@ -1369,7 +1370,7 @@ def validate_textbook_json(textbook): """ Validate the given text as representing a list of PDF textbooks """ - if isinstance(textbook, basestring): + if isinstance(textbook, six.string_types): try: textbook = json.loads(textbook) except ValueError: @@ -1378,7 +1379,7 @@ def validate_textbook_json(textbook): raise TextbookValidationError("must be JSON object") if not textbook.get("tab_title"): raise TextbookValidationError("must have tab_title") - tid = unicode(textbook.get("id", "")) + tid = six.text_type(textbook.get("id", "")) if tid and not tid[0].isdigit(): raise TextbookValidationError("textbook ID must start with a digit") return textbook @@ -1495,7 +1496,7 @@ def textbooks_detail_handler(request, course_key_string, textbook_id): with store.bulk_operations(course_key): course_module = get_course_and_check_access(course_key, request.user) matching_id = [tb for tb in course_module.pdf_textbooks - if unicode(tb.get("id")) == unicode(textbook_id)] + if six.text_type(tb.get("id")) == six.text_type(textbook_id)] if matching_id: textbook = matching_id[0] else: @@ -1683,7 +1684,7 @@ def group_configurations_detail_handler(request, course_key_string, group_config with store.bulk_operations(course_key): course = get_course_and_check_access(course_key, request.user) matching_id = [p for p in course.user_partitions - if unicode(p.id) == unicode(group_configuration_id)] + if six.text_type(p.id) == six.text_type(group_configuration_id)] if matching_id: configuration = matching_id[0] else: diff --git a/cms/djangoapps/contentstore/views/dev.py b/cms/djangoapps/contentstore/views/dev.py index 2b2178eaba..a2fc3f7f14 100644 --- a/cms/djangoapps/contentstore/views/dev.py +++ b/cms/djangoapps/contentstore/views/dev.py @@ -4,6 +4,8 @@ These views will NOT be shown on production: trying to access them will result in a 404 error. """ # pylint: disable=unused-argument +from __future__ import absolute_import + from edxmako.shortcuts import render_to_response diff --git a/cms/djangoapps/contentstore/views/entrance_exam.py b/cms/djangoapps/contentstore/views/entrance_exam.py index c6944a5673..b1d9d0d5db 100644 --- a/cms/djangoapps/contentstore/views/entrance_exam.py +++ b/cms/djangoapps/contentstore/views/entrance_exam.py @@ -2,9 +2,12 @@ Entrance Exams view module -- handles all requests related to entrance exam management via Studio Intended to be utilized as an AJAX callback handler, versus a proper view/screen """ +from __future__ import absolute_import + import logging from functools import wraps +import six from django.conf import settings from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseBadRequest @@ -130,7 +133,7 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N return HttpResponse(status=400) # Create the entrance exam item (currently it's just a chapter) - parent_locator = unicode(course.location) + parent_locator = six.text_type(course.location) created_block = create_xblock( parent_locator=parent_locator, user=request.user, @@ -145,13 +148,13 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N metadata = { 'entrance_exam_enabled': True, 'entrance_exam_minimum_score_pct': entrance_exam_minimum_score_pct, - 'entrance_exam_id': unicode(created_block.location), + 'entrance_exam_id': six.text_type(created_block.location), } CourseMetadata.update_from_dict(metadata, course, request.user) # Create the entrance exam section item. create_xblock( - parent_locator=unicode(created_block.location), + parent_locator=six.text_type(created_block.location), user=request.user, category='sequential', display_name=_('Entrance Exam - Subsection') @@ -177,7 +180,7 @@ def _get_entrance_exam(request, course_key): # pylint: disable=W0613 try: exam_descriptor = modulestore().get_item(exam_key) return HttpResponse( - dump_js_escaped_json({'locator': unicode(exam_descriptor.location)}), + dump_js_escaped_json({'locator': six.text_type(exam_descriptor.location)}), status=200, content_type='application/json') except ItemNotFoundError: return HttpResponse(status=404) @@ -246,7 +249,7 @@ def add_entrance_exam_milestone(course_id, x_block): if len(milestones): milestone = milestones[0] else: - description = u'Autogenerated during {} entrance exam creation.'.format(unicode(course_id)) + description = u'Autogenerated during {} entrance exam creation.'.format(six.text_type(course_id)) milestone = milestones_helpers.add_milestone({ 'name': _('Completed Course Entrance Exam'), 'namespace': milestone_namespace, @@ -254,13 +257,13 @@ def add_entrance_exam_milestone(course_id, x_block): }) relationship_types = milestones_helpers.get_milestone_relationship_types() milestones_helpers.add_course_milestone( - unicode(course_id), + six.text_type(course_id), relationship_types['REQUIRES'], milestone ) milestones_helpers.add_course_content_milestone( - unicode(course_id), - unicode(x_block.location), + six.text_type(course_id), + six.text_type(x_block.location), relationship_types['FULFILLS'], milestone ) @@ -277,4 +280,4 @@ def remove_entrance_exam_milestone_reference(request, course_key): for course_child in course_children: if course_child.is_entrance_exam: delete_item(request, course_child.scope_ids.usage_id) - milestones_helpers.remove_content_references(unicode(course_child.scope_ids.usage_id)) + milestones_helpers.remove_content_references(six.text_type(course_child.scope_ids.usage_id)) diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py index 755417bda4..8045b0fd4a 100644 --- a/cms/djangoapps/contentstore/views/error.py +++ b/cms/djangoapps/contentstore/views/error.py @@ -1,5 +1,7 @@ # pylint: disable=missing-docstring,unused-argument +from __future__ import absolute_import + import functools from django.http import HttpResponse, HttpResponseNotFound, HttpResponseServerError diff --git a/cms/djangoapps/contentstore/views/export_git.py b/cms/djangoapps/contentstore/views/export_git.py index 47a5624170..eb0c0d364b 100644 --- a/cms/djangoapps/contentstore/views/export_git.py +++ b/cms/djangoapps/contentstore/views/export_git.py @@ -3,8 +3,11 @@ This views handles exporting the course xml to a git repository if the giturl attribute is set. """ +from __future__ import absolute_import + import logging +import six from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.utils.translation import ugettext as _ @@ -46,7 +49,7 @@ def export_git(request, course_key_string): msg = _('Course successfully exported to git repository') except git_export_utils.GitExportError as ex: failed = True - msg = unicode(ex) + msg = six.text_type(ex) return render_to_response('export_git.html', { 'context_course': course_module, diff --git a/cms/djangoapps/contentstore/views/helpers.py b/cms/djangoapps/contentstore/views/helpers.py index 73811cf43d..4eb087dfb8 100644 --- a/cms/djangoapps/contentstore/views/helpers.py +++ b/cms/djangoapps/contentstore/views/helpers.py @@ -4,9 +4,9 @@ Helper methods for Studio views. from __future__ import absolute_import -import urllib from uuid import uuid4 +import six from django.conf import settings from django.http import HttpResponse from django.utils.translation import ugettext as _ @@ -111,7 +111,7 @@ def xblock_studio_url(xblock, parent_xblock=None): elif category in ('chapter', 'sequential'): return u'{url}?show={usage_key}'.format( url=reverse_course_url('course_handler', xblock.location.course_key), - usage_key=urllib.quote(unicode(xblock.location)) + usage_key=six.moves.urllib.parse.quote(six.text_type(xblock.location)) ) elif category == 'library': library_key = xblock.location.course_key @@ -222,7 +222,7 @@ def create_xblock(parent_locator, user, category, display_name, boilerplate=None # TODO need to fix components that are sending definition_data as strings, instead of as dicts # For now, migrate them into dicts here. - if isinstance(data, basestring): + if isinstance(data, six.string_types): data = {'data': data} created_block = store.create_child( diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index aa48d8baff..fdbc4e176a 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -2,6 +2,8 @@ These views handle all actions in Studio related to import and exporting of courses """ +from __future__ import absolute_import + import base64 import json import logging @@ -42,10 +44,8 @@ __all__ = [ 'export_handler', 'export_output_handler', 'export_status_handler', ] - log = logging.getLogger(__name__) - # Regex to capture Content-Range header ranges. CONTENT_RE = re.compile(r"(?P\d{1,11})-(?P\d{1,11})/(?P\d{1,11})") @@ -147,7 +147,7 @@ def _write_chunk(request, courselike_key): try: matches = CONTENT_RE.search(request.META["HTTP_CONTENT_RANGE"]) content_range = matches.groupdict() - except KeyError: # Single chunk + except KeyError: # Single chunk # no Content-Range header, so make one that will work content_range = {'start': 0, 'stop': 1, 'end': 2} @@ -179,7 +179,7 @@ def _write_chunk(request, courselike_key): elif size > int(content_range['stop']) and size == int(content_range['end']): return JsonResponse({'ImportStatus': 1}) - with open(temp_filepath, mode) as temp_file: + with open(temp_filepath, mode) as temp_file: # pylint: disable=W6005 for chunk in request.FILES['course-data'].chunks(): temp_file.write(chunk) @@ -199,7 +199,7 @@ def _write_chunk(request, courselike_key): }) log.info(u"Course import %s: Upload complete", courselike_key) - with open(temp_filepath, 'rb') as local_file: + with open(temp_filepath, 'rb') as local_file: # pylint: disable=W6005 django_file = File(local_file) storage_path = course_import_export_storage.save(u'olx_import/' + filename, django_file) import_olx.delay( @@ -386,7 +386,7 @@ def export_status_handler(request, course_key_string): elif task_status.state in (UserTaskStatus.FAILED, UserTaskStatus.CANCELED): status = max(-(task_status.completed_steps + 1), -2) errors = UserTaskArtifact.objects.filter(status=task_status, name='Error') - if len(errors): + if errors: error = errors[0].text try: error = json.loads(error) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 492a36d24d..e96956a8e3 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -225,7 +225,10 @@ def xblock_handler(request, usage_key_string): request.user, request.json.get('display_name'), ) - return JsonResponse({'locator': unicode(dest_usage_key), 'courseKey': unicode(dest_usage_key.course_key)}) + return JsonResponse({ + 'locator': text_type(dest_usage_key), + 'courseKey': text_type(dest_usage_key.course_key) + }) else: return _create_item(request) elif request.method == 'PATCH': @@ -323,14 +326,14 @@ def xblock_view_handler(request, usage_key_string, view_name): xblock.runtime.wrappers.append(partial( wrap_xblock, 'StudioRuntime', - usage_id_serializer=unicode, + usage_id_serializer=text_type, request_token=request_token(request), )) xblock.runtime.wrappers_asides.append(partial( wrap_xblock_aside, 'StudioRuntime', - usage_id_serializer=unicode, + usage_id_serializer=text_type, request_token=request_token(request), extra_classes=['wrapper-comp-plugins'] )) @@ -508,7 +511,7 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, store.revert_to_published(xblock.location, user.id) # Returning the same sort of result that we do for other save operations. In the future, # we may want to return the full XBlockInfo. - return JsonResponse({'id': unicode(xblock.location)}) + return JsonResponse({'id': text_type(xblock.location)}) old_metadata = own_metadata(xblock) old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content) @@ -615,7 +618,7 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, store.update_item(course, user.id) result = { - 'id': unicode(xblock.location), + 'id': text_type(xblock.location), 'data': data, 'metadata': own_metadata(xblock) } @@ -692,7 +695,7 @@ def _create_item(request): ) return JsonResponse( - {'locator': unicode(created_block.location), 'courseKey': unicode(created_block.location.course_key)} + {'locator': text_type(created_block.location), 'courseKey': text_type(created_block.location.course_key)} ) @@ -724,7 +727,7 @@ def is_source_item_in_target_parents(source_item, target_parent): """ target_ancestors = _create_xblock_ancestor_info(target_parent, is_concise=True)['ancestors'] for target_ancestor in target_ancestors: - if unicode(source_item.location) == target_ancestor['id']: + if text_type(source_item.location) == target_ancestor['id']: return True return False @@ -782,15 +785,15 @@ def _move_item(source_usage_key, target_parent_usage_key, user, target_index=Non error = _('You can not move an item directly into content experiment.') elif source_index is None: error = _(u'{source_usage_key} not found in {parent_usage_key}.').format( - source_usage_key=unicode(source_usage_key), - parent_usage_key=unicode(source_parent.location) + source_usage_key=text_type(source_usage_key), + parent_usage_key=text_type(source_parent.location) ) else: try: target_index = int(target_index) if target_index is not None else None - if len(target_parent.children) < target_index: + if target_index is not None and len(target_parent.children) < target_index: error = _(u'You can not move {source_usage_key} at an invalid index ({target_index}).').format( - source_usage_key=unicode(source_usage_key), + source_usage_key=text_type(source_usage_key), target_index=target_index ) except ValueError: @@ -813,15 +816,15 @@ def _move_item(source_usage_key, target_parent_usage_key, user, target_index=Non log.info( u'MOVE: %s moved from %s to %s at %d index', - unicode(source_usage_key), - unicode(source_parent.location), - unicode(target_parent_usage_key), + text_type(source_usage_key), + text_type(source_parent.location), + text_type(target_parent_usage_key), insert_at ) context = { - 'move_source_locator': unicode(source_usage_key), - 'parent_locator': unicode(target_parent_usage_key), + 'move_source_locator': text_type(source_usage_key), + 'parent_locator': text_type(target_parent_usage_key), 'source_index': target_index if target_index is not None else source_index } return JsonResponse(context) @@ -956,7 +959,7 @@ def orphan_handler(request, course_key_string): course_usage_key = CourseKey.from_string(course_key_string) if request.method == 'GET': if has_studio_read_access(request.user, course_usage_key): - return JsonResponse([unicode(item) for item in modulestore().get_orphans(course_usage_key)]) + return JsonResponse([text_type(item) for item in modulestore().get_orphans(course_usage_key)]) else: raise PermissionDenied() if request.method == 'DELETE': @@ -984,7 +987,7 @@ def _delete_orphans(course_usage_key, user_id, commit=False): if branch == ModuleStoreEnum.BranchName.published: revision = ModuleStoreEnum.RevisionOption.published_only store.delete_item(itemloc, user_id, revision=revision) - return [unicode(item) for item in items] + return [text_type(item) for item in items] def _get_xblock(usage_key, user): @@ -1004,7 +1007,7 @@ def _get_xblock(usage_key, user): raise except InvalidLocationError: log.error("Can't find item by location.") - return JsonResponse({"error": "Can't find item by location: " + unicode(usage_key)}, 404) + return JsonResponse({"error": "Can't find item by location: " + text_type(usage_key)}, 404) def _get_module_info(xblock, rewrite_static_links=True, include_ancestor_info=False, include_publishing_info=False): @@ -1056,7 +1059,7 @@ def _get_gating_info(course, xblock): setattr(course, 'gating_prerequisites', gating_api.get_prerequisites(course.id)) info["is_prereq"] = gating_api.is_prerequisite(course.id, xblock.location) info["prereqs"] = [ - p for p in course.gating_prerequisites if unicode(xblock.location) not in p['namespace'] + p for p in course.gating_prerequisites if text_type(xblock.location) not in p['namespace'] ] prereq, prereq_min_score, prereq_min_completion = gating_api.get_required_content( course.id, @@ -1158,7 +1161,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F pct_sign=_('%')) xblock_info = { - 'id': unicode(xblock.location), + 'id': text_type(xblock.location), 'display_name': xblock.display_name_with_default, 'category': xblock.category, 'has_children': xblock.has_children diff --git a/cms/djangoapps/contentstore/views/library.py b/cms/djangoapps/contentstore/views/library.py index f26b3dca93..a9bdc0dace 100644 --- a/cms/djangoapps/contentstore/views/library.py +++ b/cms/djangoapps/contentstore/views/library.py @@ -106,13 +106,13 @@ def _display_library(library_key_string, request): if not has_studio_read_access(request.user, library_key): log.exception( u"User %s tried to access library %s without permission", - request.user.username, unicode(library_key) + request.user.username, text_type(library_key) ) raise PermissionDenied() library = modulestore().get_library(library_key) if library is None: - log.exception(u"Library %s not found", unicode(library_key)) + log.exception(u"Library %s not found", text_type(library_key)) raise Http404 response_format = 'html' @@ -132,7 +132,7 @@ def _list_libraries(request): lib_info = [ { "display_name": lib.display_name, - "library_key": unicode(lib.location.library_key), + "library_key": text_type(lib.location.library_key), } for lib in modulestore().get_libraries() if has_studio_read_access(request.user, lib.location.library_key) @@ -182,7 +182,7 @@ def _create_library(request): ) }) - lib_key_str = unicode(new_lib.location.library_key) + lib_key_str = text_type(new_lib.location.library_key) return JsonResponse({ 'url': reverse_library_url('library_handler', lib_key_str), 'library_key': lib_key_str, @@ -208,10 +208,10 @@ def library_blocks_view(library, user, response_format): prev_version = library.runtime.course_entry.structure['previous_version'] return JsonResponse({ "display_name": library.display_name, - "library_id": unicode(library.location.library_key), - "version": unicode(library.runtime.course_entry.course_key.version_guid), - "previous_version": unicode(prev_version) if prev_version else None, - "blocks": [unicode(x) for x in children], + "library_id": text_type(library.location.library_key), + "version": text_type(library.runtime.course_entry.course_key.version_guid), + "previous_version": text_type(prev_version) if prev_version else None, + "blocks": [text_type(x) for x in children], }) can_edit = has_studio_write_access(user, library.location.library_key) @@ -261,7 +261,7 @@ def manage_library_users(request, library_key_string): 'context_library': library, 'users': formatted_users, 'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES), - 'library_key': unicode(library_key), + 'library_key': text_type(library_key), 'lib_users_url': reverse_library_url('manage_library_users', library_key_string), 'show_children_previews': library.show_children_previews }) diff --git a/cms/djangoapps/contentstore/views/organization.py b/cms/djangoapps/contentstore/views/organization.py index 253229963a..7fa03d51ce 100644 --- a/cms/djangoapps/contentstore/views/organization.py +++ b/cms/djangoapps/contentstore/views/organization.py @@ -1,4 +1,6 @@ """Organizations views for use with Studio.""" +from __future__ import absolute_import + from django.contrib.auth.decorators import login_required from django.http import HttpResponse from django.utils.decorators import method_decorator diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 2c4694c691..68a26fb9ba 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -2,6 +2,7 @@ from __future__ import absolute_import import logging from functools import partial +import six from django.conf import settings from django.contrib.auth.decorators import login_required @@ -101,7 +102,7 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False): return reverse('preview_handler', kwargs={ - 'usage_key_string': unicode(block.scope_ids.usage_id), + 'usage_key_string': six.text_type(block.scope_ids.usage_id), 'handler': handler_name, 'suffix': suffix, }) + '?' + query @@ -166,7 +167,7 @@ def _preview_module_system(request, descriptor, field_data): wrap_xblock, 'PreviewRuntime', display_name_only=display_name_only, - usage_id_serializer=unicode, + usage_id_serializer=six.text_type, request_token=request_token(request) ), @@ -180,7 +181,7 @@ def _preview_module_system(request, descriptor, field_data): partial( wrap_xblock_aside, 'PreviewRuntime', - usage_id_serializer=unicode, + usage_id_serializer=six.text_type, request_token=request_token(request) ) ] diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py index c850afaa58..e9d5603cbb 100644 --- a/cms/djangoapps/contentstore/views/public.py +++ b/cms/djangoapps/contentstore/views/public.py @@ -1,18 +1,19 @@ """ Public views """ +from __future__ import absolute_import + from django.conf import settings -from django.template.context_processors import csrf -from django.urls import reverse -from django.utils.http import urlquote_plus from django.shortcuts import redirect +from django.template.context_processors import csrf +from django.utils.http import urlquote_plus from django.views.decorators.clickjacking import xframe_options_deny from django.views.decorators.csrf import ensure_csrf_cookie +from waffle.decorators import waffle_switch +from contentstore.config import waffle from edxmako.shortcuts import render_to_response from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from waffle.decorators import waffle_switch -from contentstore.config import waffle __all__ = ['signup', 'login_page', 'login_redirect_to_lms', 'howitworks', 'accessibility'] diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py index edfb438165..f5438f1ab5 100644 --- a/cms/djangoapps/contentstore/views/tabs.py +++ b/cms/djangoapps/contentstore/views/tabs.py @@ -1,6 +1,9 @@ """ Views related to course tabs """ +from __future__ import absolute_import + +import six from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.http import HttpResponseNotFound @@ -197,7 +200,7 @@ def primitive_delete(course, num): def primitive_insert(course, num, tab_type, name): "Inserts a new tab at the given number (0 based)." validate_args(num, tab_type) - new_tab = CourseTab.from_json({u'type': unicode(tab_type), u'name': unicode(name)}) + new_tab = CourseTab.from_json({u'type': six.text_type(tab_type), u'name': six.text_type(name)}) tabs = course.tabs tabs.insert(num, new_tab) modulestore().update_item(course, ModuleStoreEnum.UserID.primitive_command) diff --git a/cms/djangoapps/contentstore/views/tests/test_access.py b/cms/djangoapps/contentstore/views/tests/test_access.py index a9aefbd4c4..5a17c31e0d 100644 --- a/cms/djangoapps/contentstore/views/tests/test_access.py +++ b/cms/djangoapps/contentstore/views/tests/test_access.py @@ -1,6 +1,8 @@ """ Tests access.py """ +from __future__ import absolute_import + from django.contrib.auth.models import User from django.test import TestCase from opaque_keys.edx.locator import CourseLocator diff --git a/cms/djangoapps/contentstore/views/tests/test_assets.py b/cms/djangoapps/contentstore/views/tests/test_assets.py index 4db1a66bbd..ea9c6d7a0c 100644 --- a/cms/djangoapps/contentstore/views/tests/test_assets.py +++ b/cms/djangoapps/contentstore/views/tests/test_assets.py @@ -1,11 +1,14 @@ """ Unit tests for the asset upload endpoint. """ +from __future__ import absolute_import + import json from datetime import datetime from io import BytesIO import mock +import six from ddt import data, ddt from django.conf import settings from django.test.utils import override_settings @@ -242,7 +245,7 @@ class PaginationTestCase(AssetsTestCase): Get from the url and ensure it contains the expected number of responses """ resp = self.client.get(url, HTTP_ACCEPT='application/json') - json_response = json.loads(resp.content) + json_response = json.loads(resp.content.decode('utf-8')) assets_response = json_response['assets'] self.assertEquals(json_response['start'], expected_start) self.assertEquals(len(assets_response), expected_length) @@ -254,7 +257,7 @@ class PaginationTestCase(AssetsTestCase): """ resp = self.client.get( url + '?sort=' + sort + '&direction=' + direction, HTTP_ACCEPT='application/json') - json_response = json.loads(resp.content) + json_response = json.loads(resp.content.decode('utf-8')) assets_response = json_response['assets'] self.assertEquals(sort, json_response['sort']) self.assertEquals(direction, json_response['direction']) @@ -290,7 +293,7 @@ class PaginationTestCase(AssetsTestCase): resp = self.client.get( url + '?' + filter_type + '=' + filter_value, HTTP_ACCEPT='application/json') - json_response = json.loads(resp.content) + json_response = json.loads(resp.content.decode('utf-8')) assets_response = json_response['assets'] self.assertEquals(filter_value_split, json_response['assetTypes']) @@ -301,7 +304,9 @@ class PaginationTestCase(AssetsTestCase): for content_type in content_types: # content_type is either not any defined type (i.e. OTHER) or is a defined type (if multiple # parameters including OTHER are used) - self.assertTrue(content_type in requested_file_extensions or content_type not in all_file_extensions) + self.assertTrue( + content_type in requested_file_extensions or content_type not in all_file_extensions + ) else: for content_type in content_types: self.assertIn(content_type, requested_file_extensions) @@ -320,7 +325,7 @@ class PaginationTestCase(AssetsTestCase): """ resp = self.client.get( url + '?text_search=' + text_search, HTTP_ACCEPT='application/json') - json_response = json.loads(resp.content) + json_response = json.loads(resp.content.decode('utf-8')) assets_response = json_response['assets'] self.assertEquals(text_search, json_response['textSearch']) self.assertEquals(len(assets_response), number_matches) @@ -329,7 +334,7 @@ class PaginationTestCase(AssetsTestCase): for asset_response in assets_response: for token in text_search_tokens: - self.assertTrue(token.lower() in asset_response['display_name'].lower()) + self.assertIn(token.lower(), asset_response['display_name'].lower()) @ddt @@ -383,7 +388,7 @@ class DownloadTestCase(AssetsTestCase): self.asset_name = 'download_test' resp = self.upload_asset(self.asset_name) self.assertEquals(resp.status_code, 200) - self.uploaded_url = json.loads(resp.content)['asset']['url'] + self.uploaded_url = json.loads(resp.content.decode('utf-8'))['asset']['url'] def test_download(self): # Now, download it. @@ -424,10 +429,12 @@ class AssetToJsonTestCase(AssetsTestCase): self.assertEquals(output["display_name"], "my_file") self.assertEquals(output["date_added"], "Jun 01, 2013 at 10:30 UTC") self.assertEquals(output["url"], "/asset-v1:org+class+run+type@asset+block@my_file_name.jpg") - self.assertEquals(output["external_url"], "lms_base_url/asset-v1:org+class+run+type@asset+block@my_file_name.jpg") + self.assertEquals( + output["external_url"], "lms_base_url/asset-v1:org+class+run+type@asset+block@my_file_name.jpg" + ) self.assertEquals(output["portable_url"], "/static/my_file_name.jpg") self.assertEquals(output["thumbnail"], "/asset-v1:org+class+run+type@thumbnail+block@my_file_name_thumb.jpg") - self.assertEquals(output["id"], unicode(location)) + self.assertEquals(output["id"], six.text_type(location)) self.assertEquals(output['locked'], True) output = assets._get_asset_json("name", content_type, upload_date, location, None, False) @@ -454,7 +461,9 @@ class LockAssetTestCase(AssetsTestCase): content_type = 'application/txt' upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC) asset_location = course.id.make_asset_key('asset', 'sample_static.html') - url = reverse_course_url('assets_handler', course.id, kwargs={'asset_key_string': unicode(asset_location)}) + url = reverse_course_url( + 'assets_handler', course.id, kwargs={'asset_key_string': six.text_type(asset_location)} + ) resp = self.client.post( url, @@ -465,7 +474,7 @@ class LockAssetTestCase(AssetsTestCase): ) self.assertEqual(resp.status_code, 201) - return json.loads(resp.content) + return json.loads(resp.content.decode('utf-8')) # Load the toy course. module_store = modulestore() @@ -505,7 +514,7 @@ class DeleteAssetTestCase(AssetsTestCase): response = self.client.post(self.url, {"name": self.asset_name, "file": self.asset}) self.assertEquals(response.status_code, 200) - self.uploaded_url = json.loads(response.content)['asset']['url'] + self.uploaded_url = json.loads(response.content.decode('utf-8'))['asset']['url'] self.asset_location = AssetKey.from_string(self.uploaded_url) self.content = contentstore().find(self.asset_location) @@ -513,7 +522,7 @@ class DeleteAssetTestCase(AssetsTestCase): def test_delete_asset(self): """ Tests the happy path :) """ test_url = reverse_course_url( - 'assets_handler', self.course.id, kwargs={'asset_key_string': unicode(self.uploaded_url)}) + 'assets_handler', self.course.id, kwargs={'asset_key_string': six.text_type(self.uploaded_url)}) resp = self.client.delete(test_url, HTTP_ACCEPT="application/json") self.assertEquals(resp.status_code, 204) @@ -525,12 +534,12 @@ class DeleteAssetTestCase(AssetsTestCase): # upload image response = self.client.post(self.url, {"name": "delete_image_test", "file": image_asset}) self.assertEquals(response.status_code, 200) - uploaded_image_url = json.loads(response.content)['asset']['url'] + uploaded_image_url = json.loads(response.content.decode('utf-8'))['asset']['url'] # upload image thumbnail response = self.client.post(self.url, {"name": "delete_image_thumb_test", "file": thumbnail_image_asset}) self.assertEquals(response.status_code, 200) - thumbnail_url = json.loads(response.content)['asset']['url'] + thumbnail_url = json.loads(response.content.decode('utf-8'))['asset']['url'] thumbnail_location = StaticContent.get_location_from_path(thumbnail_url) image_asset_location = AssetKey.from_string(uploaded_image_url) @@ -542,21 +551,23 @@ class DeleteAssetTestCase(AssetsTestCase): mock_asset_key.return_value = thumbnail_location test_url = reverse_course_url( - 'assets_handler', self.course.id, kwargs={'asset_key_string': unicode(uploaded_image_url)}) + 'assets_handler', self.course.id, kwargs={'asset_key_string': six.text_type(uploaded_image_url)}) resp = self.client.delete(test_url, HTTP_ACCEPT="application/json") self.assertEquals(resp.status_code, 204) def test_delete_asset_with_invalid_asset(self): """ Tests the sad path :( """ test_url = reverse_course_url( - 'assets_handler', self.course.id, kwargs={'asset_key_string': unicode("/c4x/edX/toy/asset/invalid.pdf")}) + 'assets_handler', + self.course.id, kwargs={'asset_key_string': six.text_type("/c4x/edX/toy/asset/invalid.pdf")} + ) resp = self.client.delete(test_url, HTTP_ACCEPT="application/json") self.assertEquals(resp.status_code, 404) def test_delete_asset_with_invalid_thumbnail(self): """ Tests the sad path :( """ test_url = reverse_course_url( - 'assets_handler', self.course.id, kwargs={'asset_key_string': unicode(self.uploaded_url)}) + 'assets_handler', self.course.id, kwargs={'asset_key_string': six.text_type(self.uploaded_url)}) self.content.thumbnail_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/invalid') contentstore().save(self.content) resp = self.client.delete(test_url, HTTP_ACCEPT="application/json") diff --git a/cms/djangoapps/contentstore/views/tests/test_certificates.py b/cms/djangoapps/contentstore/views/tests/test_certificates.py index e315cf76fe..e4001c210e 100644 --- a/cms/djangoapps/contentstore/views/tests/test_certificates.py +++ b/cms/djangoapps/contentstore/views/tests/test_certificates.py @@ -3,14 +3,18 @@ """ Certificates Tests. """ +from __future__ import absolute_import + import itertools import json import ddt import mock +import six from django.conf import settings from django.test.utils import override_settings from opaque_keys.edx.keys import AssetKey +from six.moves import range from contentstore.tests.utils import CourseTestCase from contentstore.utils import get_lms_link_for_certificate_web_view, reverse_course_url @@ -80,7 +84,7 @@ class HelperMethods(object): 'title': 'Title ' + str(i), 'signature_image_path': asset_path_format.format(i), 'id': i - } for i in xrange(signatory_count) + } for i in range(signatory_count) ] @@ -95,7 +99,7 @@ class HelperMethods(object): 'signatories': signatories, 'version': CERTIFICATE_SCHEMA_VERSION, 'is_active': is_active - } for i in xrange(count) + } for i in range(count) ] self.course.certificates = {'certificates': certificates} self.save_course() @@ -142,7 +146,7 @@ class CertificatesBaseTestCase(object): self.assertEqual(response.status_code, 400) self.assertNotIn("Location", response) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertIn("error", content) def test_invalid_json(self): @@ -163,7 +167,7 @@ class CertificatesBaseTestCase(object): self.assertEqual(response.status_code, 400) self.assertNotIn("Location", response) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertIn("error", content) def test_certificate_data_validation(self): @@ -231,12 +235,12 @@ class CertificatesListHandlerTestCase( self.assertEqual(response.status_code, 201) self.assertIn("Location", response) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) certificate_id = self._remove_ids(content) self.assertEqual(content, expected) self.assert_event_emitted( 'edx.certificate.configuration.created', - course_id=unicode(self.course.id), + course_id=six.text_type(self.course.id), configuration_id=certificate_id, ) @@ -265,7 +269,7 @@ class CertificatesListHandlerTestCase( @override_settings(LMS_BASE="lms_base_url") def test_lms_link_for_certificate_web_view(self): test_url = "//lms_base_url/certificates/user/" \ - + str(self.user.id) + "/course/" + unicode(self.course.id) + '?preview=honor' + + str(self.user.id) + "/course/" + six.text_type(self.course.id) + '?preview=honor' link = get_lms_link_for_certificate_web_view( user_id=self.user.id, course_key=self.course.id, @@ -293,7 +297,7 @@ class CertificatesListHandlerTestCase( # in JSON response response = self.client.get_json(self._url()) - data = json.loads(response.content) + data = json.loads(response.content.decode('utf-8')) self.assertEquals(len(data), 1) self.assertEqual(data[0]['name'], 'Test certificate') self.assertEqual(data[0]['description'], 'Test description') @@ -413,7 +417,7 @@ class CertificatesListHandlerTestCase( HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - new_certificate = json.loads(response.content) + new_certificate = json.loads(response.content.decode('utf-8')) for prev_certificate in self.course.certificates['certificates']: self.assertNotEqual(new_certificate.get('id'), prev_certificate.get('id')) @@ -467,11 +471,11 @@ class CertificatesDetailHandlerTestCase( HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(content, expected) self.assert_event_emitted( 'edx.certificate.configuration.created', - course_id=unicode(self.course.id), + course_id=six.text_type(self.course.id), configuration_id=666, ) @@ -499,11 +503,11 @@ class CertificatesDetailHandlerTestCase( HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(content, expected) self.assert_event_emitted( 'edx.certificate.configuration.modified', - course_id=unicode(self.course.id), + course_id=six.text_type(self.course.id), configuration_id=1, ) self.reload_course() @@ -550,7 +554,7 @@ class CertificatesDetailHandlerTestCase( HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 201) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(content, expected) @ddt.data(C4X_SIGNATORY_PATH, SIGNATORY_PATH) @@ -568,7 +572,7 @@ class CertificatesDetailHandlerTestCase( self.assertEqual(response.status_code, 204) self.assert_event_emitted( 'edx.certificate.configuration.deleted', - course_id=unicode(self.course.id), + course_id=six.text_type(self.course.id), configuration_id='1', ) self.reload_course() @@ -592,7 +596,7 @@ class CertificatesDetailHandlerTestCase( self.assertEqual(response.status_code, 204) self.assert_event_emitted( 'edx.certificate.configuration.deleted', - course_id=unicode(self.course.id), + course_id=six.text_type(self.course.id), configuration_id='1', ) self.reload_course() @@ -617,7 +621,7 @@ class CertificatesDetailHandlerTestCase( self.assertEqual(response.status_code, 204) self.assert_event_emitted( 'edx.certificate.configuration.deleted', - course_id=unicode(self.course.id), + course_id=six.text_type(self.course.id), configuration_id='1', ) self.reload_course() @@ -785,7 +789,7 @@ class CertificatesDetailHandlerTestCase( cert_event_type = 'activated' if is_active else 'deactivated' self.assert_event_emitted( '.'.join(['edx.certificate.configuration', cert_event_type]), - course_id=unicode(self.course.id), + course_id=six.text_type(self.course.id), ) @ddt.data(*itertools.product([True, False], [C4X_SIGNATORY_PATH, SIGNATORY_PATH])) diff --git a/cms/djangoapps/contentstore/views/tests/test_container_page.py b/cms/djangoapps/contentstore/views/tests/test_container_page.py index 25ef5d437c..fddf3bf6e8 100644 --- a/cms/djangoapps/contentstore/views/tests/test_container_page.py +++ b/cms/djangoapps/contentstore/views/tests/test_container_page.py @@ -1,9 +1,12 @@ """ Unit tests for the container page. """ +from __future__ import absolute_import + import datetime import re +import six from django.http import Http404 from django.test.client import RequestFactory from django.utils import http @@ -60,12 +63,12 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase): u'data-locator="{0}" data-course-key="{0.course_key}">'.format(self.child_container.location) ), expected_breadcrumbs=( - ur'\s*Week 1\s*\s*' - ur'\s*Lesson 1\s*\s*' - ur'\s*Unit\s*' + u'\\s*Week 1\\s*\\s*' + u'\\s*Lesson 1\\s*\\s*' + u'\\s*Unit\\s*' ).format( - course=re.escape(unicode(self.course.id)), - unit=re.escape(unicode(self.vertical.location)), + course=re.escape(six.text_type(self.course.id)), + unit=re.escape(six.text_type(self.vertical.location)), classes='navigation-item navigation-link navigation-parent', section_parameters=re.escape(u'?show={}'.format(http.urlquote(self.chapter.location))), subsection_parameters=re.escape(u'?show={}'.format(http.urlquote(self.sequential.location))), @@ -88,14 +91,14 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase): u'data-locator="{0}" data-course-key="{0.course_key}">'.format(draft_container.location) ), expected_breadcrumbs=( - ur'\s*Week 1\s*\s*' - ur'\s*Lesson 1\s*\s*' - ur'\s*Unit\s*\s*' - ur'\s*Split Test\s*' + u'\\s*Week 1\\s*\\s*' + u'\\s*Lesson 1\\s*\\s*' + u'\\s*Unit\\s*\\s*' + u'\\s*Split Test\\s*' ).format( - course=re.escape(unicode(self.course.id)), - unit=re.escape(unicode(self.vertical.location)), - split_test=re.escape(unicode(self.child_container.location)), + course=re.escape(six.text_type(self.course.id)), + unit=re.escape(six.text_type(self.vertical.location)), + split_test=re.escape(six.text_type(self.child_container.location)), classes=u'navigation-item navigation-link navigation-parent', section_parameters=re.escape(u'?show={}'.format(http.urlquote(self.chapter.location))), subsection_parameters=re.escape(u'?show={}'.format(http.urlquote(self.sequential.location))), @@ -223,6 +226,6 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase): # Check 200 response if 'usage_key_string' is correct response = views.container_handler( request=request, - usage_key_string=unicode(self.vertical.location) + usage_key_string=six.text_type(self.vertical.location) ) self.assertEqual(response.status_code, 200) diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index 42879eed70..e9af42c822 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -1,6 +1,8 @@ """ Unit tests for getting the list of courses and the course outline. """ +from __future__ import absolute_import + import datetime import json @@ -8,24 +10,25 @@ import ddt import lxml import mock import pytz +import six from django.conf import settings from django.core.exceptions import PermissionDenied from django.test.utils import override_settings from django.utils.translation import ugettext as _ +from edx_django_utils.monitoring.middleware import _DEFAULT_NAMESPACE as DJANGO_UTILS_NAMESPACE from opaque_keys.edx.locator import CourseLocator from search.api import perform_search -from edx_django_utils.monitoring.middleware import _DEFAULT_NAMESPACE as DJANGO_UTILS_NAMESPACE +from contentstore.config.waffle import WAFFLE_NAMESPACE as STUDIO_WAFFLE_NAMESPACE from contentstore.courseware_index import CoursewareSearchIndexer, SearchIndexingError from contentstore.tests.utils import CourseTestCase from contentstore.utils import add_instructor, reverse_course_url, reverse_usage_url +from contentstore.views.course import WAFFLE_NAMESPACE as COURSE_WAFFLE_NAMESPACE from contentstore.views.course import ( _deprecated_blocks_info, course_outline_initial_state, reindex_course_and_check_access ) -from contentstore.config.waffle import WAFFLE_NAMESPACE as STUDIO_WAFFLE_NAMESPACE -from contentstore.views.course import WAFFLE_NAMESPACE as COURSE_WAFFLE_NAMESPACE from contentstore.views.item import VisibilityState, create_xblock_info from course_action_state.managers import CourseRerunUIStateManager from course_action_state.models import CourseRerunState @@ -139,11 +142,11 @@ class TestCourseIndex(CourseTestCase): ItemFactory.create(parent_location=subsection.location, category="video", display_name="My Video") resp = self.client.get(outline_url, HTTP_ACCEPT='application/json') - json_response = json.loads(resp.content) + json_response = json.loads(resp.content.decode('utf-8')) # First spot check some values in the root response self.assertEqual(json_response['category'], 'course') - self.assertEqual(json_response['id'], unicode(self.course.location)) + self.assertEqual(json_response['id'], six.text_type(self.course.location)) self.assertEqual(json_response['display_name'], self.course.display_name) self.assertTrue(json_response['published']) self.assertIsNone(json_response['visibility_state']) @@ -153,7 +156,7 @@ class TestCourseIndex(CourseTestCase): self.assertGreater(len(children), 0) first_child_response = children[0] self.assertEqual(first_child_response['category'], 'chapter') - self.assertEqual(first_child_response['id'], unicode(chapter.location)) + self.assertEqual(first_child_response['id'], six.text_type(chapter.location)) self.assertEqual(first_child_response['display_name'], 'Week 1') self.assertTrue(json_response['published']) self.assertEqual(first_child_response['visibility_state'], VisibilityState.unscheduled) @@ -195,7 +198,7 @@ class TestCourseIndex(CourseTestCase): }) resp = self.client.get(notification_url, HTTP_ACCEPT='application/json') - json_response = json.loads(resp.content) + json_response = json.loads(resp.content.decode('utf-8')) self.assertEquals(json_response['state'], state) self.assertEquals(json_response['action'], action) @@ -461,11 +464,11 @@ class TestCourseOutline(CourseTestCase): outline_url = reverse_course_url('course_handler', self.course.id) outline_url = outline_url + '?format=concise' if is_concise else outline_url resp = self.client.get(outline_url, HTTP_ACCEPT='application/json') - json_response = json.loads(resp.content) + json_response = json.loads(resp.content.decode('utf-8')) # First spot check some values in the root response self.assertEqual(json_response['category'], 'course') - self.assertEqual(json_response['id'], unicode(self.course.location)) + self.assertEqual(json_response['id'], six.text_type(self.course.location)) self.assertEqual(json_response['display_name'], self.course.display_name) self.assertNotEqual(json_response.get('published', False), is_concise) self.assertIsNone(json_response.get('visibility_state')) @@ -475,7 +478,7 @@ class TestCourseOutline(CourseTestCase): self.assertGreater(len(children), 0) first_child_response = children[0] self.assertEqual(first_child_response['category'], 'chapter') - self.assertEqual(first_child_response['id'], unicode(self.chapter.location)) + self.assertEqual(first_child_response['id'], six.text_type(self.chapter.location)) self.assertEqual(first_child_response['display_name'], 'Week 1') self.assertNotEqual(json_response.get('published', False), is_concise) if not is_concise: @@ -509,12 +512,12 @@ class TestCourseOutline(CourseTestCase): self.assertIsNone(course_outline_initial_state('no-such-locator', course_structure)) # Verify that the correct initial state is returned for the test chapter - chapter_locator = unicode(self.chapter.location) + chapter_locator = six.text_type(self.chapter.location) initial_state = course_outline_initial_state(chapter_locator, course_structure) self.assertEqual(initial_state['locator_to_show'], chapter_locator) expanded_locators = initial_state['expanded_locators'] - self.assertIn(unicode(self.sequential.location), expanded_locators) - self.assertIn(unicode(self.vertical.location), expanded_locators) + self.assertIn(six.text_type(self.sequential.location), expanded_locators) + self.assertIn(six.text_type(self.vertical.location), expanded_locators) def _create_test_data(self, course_module, create_blocks=False, publish=True, block_types=None): """ @@ -551,7 +554,7 @@ class TestCourseOutline(CourseTestCase): [component for component in advanced_modules if component in deprecated_block_types] ) - self.assertItemsEqual(info['blocks'], expected_blocks) + six.assertCountEqual(self, info['blocks'], expected_blocks) self.assertEqual( info['advance_settings_url'], reverse_course_url('advanced_settings_handler', course_id) @@ -677,7 +680,7 @@ class TestCourseReIndex(CourseTestCase): self.assertIn(self.SUCCESSFUL_RESPONSE, response.content) self.assertEqual(response.status_code, 200) - @mock.patch('xmodule.html_module.HtmlDescriptor.index_dictionary') + @mock.patch('xmodule.html_module.HtmlBlock.index_dictionary') def test_reindex_course_search_index_error(self, mock_index_dictionary): """ Test json response with mocked error data for html @@ -703,7 +706,7 @@ class TestCourseReIndex(CourseTestCase): user=self.user, size=10, from_=0, - course_id=unicode(self.course.id)) + course_id=six.text_type(self.course.id)) self.assertEqual(response['total'], 1) # Start manual reindex @@ -715,10 +718,10 @@ class TestCourseReIndex(CourseTestCase): user=self.user, size=10, from_=0, - course_id=unicode(self.course.id)) + course_id=six.text_type(self.course.id)) self.assertEqual(response['total'], 1) - @mock.patch('xmodule.video_module.VideoDescriptor.index_dictionary') + @mock.patch('xmodule.video_module.VideoBlock.index_dictionary') def test_reindex_video_error_json_responses(self, mock_index_dictionary): """ Test json response with mocked error data for video @@ -729,7 +732,7 @@ class TestCourseReIndex(CourseTestCase): user=self.user, size=10, from_=0, - course_id=unicode(self.course.id)) + course_id=six.text_type(self.course.id)) self.assertEqual(response['total'], 1) # set mocked exception response @@ -740,7 +743,7 @@ class TestCourseReIndex(CourseTestCase): with self.assertRaises(SearchIndexingError): reindex_course_and_check_access(self.course.id, self.user) - @mock.patch('xmodule.html_module.HtmlDescriptor.index_dictionary') + @mock.patch('xmodule.html_module.HtmlBlock.index_dictionary') def test_reindex_html_error_json_responses(self, mock_index_dictionary): """ Test json response with mocked error data for html @@ -751,7 +754,7 @@ class TestCourseReIndex(CourseTestCase): user=self.user, size=10, from_=0, - course_id=unicode(self.course.id)) + course_id=six.text_type(self.course.id)) self.assertEqual(response['total'], 1) # set mocked exception response @@ -773,7 +776,7 @@ class TestCourseReIndex(CourseTestCase): user=self.user, size=10, from_=0, - course_id=unicode(self.course.id)) + course_id=six.text_type(self.course.id)) self.assertEqual(response['total'], 1) # set mocked exception response @@ -813,7 +816,7 @@ class TestCourseReIndex(CourseTestCase): user=self.user, size=10, from_=0, - course_id=unicode(self.course.id)) + course_id=six.text_type(self.course.id)) self.assertEqual(response['total'], 1) # Start manual reindex @@ -825,10 +828,10 @@ class TestCourseReIndex(CourseTestCase): user=self.user, size=10, from_=0, - course_id=unicode(self.course.id)) + course_id=six.text_type(self.course.id)) self.assertEqual(response['total'], 1) - @mock.patch('xmodule.video_module.VideoDescriptor.index_dictionary') + @mock.patch('xmodule.video_module.VideoBlock.index_dictionary') def test_indexing_video_error_responses(self, mock_index_dictionary): """ Test do_course_reindex response with mocked error data for video @@ -839,7 +842,7 @@ class TestCourseReIndex(CourseTestCase): user=self.user, size=10, from_=0, - course_id=unicode(self.course.id)) + course_id=six.text_type(self.course.id)) self.assertEqual(response['total'], 1) # set mocked exception response @@ -850,7 +853,7 @@ class TestCourseReIndex(CourseTestCase): with self.assertRaises(SearchIndexingError): CoursewareSearchIndexer.do_course_reindex(modulestore(), self.course.id) - @mock.patch('xmodule.html_module.HtmlDescriptor.index_dictionary') + @mock.patch('xmodule.html_module.HtmlBlock.index_dictionary') def test_indexing_html_error_responses(self, mock_index_dictionary): """ Test do_course_reindex response with mocked error data for html @@ -861,7 +864,7 @@ class TestCourseReIndex(CourseTestCase): user=self.user, size=10, from_=0, - course_id=unicode(self.course.id)) + course_id=six.text_type(self.course.id)) self.assertEqual(response['total'], 1) # set mocked exception response @@ -883,7 +886,7 @@ class TestCourseReIndex(CourseTestCase): user=self.user, size=10, from_=0, - course_id=unicode(self.course.id)) + course_id=six.text_type(self.course.id)) self.assertEqual(response['total'], 1) # set mocked exception response diff --git a/cms/djangoapps/contentstore/views/tests/test_course_updates.py b/cms/djangoapps/contentstore/views/tests/test_course_updates.py index 17f411edde..acdfd28b35 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_updates.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_updates.py @@ -1,13 +1,14 @@ """ unit tests for course_info views and models. """ +from __future__ import absolute_import + import json from django.test.utils import override_settings from mock import patch from opaque_keys.edx.keys import UsageKey -from contentstore.models import PushNotificationConfig from contentstore.tests.test_course_settings import CourseTestCase from contentstore.utils import reverse_course_url, reverse_usage_url from xmodule.modulestore.django import modulestore @@ -36,7 +37,7 @@ class CourseUpdateTest(CourseTestCase): resp = self.client.ajax_post(url, payload) self.assertContains(resp, '', status_code=200) - return json.loads(resp.content) + return json.loads(resp.content.decode('utf-8')) resp = self.client.get_html( reverse_course_url('course_info_handler', self.course.id) @@ -56,7 +57,7 @@ class CourseUpdateTest(CourseTestCase): first_update_url, payload, HTTP_X_HTTP_METHOD_OVERRIDE="PUT", REQUEST_METHOD="POST" ) - self.assertHTMLEqual(content, json.loads(resp.content)['content'], + self.assertHTMLEqual(content, json.loads(resp.content.decode('utf-8'))['content'], "iframe w/ div") # refetch using provided id refetched = self.client.get_json(first_update_url) @@ -71,7 +72,7 @@ class CourseUpdateTest(CourseTestCase): course_update_url = self.create_update_url() resp = self.client.get_json(course_update_url) - payload = json.loads(resp.content) + payload = json.loads(resp.content.decode('utf-8')) self.assertEqual(len(payload), 2) # try json w/o required fields @@ -118,12 +119,12 @@ class CourseUpdateTest(CourseTestCase): self.assertHTMLEqual(content, payload['content'], "single iframe") # first count the entries resp = self.client.get_json(course_update_url) - payload = json.loads(resp.content) + payload = json.loads(resp.content.decode('utf-8')) before_delete = len(payload) url = self.create_update_url(provided_id=this_id) resp = self.client.delete(url) - payload = json.loads(resp.content) + payload = json.loads(resp.content.decode('utf-8')) self.assertEqual(len(payload), before_delete - 1) def test_course_updates_compatibility(self): @@ -148,7 +149,7 @@ class CourseUpdateTest(CourseTestCase): # test getting all updates list course_update_url = self.create_update_url() resp = self.client.get_json(course_update_url) - payload = json.loads(resp.content) + payload = json.loads(resp.content.decode('utf-8')) self.assertEqual(payload, [{u'date': update_date, u'content': update_content, u'id': 1}]) self.assertEqual(len(payload), 1) @@ -156,7 +157,7 @@ class CourseUpdateTest(CourseTestCase): first_update_url = self.create_update_url(provided_id=payload[0]['id']) resp = self.client.get_json(first_update_url) - payload = json.loads(resp.content) + payload = json.loads(resp.content.decode('utf-8')) self.assertEqual(payload, {u'date': u'January 23, 2014', u'content': u'Hello world!', u'id': 1}) self.assertHTMLEqual(update_date, payload['date']) self.assertHTMLEqual(update_content, payload['content']) @@ -171,7 +172,7 @@ class CourseUpdateTest(CourseTestCase): resp = self.client.ajax_post( course_update_url + '1', payload, HTTP_X_HTTP_METHOD_OVERRIDE="PUT", REQUEST_METHOD="POST" ) - self.assertHTMLEqual(update_content, json.loads(resp.content)['content']) + self.assertHTMLEqual(update_content, json.loads(resp.content.decode('utf-8'))['content']) course_updates = modulestore().get_item(location) self.assertEqual(course_updates.items, [{u'date': update_date, u'content': update_content, u'id': 1}]) # course_updates 'data' field should not update automatically @@ -182,7 +183,7 @@ class CourseUpdateTest(CourseTestCase): self.assertEqual(course_updates.items, [{u'date': update_date, u'content': update_content, u'id': 1}]) # now try to delete first update item resp = self.client.delete(course_update_url + '1') - self.assertEqual(json.loads(resp.content), []) + self.assertEqual(json.loads(resp.content.decode('utf-8')), []) # confirm that course update is soft deleted ('status' flag set to 'deleted') in db course_updates = modulestore().get_item(location) self.assertEqual(course_updates.items, @@ -190,7 +191,7 @@ class CourseUpdateTest(CourseTestCase): # now try to get deleted update resp = self.client.get_json(course_update_url + '1') - payload = json.loads(resp.content) + payload = json.loads(resp.content.decode('utf-8')) self.assertEqual(payload.get('error'), u"Course update not found.") self.assertEqual(resp.status_code, 404) @@ -205,7 +206,7 @@ class CourseUpdateTest(CourseTestCase): resp = self.client.ajax_post( course_update_url, payload, REQUEST_METHOD="POST" ) - self.assertHTMLEqual(update_content, json.loads(resp.content)['content']) + self.assertHTMLEqual(update_content, json.loads(resp.content.decode('utf-8'))['content']) def test_no_ol_course_update(self): '''Test trying to add to a saved course_update which is not an ol.''' @@ -228,16 +229,16 @@ class CourseUpdateTest(CourseTestCase): course_update_url = self.create_update_url() resp = self.client.ajax_post(course_update_url, payload) - payload = json.loads(resp.content) + payload = json.loads(resp.content.decode('utf-8')) self.assertHTMLEqual(payload['content'], content) # now confirm that the bad news and the iframe make up single update resp = self.client.get_json(course_update_url) - payload = json.loads(resp.content) + payload = json.loads(resp.content.decode('utf-8')) self.assertEqual(len(payload), 1) - def post_course_update(self, send_push_notification=False): + def post_course_update(self): """ Posts an update to the course """ @@ -248,26 +249,20 @@ class CourseUpdateTest(CourseTestCase): content = u"Sample update" payload = {'content': content, 'date': 'January 8, 2013'} - if send_push_notification: - payload['push_notification_selected'] = True resp = self.client.ajax_post(course_update_url, payload) # check that response status is 200 not 400 self.assertEqual(resp.status_code, 200) - payload = json.loads(resp.content) + payload = json.loads(resp.content.decode('utf-8')) self.assertHTMLEqual(payload['content'], content) - @patch("contentstore.push_notification.send_push_course_update") - def test_post_course_update(self, mock_push_update): + def test_post_course_update(self): """ Test that a user can successfully post on course updates and handouts of a course """ self.post_course_update() - # check that push notifications are not sent - self.assertFalse(mock_push_update.called) - updates_location = self.course.id.make_usage_key('course_info', 'updates') self.assertTrue(isinstance(updates_location, UsageKey)) self.assertEqual(updates_location.block_id, u'updates') @@ -283,34 +278,5 @@ class CourseUpdateTest(CourseTestCase): # check that response status is 200 not 500 self.assertEqual(resp.status_code, 200) - payload = json.loads(resp.content) + payload = json.loads(resp.content.decode('utf-8')) self.assertHTMLEqual(payload['data'], content) - - @patch("contentstore.push_notification.send_push_course_update") - def test_notifications_enabled_but_not_requested(self, mock_push_update): - PushNotificationConfig(enabled=True).save() - self.post_course_update() - self.assertFalse(mock_push_update.called) - - @patch("contentstore.push_notification.send_push_course_update") - def test_notifications_enabled_and_sent(self, mock_push_update): - PushNotificationConfig(enabled=True).save() - self.post_course_update(send_push_notification=True) - self.assertTrue(mock_push_update.called) - - @override_settings(PARSE_KEYS={"APPLICATION_ID": "TEST_APPLICATION_ID", "REST_API_KEY": "TEST_REST_API_KEY"}) - @patch("contentstore.push_notification.Push") - def test_notifications_sent_to_parse(self, mock_parse_push): - PushNotificationConfig(enabled=True).save() - self.post_course_update(send_push_notification=True) - self.assertEquals(mock_parse_push.alert.call_count, 2) - - @override_settings(PARSE_KEYS={"APPLICATION_ID": "TEST_APPLICATION_ID", "REST_API_KEY": "TEST_REST_API_KEY"}) - @patch("contentstore.push_notification.log_exception") - @patch("contentstore.push_notification.Push") - def test_notifications_error_from_parse(self, mock_parse_push, mock_log_exception): - PushNotificationConfig(enabled=True).save() - from parse_rest.core import ParseError - mock_parse_push.alert.side_effect = ParseError - self.post_course_update(send_push_notification=True) - self.assertTrue(mock_log_exception.called) diff --git a/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py b/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py index 6f882a79d1..68710b34eb 100644 --- a/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py +++ b/cms/djangoapps/contentstore/views/tests/test_credit_eligibility.py @@ -2,7 +2,10 @@ Unit tests for credit eligibility UI in Studio. """ +from __future__ import absolute_import + import mock +import six from contentstore.tests.utils import CourseTestCase from contentstore.utils import reverse_course_url @@ -20,7 +23,7 @@ class CreditEligibilityTest(CourseTestCase): def setUp(self): super(CreditEligibilityTest, self).setUp() self.course = CourseFactory.create(org='edX', number='dummy', display_name='Credit Course') - self.course_details_url = reverse_course_url('settings_handler', unicode(self.course.id)) + self.course_details_url = reverse_course_url('settings_handler', six.text_type(self.course.id)) @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_CREDIT_ELIGIBILITY': False}) def test_course_details_with_disabled_setting(self): @@ -48,7 +51,7 @@ class CreditEligibilityTest(CourseTestCase): # verify that credit eligibility requirements block shows if the # course is set as credit course and it has eligibility requirements - credit_course = CreditCourse(course_key=unicode(self.course.id), enabled=True) + credit_course = CreditCourse(course_key=six.text_type(self.course.id), enabled=True) credit_course.save() self.assertEqual(len(get_credit_requirements(self.course.id)), 0) # test that after publishing course, minimum grade requirement is added diff --git a/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py b/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py index 0f66cbc37b..7e7a2dc84d 100644 --- a/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py +++ b/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py @@ -1,7 +1,11 @@ """ Test module for Entrance Exams AJAX callback handler workflows """ +from __future__ import absolute_import + import json + +import six from django.conf import settings from django.contrib.auth.models import User from django.test.client import RequestFactory @@ -38,15 +42,15 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): super(EntranceExamHandlerTests, self).setUp() self.course_key = self.course.id self.usage_key = self.course.location - self.course_url = '/course/{}'.format(unicode(self.course.id)) - self.exam_url = '/course/{}/entrance_exam/'.format(unicode(self.course.id)) + self.course_url = '/course/{}'.format(six.text_type(self.course.id)) + self.exam_url = '/course/{}/entrance_exam/'.format(six.text_type(self.course.id)) self.milestone_relationship_types = milestones_helpers.get_milestone_relationship_types() def test_entrance_exam_milestone_addition(self): """ Unit Test: test addition of entrance exam milestone content """ - parent_locator = unicode(self.course.location) + parent_locator = six.text_type(self.course.location) created_block = create_xblock( parent_locator=parent_locator, user=self.user, @@ -56,8 +60,8 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): ) add_entrance_exam_milestone(self.course.id, created_block) content_milestones = milestones_helpers.get_course_content_milestones( - unicode(self.course.id), - unicode(created_block.location), + six.text_type(self.course.id), + six.text_type(created_block.location), self.milestone_relationship_types['FULFILLS'] ) self.assertTrue(len(content_milestones)) @@ -67,7 +71,7 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): """ Unit Test: test removal of entrance exam milestone content """ - parent_locator = unicode(self.course.location) + parent_locator = six.text_type(self.course.location) created_block = create_xblock( parent_locator=parent_locator, user=self.user, @@ -77,8 +81,8 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): ) add_entrance_exam_milestone(self.course.id, created_block) content_milestones = milestones_helpers.get_course_content_milestones( - unicode(self.course.id), - unicode(created_block.location), + six.text_type(self.course.id), + six.text_type(created_block.location), self.milestone_relationship_types['FULFILLS'] ) self.assertEqual(len(content_milestones), 1) @@ -87,8 +91,8 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): request.user = user remove_entrance_exam_milestone_reference(request, self.course.id) content_milestones = milestones_helpers.get_course_content_milestones( - unicode(self.course.id), - unicode(created_block.location), + six.text_type(self.course.id), + six.text_type(created_block.location), self.milestone_relationship_types['FULFILLS'] ) self.assertEqual(len(content_milestones), 0) @@ -108,9 +112,9 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): self.assertTrue(metadata['entrance_exam_enabled']) self.assertIsNotNone(metadata['entrance_exam_minimum_score_pct']) self.assertIsNotNone(metadata['entrance_exam_id']['value']) - self.assertTrue(len(milestones_helpers.get_course_milestones(unicode(self.course.id)))) + self.assertTrue(len(milestones_helpers.get_course_milestones(six.text_type(self.course.id)))) content_milestones = milestones_helpers.get_course_content_milestones( - unicode(self.course.id), + six.text_type(self.course.id), metadata['entrance_exam_id']['value'], self.milestone_relationship_types['FULFILLS'] ) @@ -130,7 +134,7 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): # Add a new child sequential to the exam module # Confirm that the grader type is 'Entrance Exam' - chapter_locator_string = json.loads(resp.content).get('locator') + chapter_locator_string = json.loads(resp.content.decode('utf-8')).get('locator') # chapter_locator = UsageKey.from_string(chapter_locator_string) seq_data = { 'category': "sequential", @@ -138,7 +142,7 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): 'parent_locator': chapter_locator_string, } resp = self.client.ajax_post(reverse_url('xblock_handler'), seq_data) - seq_locator_string = json.loads(resp.content).get('locator') + seq_locator_string = json.loads(resp.content.decode('utf-8')).get('locator') seq_locator = UsageKey.from_string(seq_locator_string) section_grader_type = CourseGradingModel.get_section_grader_type(seq_locator) self.assertEqual(GRADER_TYPES['ENTRANCE_EXAM'], section_grader_type['graderType']) @@ -176,11 +180,11 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): ) user.set_password('test') user.save() - milestones = milestones_helpers.get_course_milestones(unicode(self.course_key)) + milestones = milestones_helpers.get_course_milestones(six.text_type(self.course_key)) self.assertEqual(len(milestones), 1) milestone_key = '{}.{}'.format(milestones[0]['namespace'], milestones[0]['name']) paths = milestones_helpers.get_course_milestones_fulfillment_paths( - unicode(self.course_key), + six.text_type(self.course_key), milestones_helpers.serialize_user(user) ) diff --git a/cms/djangoapps/contentstore/views/tests/test_gating.py b/cms/djangoapps/contentstore/views/tests/test_gating.py index 454cc17bcc..cc1d96818e 100644 --- a/cms/djangoapps/contentstore/views/tests/test_gating.py +++ b/cms/djangoapps/contentstore/views/tests/test_gating.py @@ -1,9 +1,12 @@ """ Unit tests for the gating feature in Studio """ +from __future__ import absolute_import + import json import ddt +import six from mock import patch from contentstore.tests.utils import CourseTestCase @@ -86,12 +89,13 @@ class TestSubsectionGating(CourseTestCase): self.client.ajax_post( self.seq2_url, - data={'prereqUsageKey': unicode(self.seq1.location), 'prereqMinScore': '100', 'prereqMinCompletion': '100'} + data={'prereqUsageKey': six.text_type(self.seq1.location), 'prereqMinScore': '100', + 'prereqMinCompletion': '100'} ) mock_set_required_content.assert_called_with( self.course.id, self.seq2.location, - unicode(self.seq1.location), + six.text_type(self.seq1.location), '100', '100' ) @@ -128,17 +132,17 @@ class TestSubsectionGating(CourseTestCase): mock_is_prereq, mock_get_required_content, mock_get_prereqs ): mock_is_prereq.return_value = True - mock_get_required_content.return_value = unicode(self.seq1.location), min_score, min_completion + mock_get_required_content.return_value = six.text_type(self.seq1.location), min_score, min_completion mock_get_prereqs.return_value = [ - {'namespace': '{}{}'.format(unicode(self.seq1.location), GATING_NAMESPACE_QUALIFIER)}, - {'namespace': '{}{}'.format(unicode(self.seq2.location), GATING_NAMESPACE_QUALIFIER)} + {'namespace': '{}{}'.format(six.text_type(self.seq1.location), GATING_NAMESPACE_QUALIFIER)}, + {'namespace': '{}{}'.format(six.text_type(self.seq2.location), GATING_NAMESPACE_QUALIFIER)} ] resp = json.loads(self.client.get_json(self.seq2_url).content) mock_is_prereq.assert_called_with(self.course.id, self.seq2.location) mock_get_required_content.assert_called_with(self.course.id, self.seq2.location) mock_get_prereqs.assert_called_with(self.course.id) self.assertTrue(resp['is_prereq']) - self.assertEqual(resp['prereq'], unicode(self.seq1.location)) + self.assertEqual(resp['prereq'], six.text_type(self.seq1.location)) self.assertEqual(resp['prereq_min_score'], min_score) self.assertEqual(resp['prereq_min_completion'], min_completion) self.assertEqual(resp['visibility_state'], VisibilityState.gated) diff --git a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py index 0bc4f14503..3c3ab85e6b 100644 --- a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py +++ b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py @@ -3,22 +3,26 @@ """ Group Configuration Tests. """ +from __future__ import absolute_import + import json from operator import itemgetter import ddt +import six from mock import patch +from six.moves import range -from contentstore.utils import reverse_course_url, reverse_usage_url -from contentstore.course_group_config import GroupConfiguration, CONTENT_GROUP_CONFIGURATION_NAME, ENROLLMENT_SCHEME +from contentstore.course_group_config import CONTENT_GROUP_CONFIGURATION_NAME, ENROLLMENT_SCHEME, GroupConfiguration from contentstore.tests.utils import CourseTestCase +from contentstore.utils import reverse_course_url, reverse_usage_url from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME -from xmodule.partitions.partitions import Group, UserPartition, ENROLLMENT_TRACK_PARTITION_ID -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from xmodule.validation import StudioValidation, StudioValidationMessage -from xmodule.modulestore.django import modulestore from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID, Group, UserPartition +from xmodule.validation import StudioValidation, StudioValidationMessage GROUP_CONFIGURATION_JSON = { u'name': u'Test name', @@ -160,7 +164,7 @@ class HelperMethods(object): i, 'Name ' + str(i), 'Description ' + str(i), [Group(0, 'Group A'), Group(1, 'Group B'), Group(2, 'Group C')], scheme=None, scheme_id=scheme_id - ) for i in xrange(count) + ) for i in range(count) ] self.course.user_partitions = partitions self.save_course() @@ -217,7 +221,7 @@ class GroupConfigurationsBaseTestCase(object): ) self.assertEqual(response.status_code, 400) self.assertNotIn("Location", response) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertIn("error", content) def test_invalid_json(self): @@ -236,7 +240,7 @@ class GroupConfigurationsBaseTestCase(object): ) self.assertEqual(response.status_code, 400) self.assertNotIn("Location", response) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertIn("error", content) @@ -305,7 +309,7 @@ class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurations ) self.assertEqual(response.status_code, 201) self.assertIn("Location", response) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) configuration_id, group_ids = self._remove_ids(content) # pylint: disable=unused-variable self.assertEqual(content, expected) # IDs are unique @@ -338,7 +342,7 @@ class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurations """ group_config = dict(GROUP_CONFIGURATION_JSON) group_config['scheme'] = scheme_id - group_config.setdefault('parameters', {})['course_id'] = unicode(self.course.id) + group_config.setdefault('parameters', {})['course_id'] = six.text_type(self.course.id) response = self.client.ajax_post( self._url(), data=group_config @@ -388,7 +392,7 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(content, expected) self.reload_course() @@ -429,7 +433,7 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(content, expected) self.reload_course() @@ -481,7 +485,7 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 400) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertTrue(content['error']) self.reload_course() # Verify that user_partitions and groups are still the same. @@ -534,7 +538,7 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(content, expected) self.reload_course() # Verify that user_partitions in the course contains the new group configuration. @@ -575,7 +579,7 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertEqual(content, expected) self.reload_course() @@ -623,7 +627,7 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 400) - content = json.loads(response.content) + content = json.loads(response.content.decode('utf-8')) self.assertTrue(content['error']) self.reload_course() # Verify that user_partitions is still the same. @@ -655,7 +659,7 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio """ group_config = dict(GROUP_CONFIGURATION_JSON) group_config['scheme'] = scheme_id - group_config.setdefault('parameters', {})['course_id'] = unicode(self.course.id) + group_config.setdefault('parameters', {})['course_id'] = six.text_type(self.course.id) response = self.client.ajax_post( self._url(), data=group_config @@ -673,7 +677,7 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio """ group_config = dict(GROUP_CONFIGURATION_JSON) group_config['scheme'] = scheme_id - group_config.setdefault('parameters', {})['course_id'] = unicode(self.course.id) + group_config.setdefault('parameters', {})['course_id'] = six.text_type(self.course.id) response = self.client.put( self._url(cid=partition_id), data=json.dumps(group_config), @@ -1112,10 +1116,10 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods): # This used to cause an exception since the code assumed that # only one partition would be available. actual = GroupConfiguration.get_partitions_usage_info(self.store, self.course) - self.assertEqual(actual.keys(), [0]) + self.assertEqual(list(actual.keys()), [0]) actual = GroupConfiguration.get_content_groups_items_usage_info(self.store, self.course) - self.assertEqual(actual.keys(), [0]) + self.assertEqual(list(actual.keys()), [0]) def test_can_handle_duplicate_group_ids(self): # Create the user partitions @@ -1150,14 +1154,14 @@ class GroupConfigurationsUsageInfoTestCase(CourseTestCase, HelperMethods): # This used to cause an exception since the code assumed that # only one partition would be available. actual = GroupConfiguration.get_partitions_usage_info(self.store, self.course) - self.assertEqual(actual.keys(), [0, 1]) - self.assertEqual(actual[0].keys(), [2]) - self.assertEqual(actual[1].keys(), [3]) + self.assertEqual(list(actual.keys()), [0, 1]) + self.assertEqual(list(actual[0].keys()), [2]) + self.assertEqual(list(actual[1].keys()), [3]) actual = GroupConfiguration.get_content_groups_items_usage_info(self.store, self.course) - self.assertEqual(actual.keys(), [0, 1]) - self.assertEqual(actual[0].keys(), [2]) - self.assertEqual(actual[1].keys(), [3]) + self.assertEqual(list(actual.keys()), [0, 1]) + self.assertEqual(list(actual[0].keys()), [2]) + self.assertEqual(list(actual[1].keys()), [3]) class GroupConfigurationsValidationTestCase(CourseTestCase, HelperMethods): diff --git a/cms/djangoapps/contentstore/views/tests/test_header_menu.py b/cms/djangoapps/contentstore/views/tests/test_header_menu.py index 9c8d7c6f11..a5334f97cb 100644 --- a/cms/djangoapps/contentstore/views/tests/test_header_menu.py +++ b/cms/djangoapps/contentstore/views/tests/test_header_menu.py @@ -3,6 +3,8 @@ """ Course Header Menu Tests. """ +from __future__ import absolute_import + from django.conf import settings from django.test.utils import override_settings diff --git a/cms/djangoapps/contentstore/views/tests/test_helpers.py b/cms/djangoapps/contentstore/views/tests/test_helpers.py index ace156d281..e141adcd62 100644 --- a/cms/djangoapps/contentstore/views/tests/test_helpers.py +++ b/cms/djangoapps/contentstore/views/tests/test_helpers.py @@ -2,6 +2,9 @@ Unit tests for helpers.py. """ +from __future__ import absolute_import + +import six from django.utils import http from contentstore.tests.utils import CourseTestCase @@ -17,7 +20,7 @@ class HelpersTestCase(CourseTestCase): def test_xblock_studio_url(self): # Verify course URL - course_url = u'/course/{}'.format(unicode(self.course.id)) + course_url = u'/course/{}'.format(six.text_type(self.course.id)) self.assertEqual(xblock_studio_url(self.course), course_url) # Verify chapter URL @@ -53,7 +56,7 @@ class HelpersTestCase(CourseTestCase): # Verify library URL library = LibraryFactory.create() - expected_url = u'/library/{}'.format(unicode(library.location.library_key)) + expected_url = u'/library/{}'.format(six.text_type(library.location.library_key)) self.assertEqual(xblock_studio_url(library), expected_url) def test_xblock_type_display_name(self): diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py index e2162d7840..cfe8577cf0 100644 --- a/cms/djangoapps/contentstore/views/tests/test_import_export.py +++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py @@ -1,19 +1,22 @@ """ Unit tests for course import and export """ +from __future__ import absolute_import + import copy import json import logging import os import re import shutil -import StringIO +from six import StringIO import tarfile import tempfile from uuid import uuid4 import ddt import lxml +import six from django.conf import settings from django.core.files.storage import FileSystemStorage from django.test.utils import override_settings @@ -21,6 +24,7 @@ from milestones.tests.utils import MilestonesTestCaseMixin from mock import Mock, patch from opaque_keys.edx.locator import LibraryLocator from path import Path as path +from six.moves import zip from storages.backends.s3boto import S3BotoStorage from user_tasks.models import UserTaskStatus @@ -103,7 +107,7 @@ class ImportEntranceExamTestCase(CourseTestCase, MilestonesTestCaseMixin): """ Check that pre existed entrance exam content should be overwrite with the imported course. """ - exam_url = '/course/{}/entrance_exam/'.format(unicode(self.course.id)) + exam_url = '/course/{}/entrance_exam/'.format(six.text_type(self.course.id)) resp = self.client.post(exam_url, {'entrance_exam_minimum_score_pct': 0.5}, http_accept='application/json') self.assertEqual(resp.status_code, 201) @@ -113,9 +117,9 @@ class ImportEntranceExamTestCase(CourseTestCase, MilestonesTestCaseMixin): self.assertTrue(metadata['entrance_exam_enabled']) self.assertIsNotNone(metadata['entrance_exam_minimum_score_pct']) self.assertEqual(metadata['entrance_exam_minimum_score_pct']['value'], 0.5) - self.assertTrue(len(milestones_helpers.get_course_milestones(unicode(self.course.id)))) + self.assertTrue(len(milestones_helpers.get_course_milestones(six.text_type(self.course.id)))) content_milestones = milestones_helpers.get_course_content_milestones( - unicode(self.course.id), + six.text_type(self.course.id), metadata['entrance_exam_id']['value'], milestones_helpers.get_milestone_relationship_types()['FULFILLS'] ) @@ -149,7 +153,7 @@ class ImportTestCase(CourseTestCase): def touch(name): """ Equivalent to shell's 'touch'""" - with file(name, 'a'): + with open(name, 'a'): # pylint: disable=W6005 os.utime(name, None) # Create tar test files ----------------------------------------------- @@ -349,7 +353,7 @@ class ImportTestCase(CourseTestCase): kwargs={'filename': os.path.split(tarpath)[1]} ) ) - status = json.loads(resp.content)["ImportStatus"] + status = json.loads(resp.content.decode('utf-8'))["ImportStatus"] self.assertEqual(status, -1) try_tar(self._fifo_tar()) @@ -562,7 +566,7 @@ class ExportTestCase(CourseTestCase): resp = self.client.post(self.url) self.assertEquals(resp.status_code, 200) resp = self.client.get(self.status_url) - result = json.loads(resp.content) + result = json.loads(resp.content.decode('utf-8')) status = result['ExportStatus'] self.assertEquals(status, 3) self.assertIn('ExportOutput', result) @@ -570,7 +574,7 @@ class ExportTestCase(CourseTestCase): resp = self.client.get(output_url) self._verify_export_succeeded(resp) - buff = StringIO.StringIO(b"".join(resp.streaming_content)) + buff = StringIO("".join(resp.streaming_content)) return tarfile.open(fileobj=buff) def _verify_export_succeeded(self, resp): @@ -669,7 +673,7 @@ class ExportTestCase(CourseTestCase): self.assertEquals(resp.status_code, 200) resp = self.client.get(self.status_url) self.assertEquals(resp.status_code, 200) - result = json.loads(resp.content) + result = json.loads(resp.content.decode('utf-8')) self.assertNotIn('ExportOutput', result) self.assertIn('ExportError', result) error = result['ExportError'] @@ -757,7 +761,7 @@ class ExportTestCase(CourseTestCase): """ resp = self.client.get(self.status_url) self.assertEqual(resp.status_code, 200) - result = json.loads(resp.content) + result = json.loads(resp.content.decode('utf-8')) self.assertEqual(result['ExportStatus'], 0) def test_output_non_course_author(self): @@ -796,7 +800,7 @@ class ExportTestCase(CourseTestCase): file_url='/path/to/testfile.tar.gz', ) resp = self.client.get(self.status_url) - result = json.loads(resp.content) + result = json.loads(resp.content.decode('utf-8')) self.assertEqual(result['ExportOutput'], '/path/to/testfile.tar.gz') @patch('contentstore.views.import_export._latest_task_status') @@ -816,7 +820,7 @@ class ExportTestCase(CourseTestCase): file_url='/s3/file/path/testfile.tar.gz', ) resp = self.client.get(self.status_url) - result = json.loads(resp.content) + result = json.loads(resp.content.decode('utf-8')) self.assertEqual(result['ExportOutput'], '/s3/file/path/testfile.tar.gz') @patch('contentstore.views.import_export._latest_task_status') @@ -833,7 +837,7 @@ class ExportTestCase(CourseTestCase): mock_latest_task_status.return_value = Mock(state=UserTaskStatus.SUCCEEDED) mock_get_user_task_artifact.return_value = self._mock_artifact(spec=FileSystemStorage) resp = self.client.get(self.status_url) - result = json.loads(resp.content) + result = json.loads(resp.content.decode('utf-8')) file_export_output_url = reverse_course_url('export_output_handler', self.course.id) self.assertEqual(result['ExportOutput'], file_export_output_url) diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 0a46d5ffb3..0b15e57a2b 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -1,13 +1,17 @@ """Tests for items views.""" +from __future__ import absolute_import + import json +import re from datetime import datetime, timedelta import ddt +import six from django.conf import settings -from django.urls import reverse from django.http import Http404 from django.test import TestCase from django.test.client import RequestFactory +from django.urls import reverse from mock import Mock, PropertyMock, patch from opaque_keys import InvalidKeyError from opaque_keys.edx.asides import AsideUsageKeyV2 @@ -16,6 +20,7 @@ from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator from pyquery import PyQuery from pytz import UTC from six import text_type +from six.moves import range from web_fragments.fragment import Fragment from webob import Response from xblock.core import XBlockAside @@ -36,7 +41,7 @@ from contentstore.views.item import ( _xblock_type_and_display_name, add_container_page_publishing_info, create_xblock_info, - highlights_setting, + highlights_setting ) from lms_xblock.mixin import NONSENSICAL_ACCESS_RESTRICTION from student.tests.factories import UserFactory @@ -97,7 +102,7 @@ class ItemTest(CourseTestCase): Get the UsageKey from the response payload and verify that the status_code was 200. :param response: """ - parsed = json.loads(response.content) + parsed = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) key = UsageKey.from_string(parsed['locator']) if key.course_key.run is None: @@ -106,7 +111,9 @@ class ItemTest(CourseTestCase): def create_xblock(self, parent_usage_key=None, display_name=None, category=None, boilerplate=None): data = { - 'parent_locator': unicode(self.usage_key) if parent_usage_key is None else unicode(parent_usage_key), + 'parent_locator': six.text_type( + self.usage_key + )if parent_usage_key is None else six.text_type(parent_usage_key), 'category': category } if display_name is not None: @@ -141,7 +148,7 @@ class GetItemTest(ItemTest): """ resp = self._get_preview(usage_key, data) self.assertEqual(resp.status_code, 200) - resp_content = json.loads(resp.content) + resp_content = json.loads(resp.content.decode('utf-8')) html = resp_content['html'] self.assertTrue(html) resources = resp_content['resources'] @@ -249,8 +256,8 @@ class GetItemTest(ItemTest): html, # The instance of the wrapper class will have an auto-generated ID. Allow any # characters after wrapper. - ur'"/container/{}" class="action-button">\s*View'.format( - wrapper_usage_key + u'"/container/{}" class="action-button">\\s*View'.format( + re.escape(six.text_type(wrapper_usage_key)) ) ) @@ -377,7 +384,7 @@ class GetItemTest(ItemTest): self.assertEqual(resp.status_code, 200) # Check that the partition and group information was returned - result = json.loads(resp.content) + result = json.loads(resp.content.decode('utf-8')) self.assertEqual(result["user_partitions"], [ { "id": ENROLLMENT_TRACK_PARTITION_ID, @@ -447,7 +454,7 @@ class GetItemTest(ItemTest): xblock (XBlock): An XBlock item. xblock_info (dict): A dict containing xblock information. """ - self.assertEqual(unicode(xblock.location), xblock_info['id']) + self.assertEqual(six.text_type(xblock.location), xblock_info['id']) self.assertEqual(xblock.display_name, xblock_info['display_name']) self.assertEqual(xblock.category, xblock_info['category']) @@ -456,7 +463,7 @@ class GetItemTest(ItemTest): url = reverse_usage_url('xblock_handler', usage_key) + '?fields={field_type}'.format(field_type=field_type) response = self.client.get(url) self.assertEqual(response.status_code, 200) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) if field_type == 'ancestorInfo': self.assertIn('ancestors', response) for ancestor_info in response['ancestors']: @@ -591,21 +598,21 @@ class DuplicateHelper(object): self.assertEqual(duplicated_asides[0].field13, 'aside1_default_value3') self.assertNotEqual( - unicode(original_item.location), - unicode(duplicated_item.location), + six.text_type(original_item.location), + six.text_type(duplicated_item.location), "Location of duplicate should be different from original" ) # Parent will only be equal for root of duplicated structure, in the case # where an item is duplicated in-place. - if parent_usage_key and unicode(original_item.parent) == unicode(parent_usage_key): + if parent_usage_key and six.text_type(original_item.parent) == six.text_type(parent_usage_key): self.assertEqual( - unicode(parent_usage_key), unicode(duplicated_item.parent), + six.text_type(parent_usage_key), six.text_type(duplicated_item.parent), "Parent of duplicate should equal parent of source for root xblock when duplicated in-place" ) else: self.assertNotEqual( - unicode(original_item.parent), unicode(duplicated_item.parent), + six.text_type(original_item.parent), six.text_type(duplicated_item.parent), "Parent duplicate should be different from source" ) @@ -622,7 +629,7 @@ class DuplicateHelper(object): len(duplicated_item.children), "Duplicated item differs in number of children" ) - for i in xrange(len(original_item.children)): + for i in range(len(original_item.children)): if not self._check_equality(original_item.children[i], duplicated_item.children[i], is_child=True): return False duplicated_item.children = original_item.children @@ -650,8 +657,8 @@ class DuplicateHelper(object): """ # pylint: disable=no-member data = { - 'parent_locator': unicode(parent_usage_key), - 'duplicate_source_locator': unicode(source_usage_key) + 'parent_locator': six.text_type(parent_usage_key), + 'duplicate_source_locator': six.text_type(source_usage_key) } if display_name is not None: data['display_name'] = display_name @@ -862,8 +869,8 @@ class TestMoveItem(ItemTest): resp (JsonResponse): Response after the move operation is complete. """ data = { - 'move_source_locator': unicode(source_usage_key), - 'parent_locator': unicode(target_usage_key) + 'move_source_locator': six.text_type(source_usage_key), + 'parent_locator': six.text_type(target_usage_key) } if target_index is not None: data['target_index'] = target_index @@ -889,9 +896,9 @@ class TestMoveItem(ItemTest): expected_index = target_index if target_index is not None else source_index response = self._move_component(source_usage_key, target_usage_key, target_index) self.assertEqual(response.status_code, 200) - response = json.loads(response.content) - self.assertEqual(response['move_source_locator'], unicode(source_usage_key)) - self.assertEqual(response['parent_locator'], unicode(target_usage_key)) + response = json.loads(response.content.decode('utf-8')) + self.assertEqual(response['move_source_locator'], six.text_type(source_usage_key)) + self.assertEqual(response['parent_locator'], six.text_type(target_usage_key)) self.assertEqual(response['source_index'], expected_index) # Verify parent referance has been changed now. @@ -952,7 +959,7 @@ class TestMoveItem(ItemTest): # Move component and verify that response contains initial index response = self._move_component(self.html_usage_key, self.vert2_usage_key) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) self.assertEquals(original_index, response['source_index']) # Verify that new parent has the moved component at the last index. @@ -965,7 +972,7 @@ class TestMoveItem(ItemTest): # Undo Move to the original index, use the source index fetched from the response. response = self._move_component(self.html_usage_key, self.vert_usage_key, response['source_index']) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) self.assertEquals(original_index, response['source_index']) def test_move_large_target_index(self): @@ -976,7 +983,7 @@ class TestMoveItem(ItemTest): parent_children_length = len(parent.children) response = self._move_component(self.html_usage_key, self.vert2_usage_key, parent_children_length + 10) self.assertEqual(response.status_code, 400) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) expected_error = u'You can not move {usage_key} at an invalid index ({target_index}).'.format( usage_key=self.html_usage_key, @@ -993,7 +1000,7 @@ class TestMoveItem(ItemTest): parent_loc = self.store.get_parent_location(self.html_usage_key) response = self._move_component(self.html_usage_key, self.seq_usage_key) self.assertEqual(response.status_code, 400) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) expected_error = u'You can not move {source_type} into {target_type}.'.format( source_type=self.html_usage_key.block_type, @@ -1011,7 +1018,7 @@ class TestMoveItem(ItemTest): self.assertEqual(parent_loc, self.vert_usage_key) response = self._move_component(self.html_usage_key, self.vert_usage_key) self.assertEqual(response.status_code, 400) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) self.assertEqual(response['error'], 'Item is already present in target location.') self.assertEqual(self.store.get_parent_location(self.html_usage_key), parent_loc) @@ -1028,7 +1035,7 @@ class TestMoveItem(ItemTest): self.assertEqual(parent_loc, self.vert_usage_key) response = self._move_component(library_content_usage_key, library_content_usage_key) self.assertEqual(response.status_code, 400) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) self.assertEqual(response['error'], 'You can not move an item into itself.') self.assertEqual(self.store.get_parent_location(self.html_usage_key), parent_loc) @@ -1093,7 +1100,7 @@ class TestMoveItem(ItemTest): self.setup_and_verify_content_experiment(0) response = self._move_component(self.html_usage_key, self.split_test_usage_key) self.assertEqual(response.status_code, 400) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) self.assertEqual(response['error'], 'You can not move an item directly into content experiment.') self.assertEqual(self.store.get_parent_location(self.html_usage_key), self.vert_usage_key) @@ -1108,7 +1115,7 @@ class TestMoveItem(ItemTest): for child_vert_usage_key in split_test.children: response = self._move_component(self.split_test_usage_key, child_vert_usage_key) self.assertEqual(response.status_code, 400) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) self.assertEqual(response['error'], 'You can not move an item into it\'s child.') self.assertEqual(self.store.get_parent_location(self.split_test_usage_key), self.vert_usage_key) @@ -1125,7 +1132,7 @@ class TestMoveItem(ItemTest): # Try to move content experiment further down the level to a child group A nested inside main group A. response = self._move_component(self.split_test_usage_key, child_split_test.children[0]) self.assertEqual(response.status_code, 400) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) self.assertEqual(response['error'], 'You can not move an item into it\'s child.') self.assertEqual(self.store.get_parent_location(self.split_test_usage_key), self.vert_usage_key) @@ -1138,7 +1145,7 @@ class TestMoveItem(ItemTest): parent_loc = self.store.get_parent_location(self.html_usage_key) response = self._move_component(self.html_usage_key, self.vert2_usage_key, target_index) self.assertEqual(response.status_code, 400) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) error = u'You must provide target_index ({target_index}) as an integer.'.format(target_index=target_index) self.assertEqual(response['error'], error) @@ -1149,7 +1156,7 @@ class TestMoveItem(ItemTest): """ Test move an item without specifying the target location. """ - data = {'move_source_locator': unicode(self.html_usage_key)} + data = {'move_source_locator': six.text_type(self.html_usage_key)} with self.assertRaises(InvalidKeyError): self.client.patch( reverse('xblock_handler'), @@ -1165,7 +1172,7 @@ class TestMoveItem(ItemTest): reverse('xblock_handler') ) self.assertEqual(response.status_code, 400) - response = json.loads(response.content) + response = json.loads(response.content.decode('utf-8')) self.assertEqual(response['error'], 'Patch request did not recognise any parameters to handle.') def _verify_validation_message(self, message, expected_message, expected_message_type): @@ -1238,9 +1245,9 @@ class TestMoveItem(ItemTest): self.assert_move_item(self.html_usage_key, self.vert2_usage_key, insert_at) mock_logger.info.assert_called_with( u'MOVE: %s moved from %s to %s at %d index', - unicode(self.html_usage_key), - unicode(self.vert_usage_key), - unicode(self.vert2_usage_key), + six.text_type(self.html_usage_key), + six.text_type(self.vert_usage_key), + six.text_type(self.vert2_usage_key), insert_at ) @@ -1308,8 +1315,8 @@ class TestMoveItem(ItemTest): self.setup_course(default_store=store_type) data = { - 'move_source_locator': unicode(self.usage_key.course_key.make_usage_key('html', 'html_test')), - 'parent_locator': unicode(self.vert2_usage_key) + 'move_source_locator': six.text_type(self.usage_key.course_key.make_usage_key('html', 'html_test')), + 'parent_locator': six.text_type(self.vert2_usage_key) } with self.assertRaises(ItemNotFoundError): self.client.patch( @@ -1554,7 +1561,13 @@ class TestEditItem(TestEditItemSetup): resp = self.client.ajax_post( self.seq_update_url, - data={'children': [unicode(self.problem_usage_key), unicode(unit2_usage_key), unicode(unit1_usage_key)]} + data={ + 'children': [ + six.text_type(self.problem_usage_key), + six.text_type(unit2_usage_key), + six.text_type(unit1_usage_key) + ] + } ) self.assertEqual(resp.status_code, 200) @@ -1577,7 +1590,7 @@ class TestEditItem(TestEditItemSetup): # move unit 1 from sequential1 to sequential2 resp = self.client.ajax_post( self.seq2_update_url, - data={'children': [unicode(unit_1_key), unicode(unit_2_key)]} + data={'children': [six.text_type(unit_1_key), six.text_type(unit_2_key)]} ) self.assertEqual(resp.status_code, 200) @@ -1600,7 +1613,7 @@ class TestEditItem(TestEditItemSetup): # adding orphaned unit 1 should return an error resp = self.client.ajax_post( self.seq2_update_url, - data={'children': [unicode(unit_1_key)]} + data={'children': [six.text_type(unit_1_key)]} ) self.assertEqual(resp.status_code, 400) self.assertIn("Invalid data, possibly caused by concurrent authors", resp.content) @@ -1625,7 +1638,7 @@ class TestEditItem(TestEditItemSetup): # remove unit 2 should return an error resp = self.client.ajax_post( self.seq2_update_url, - data={'children': [unicode(unit_1_key)]} + data={'children': [six.text_type(unit_1_key)]} ) self.assertEqual(resp.status_code, 400) self.assertIn("Invalid data, possibly caused by concurrent authors", resp.content) @@ -1793,7 +1806,7 @@ class TestEditItem(TestEditItemSetup): self.client.ajax_post( self.problem_update_url, data={ - 'id': unicode(self.problem_usage_key), + 'id': six.text_type(self.problem_usage_key), 'metadata': {}, 'data': "

Problem content draft.

" } @@ -1848,7 +1861,7 @@ class TestEditItem(TestEditItemSetup): resp = self.client.ajax_post( unit_update_url, data={ - 'id': unicode(unit_usage_key), + 'id': six.text_type(unit_usage_key), 'metadata': {}, } ) @@ -1868,14 +1881,14 @@ class TestEditItem(TestEditItemSetup): response = self.client.ajax_post( update_url, data={ - 'id': unicode(video_usage_key), + 'id': six.text_type(video_usage_key), 'metadata': { 'saved_video_position': "Not a valid relative time", }, } ) self.assertEqual(response.status_code, 400) - parsed = json.loads(response.content) + parsed = json.loads(response.content.decode('utf-8')) self.assertIn("error", parsed) self.assertIn("Incorrect RelativeTime value", parsed["error"]) # See xmodule/fields.py @@ -1894,10 +1907,10 @@ class TestEditItemSplitMongo(TestEditItemSetup): """ view_url = reverse_usage_url("xblock_view_handler", self.problem_usage_key, {"view_name": STUDIO_VIEW}) - for __ in xrange(3): + for __ in range(3): resp = self.client.get(view_url, HTTP_ACCEPT='application/json') self.assertEqual(resp.status_code, 200) - content = json.loads(resp.content) + content = json.loads(resp.content.decode('utf-8')) self.assertEqual(len(PyQuery(content['html'])('.xblock-{}'.format(STUDIO_VIEW))), 1) @@ -1910,8 +1923,8 @@ class TestEditSplitModule(ItemTest): super(TestEditSplitModule, self).setUp() self.user = UserFactory() - self.first_user_partition_group_1 = Group(unicode(MINIMUM_STATIC_PARTITION_ID + 1), 'alpha') - self.first_user_partition_group_2 = Group(unicode(MINIMUM_STATIC_PARTITION_ID + 2), 'beta') + self.first_user_partition_group_1 = Group(six.text_type(MINIMUM_STATIC_PARTITION_ID + 1), 'alpha') + self.first_user_partition_group_2 = Group(six.text_type(MINIMUM_STATIC_PARTITION_ID + 2), 'beta') self.first_user_partition = UserPartition( MINIMUM_STATIC_PARTITION_ID, 'first_partition', 'First Partition', [self.first_user_partition_group_1, self.first_user_partition_group_2] @@ -1919,9 +1932,9 @@ class TestEditSplitModule(ItemTest): # There is a test point below (test_create_groups) that purposefully wants the group IDs # of the 2 partitions to overlap (which is not something that normally happens). - self.second_user_partition_group_1 = Group(unicode(MINIMUM_STATIC_PARTITION_ID + 1), 'Group 1') - self.second_user_partition_group_2 = Group(unicode(MINIMUM_STATIC_PARTITION_ID + 2), 'Group 2') - self.second_user_partition_group_3 = Group(unicode(MINIMUM_STATIC_PARTITION_ID + 3), 'Group 3') + self.second_user_partition_group_1 = Group(six.text_type(MINIMUM_STATIC_PARTITION_ID + 1), 'Group 1') + self.second_user_partition_group_2 = Group(six.text_type(MINIMUM_STATIC_PARTITION_ID + 2), 'Group 2') + self.second_user_partition_group_3 = Group(six.text_type(MINIMUM_STATIC_PARTITION_ID + 3), 'Group 3') self.second_user_partition = UserPartition( MINIMUM_STATIC_PARTITION_ID + 10, 'second_partition', 'Second Partition', [ @@ -1989,8 +2002,8 @@ class TestEditSplitModule(ItemTest): vertical_1 = self.get_item_from_modulestore(split_test.children[1], verify_is_draft=True) self.assertEqual("vertical", vertical_0.category) self.assertEqual("vertical", vertical_1.category) - self.assertEqual("Group ID " + unicode(MINIMUM_STATIC_PARTITION_ID + 1), vertical_0.display_name) - self.assertEqual("Group ID " + unicode(MINIMUM_STATIC_PARTITION_ID + 2), vertical_1.display_name) + self.assertEqual("Group ID " + six.text_type(MINIMUM_STATIC_PARTITION_ID + 1), vertical_0.display_name) + self.assertEqual("Group ID " + six.text_type(MINIMUM_STATIC_PARTITION_ID + 2), vertical_1.display_name) # Verify that the group_id_to_child mapping is correct. self.assertEqual(2, len(split_test.group_id_to_child)) @@ -2264,8 +2277,8 @@ class TestComponentTemplates(CourseTestCase): """ self._verify_basic_component("discussion", "Discussion") self._verify_basic_component("video", "Video") - self.assertGreater(self.get_templates_of_type('html'), 0) - self.assertGreater(self.get_templates_of_type('problem'), 0) + self.assertGreater(len(self.get_templates_of_type('html')), 0) + self.assertGreater(len(self.get_templates_of_type('problem')), 0) self.assertIsNone(self.get_templates_of_type('advanced')) # Now fully disable video through XBlockConfiguration @@ -2483,7 +2496,7 @@ class TestXBlockInfo(ItemTest): def test_json_responses(self): outline_url = reverse_usage_url('xblock_outline_handler', self.usage_key) resp = self.client.get(outline_url, HTTP_ACCEPT='application/json') - json_response = json.loads(resp.content) + json_response = json.loads(resp.content.decode('utf-8')) self.validate_course_xblock_info(json_response, course_outline=True) @ddt.data( @@ -2664,7 +2677,7 @@ class TestXBlockInfo(ItemTest): Validate that the xblock info is correct for the test course. """ self.assertEqual(xblock_info['category'], 'course') - self.assertEqual(xblock_info['id'], unicode(self.course.location)) + self.assertEqual(xblock_info['id'], six.text_type(self.course.location)) self.assertEqual(xblock_info['display_name'], self.course.display_name) self.assertTrue(xblock_info['published']) self.assertFalse(xblock_info['highlights_enabled_for_messaging']) @@ -2677,7 +2690,7 @@ class TestXBlockInfo(ItemTest): Validate that the xblock info is correct for the test chapter. """ self.assertEqual(xblock_info['category'], 'chapter') - self.assertEqual(xblock_info['id'], unicode(self.chapter.location)) + self.assertEqual(xblock_info['id'], six.text_type(self.chapter.location)) self.assertEqual(xblock_info['display_name'], 'Week 1') self.assertTrue(xblock_info['published']) self.assertIsNone(xblock_info.get('edited_by', None)) @@ -2697,7 +2710,7 @@ class TestXBlockInfo(ItemTest): Validate that the xblock info is correct for the test sequential. """ self.assertEqual(xblock_info['category'], 'sequential') - self.assertEqual(xblock_info['id'], unicode(self.sequential.location)) + self.assertEqual(xblock_info['id'], six.text_type(self.sequential.location)) self.assertEqual(xblock_info['display_name'], 'Lesson 1') self.assertTrue(xblock_info['published']) self.assertIsNone(xblock_info.get('edited_by', None)) @@ -2710,7 +2723,7 @@ class TestXBlockInfo(ItemTest): Validate that the xblock info is correct for the test vertical. """ self.assertEqual(xblock_info['category'], 'vertical') - self.assertEqual(xblock_info['id'], unicode(self.vertical.location)) + self.assertEqual(xblock_info['id'], six.text_type(self.vertical.location)) self.assertEqual(xblock_info['display_name'], 'Unit 1') self.assertTrue(xblock_info['published']) self.assertEqual(xblock_info['edited_by'], 'testuser') @@ -2732,7 +2745,7 @@ class TestXBlockInfo(ItemTest): Validate that the xblock info is correct for the test component. """ self.assertEqual(xblock_info['category'], 'video') - self.assertEqual(xblock_info['id'], unicode(self.video.location)) + self.assertEqual(xblock_info['id'], six.text_type(self.video.location)) self.assertEqual(xblock_info['display_name'], 'My Video') self.assertTrue(xblock_info['published']) self.assertIsNone(xblock_info.get('edited_by', None)) @@ -2849,7 +2862,7 @@ class TestLibraryXBlockInfo(ModuleStoreTestCase): ancestors = xblock_info['ancestor_info']['ancestors'] self.assertEqual(len(ancestors), 2) self.assertEqual(ancestors[0]['category'], 'vertical') - self.assertEqual(ancestors[0]['id'], unicode(self.vertical.location)) + self.assertEqual(ancestors[0]['id'], six.text_type(self.vertical.location)) self.assertEqual(ancestors[1]['category'], 'library') def validate_component_xblock_info(self, xblock_info, original_block): @@ -2857,7 +2870,7 @@ class TestLibraryXBlockInfo(ModuleStoreTestCase): Validate that the xblock info is correct for the test component. """ self.assertEqual(xblock_info['category'], original_block.category) - self.assertEqual(xblock_info['id'], unicode(original_block.location)) + self.assertEqual(xblock_info['id'], six.text_type(original_block.location)) self.assertEqual(xblock_info['display_name'], original_block.display_name) self.assertIsNone(xblock_info.get('has_changes', None)) self.assertIsNone(xblock_info.get('published', None)) diff --git a/cms/djangoapps/contentstore/views/tests/test_library.py b/cms/djangoapps/contentstore/views/tests/test_library.py index 042153004d..130db0b5ea 100644 --- a/cms/djangoapps/contentstore/views/tests/test_library.py +++ b/cms/djangoapps/contentstore/views/tests/test_library.py @@ -3,12 +3,15 @@ Unit tests for contentstore.views.library More important high-level tests are in contentstore/tests/test_libraries.py """ +from __future__ import absolute_import + import ddt import mock from django.conf import settings from mock import patch from opaque_keys.edx.locator import CourseKey, LibraryLocator from six import binary_type, text_type +from six.moves import range from contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase, parse_json from contentstore.utils import reverse_course_url, reverse_library_url diff --git a/cms/djangoapps/contentstore/views/tests/test_organizations.py b/cms/djangoapps/contentstore/views/tests/test_organizations.py index d8894d51fc..17f4f77f43 100644 --- a/cms/djangoapps/contentstore/views/tests/test_organizations.py +++ b/cms/djangoapps/contentstore/views/tests/test_organizations.py @@ -1,8 +1,10 @@ """Tests covering the Organizations listing on the Studio home.""" +from __future__ import absolute_import + import json -from django.urls import reverse from django.test import TestCase +from django.urls import reverse from mock import patch from student.tests.factories import UserFactory @@ -30,5 +32,5 @@ class TestOrganizationListing(TestCase): """Verify that the organization names list api returns list of organization short names.""" response = self.client.get(self.org_names_listing_url, HTTP_ACCEPT='application/json') self.assertEqual(response.status_code, 200) - org_names = json.loads(response.content) + org_names = json.loads(response.content.decode('utf-8')) self.assertEqual(org_names, self.org_short_names) diff --git a/cms/djangoapps/contentstore/views/tests/test_preview.py b/cms/djangoapps/contentstore/views/tests/test_preview.py index 07d1a208ae..4aa284ecd9 100644 --- a/cms/djangoapps/contentstore/views/tests/test_preview.py +++ b/cms/djangoapps/contentstore/views/tests/test_preview.py @@ -1,10 +1,13 @@ """ Tests for contentstore.views.preview.py """ +from __future__ import absolute_import + import re import ddt import mock +import six from django.test.client import Client, RequestFactory from xblock.core import XBlock, XBlockAside @@ -56,7 +59,9 @@ class GetPreviewHtmlTestCase(ModuleStoreTestCase): html = get_preview_fragment(request, html, context).content # Verify student view html is returned, and the usage ID is as expected. - html_pattern = re.escape(unicode(course.id.make_usage_key('html', 'replaceme'))).replace('replaceme', r'html_[0-9]*') + html_pattern = re.escape( + six.text_type(course.id.make_usage_key('html', 'replaceme')) + ).replace('replaceme', r'html_[0-9]*') self.assertRegexpMatches( html, 'data-usage-id="{}"'.format(html_pattern) diff --git a/cms/djangoapps/contentstore/views/tests/test_tabs.py b/cms/djangoapps/contentstore/views/tests/test_tabs.py index 166971c82c..343d12653a 100644 --- a/cms/djangoapps/contentstore/views/tests/test_tabs.py +++ b/cms/djangoapps/contentstore/views/tests/test_tabs.py @@ -1,5 +1,7 @@ """ Tests for tab functions (just primitive). """ +from __future__ import absolute_import + import json from contentstore.tests.utils import CourseTestCase @@ -36,7 +38,7 @@ class TabsPageTests(CourseTestCase): """Verify response is an error listing the invalid_tab_id""" self.assertEqual(resp.status_code, 400) - resp_content = json.loads(resp.content) + resp_content = json.loads(resp.content.decode('utf-8')) self.assertIn("error", resp_content) self.assertIn("invalid_tab_id", resp_content['error']) @@ -118,7 +120,7 @@ class TabsPageTests(CourseTestCase): data={'tabs': [{'tab_id': tab_id} for tab_id in tab_ids]}, ) self.assertEqual(resp.status_code, 400) - resp_content = json.loads(resp.content) + resp_content = json.loads(resp.content.decode('utf-8')) self.assertIn("error", resp_content) def test_reorder_tabs_invalid_tab(self): @@ -182,7 +184,7 @@ class TabsPageTests(CourseTestCase): resp = self.client.get(preview_url, HTTP_ACCEPT='application/json') self.assertEqual(resp.status_code, 200) - resp_content = json.loads(resp.content) + resp_content = json.loads(resp.content.decode('utf-8')) html = resp_content['html'] # Verify that the HTML contains the expected elements diff --git a/cms/djangoapps/contentstore/views/tests/test_textbooks.py b/cms/djangoapps/contentstore/views/tests/test_textbooks.py index 5b6076f49d..53f6b8487d 100644 --- a/cms/djangoapps/contentstore/views/tests/test_textbooks.py +++ b/cms/djangoapps/contentstore/views/tests/test_textbooks.py @@ -1,3 +1,7 @@ +""" Test cases for the textbook index page. """ + +from __future__ import absolute_import + import json from unittest import TestCase @@ -30,7 +34,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content) + obj = json.loads(resp.content.decode('utf-8')) self.assertEqual(self.course.pdf_textbooks, obj) def test_view_index_xhr_content(self): @@ -63,7 +67,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) self.assertEqual(resp.status_code, 200) - obj = json.loads(resp.content) + obj = json.loads(resp.content.decode('utf-8')) self.assertEqual(content, obj) @@ -100,7 +104,7 @@ class TextbookIndexTestCase(CourseTestCase): HTTP_X_REQUESTED_WITH='XMLHttpRequest' ) self.assertEqual(resp.status_code, 400) - obj = json.loads(resp.content) + obj = json.loads(resp.content.decode('utf-8')) self.assertIn("error", obj) @@ -131,7 +135,7 @@ class TextbookCreateTestCase(CourseTestCase): ) self.assertEqual(resp.status_code, 201) self.assertIn("Location", resp) - textbook = json.loads(resp.content) + textbook = json.loads(resp.content.decode('utf-8')) self.assertIn("id", textbook) del textbook["id"] self.assertEqual(self.textbook, textbook) @@ -147,7 +151,7 @@ class TextbookCreateTestCase(CourseTestCase): HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(resp.status_code, 201) - textbook = json.loads(resp.content) + textbook = json.loads(resp.content.decode('utf-8')) self.assertEqual(self.textbook, textbook) def test_invalid_id(self): @@ -209,14 +213,14 @@ class TextbookDetailTestCase(CourseTestCase): "Get the first textbook" resp = self.client.get(self.url1) self.assertEqual(resp.status_code, 200) - compare = json.loads(resp.content) + compare = json.loads(resp.content.decode('utf-8')) self.assertEqual(compare, self.textbook1) def test_get_2(self): "Get the second textbook" resp = self.client.get(self.url2) self.assertEqual(resp.status_code, 200) - compare = json.loads(resp.content) + compare = json.loads(resp.content.decode('utf-8')) self.assertEqual(compare, self.textbook2) def test_get_nonexistant(self): diff --git a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py index f2f7ef9f20..b89e7992a0 100644 --- a/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py +++ b/cms/djangoapps/contentstore/views/tests/test_transcript_settings.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import + import json from io import BytesIO import ddt +import six from django.test.testcases import TestCase from django.urls import reverse from edxval import api @@ -103,7 +106,7 @@ class TranscriptCredentialsTest(CourseTestCase): content_type='application/json' ) self.assertEqual(response.status_code, expected_status_code) - self.assertEqual(response.content, expected_response) + self.assertEqual(response.content.decode('utf-8'), expected_response) @ddt.ddt @@ -240,8 +243,8 @@ class TranscriptDownloadTest(CourseTestCase): # Assert the actual response self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, expected_content) - for attribute, value in expected_headers.iteritems(): + self.assertEqual(response.content.decode('utf-8'), expected_content) + for attribute, value in six.iteritems(expected_headers): self.assertEqual(response.get(attribute), value) @ddt.data( @@ -267,7 +270,7 @@ class TranscriptDownloadTest(CourseTestCase): 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) + self.assertEqual(json.loads(response.content.decode('utf-8'))['error'], expected_error_message) @ddt.ddt @@ -369,7 +372,7 @@ class TranscriptUploadTest(CourseTestCase): # Make request to transcript upload handler 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) + self.assertEqual(json.loads(response.content.decode('utf-8'))['error'], expected_error_message) @patch('contentstore.views.transcript_settings.get_available_transcript_languages', Mock(return_value=['en', 'es'])) def test_transcript_upload_handler_existing_transcript(self): @@ -386,7 +389,7 @@ class TranscriptUploadTest(CourseTestCase): response = self.client.post(self.view_url, request_payload, format='multipart') self.assertEqual(response.status_code, 400) self.assertEqual( - json.loads(response.content)['error'], + json.loads(response.content.decode('utf-8'))['error'], u'A transcript with the "es" language code already exists.' ) @@ -410,7 +413,7 @@ class TranscriptUploadTest(CourseTestCase): self.assertEqual(response.status_code, 400) self.assertEqual( - json.loads(response.content)['error'], + json.loads(response.content.decode('utf-8'))['error'], u'There is a problem with this transcript file. Try to upload a different file.' ) @@ -434,7 +437,7 @@ class TranscriptUploadTest(CourseTestCase): self.assertEqual(response.status_code, 400) self.assertEqual( - json.loads(response.content)['error'], + json.loads(response.content.decode('utf-8'))['error'], u'There is a problem with this transcript file. Try to upload a different file.' ) diff --git a/cms/djangoapps/contentstore/views/tests/test_transcripts.py b/cms/djangoapps/contentstore/views/tests/test_transcripts.py index 05e03aebd6..f2f5ce4862 100644 --- a/cms/djangoapps/contentstore/views/tests/test_transcripts.py +++ b/cms/djangoapps/contentstore/views/tests/test_transcripts.py @@ -1,5 +1,7 @@ """Tests for items views.""" +from __future__ import absolute_import + import copy import json import tempfile @@ -8,6 +10,7 @@ from codecs import BOM_UTF8 from uuid import uuid4 import ddt +import six from django.conf import settings from django.test.utils import override_settings from django.urls import reverse @@ -21,17 +24,18 @@ 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 VideoBlock from xmodule.video_module.transcripts_utils import ( GetTranscriptsFromYouTubeException, Transcript, get_video_transcript_content, - remove_subs_from_store, + remove_subs_from_store ) TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex -SRT_TRANSCRIPT_CONTENT = """0 +SRT_TRANSCRIPT_CONTENT = b"""0 00:00:10,500 --> 00:00:13,000 Elephant's Dream @@ -83,7 +87,7 @@ class BaseTranscripts(CourseTestCase): # Add video module data = { - 'parent_locator': unicode(self.course.location), + 'parent_locator': six.text_type(self.course.location), 'category': 'video', 'type': 'video' } @@ -94,7 +98,9 @@ class BaseTranscripts(CourseTestCase): self.item = modulestore().get_item(self.video_usage_key) # hI10vDNYz4M - valid Youtube ID with transcripts. # JMD_ifUUfsU, AKqURZnYqpk, DYpADpL7jAY - valid Youtube IDs without transcripts. - self.item.data = '