BOM-2369 (A): pyupgrade on api,contentstore and cms_user_tasks apps under CMS (#26676)
* pyupgrade on cms api,contentstore and cms_user_tasks apps
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
""" Course run serializers. """
|
||||
|
||||
|
||||
import logging
|
||||
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 _
|
||||
@@ -14,8 +10,8 @@ from rest_framework.fields import empty
|
||||
|
||||
from cms.djangoapps.contentstore.views.assets import update_course_run_asset
|
||||
from cms.djangoapps.contentstore.views.course import create_new_course, get_course_and_check_access, rerun_course
|
||||
from openedx.core.lib.courses import course_image_url
|
||||
from common.djangoapps.student.models import CourseAccessRole
|
||||
from openedx.core.lib.courses import course_image_url
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
IMAGE_TYPES = {
|
||||
@@ -88,7 +84,7 @@ def image_is_jpeg_or_png(value):
|
||||
content_type = value.content_type
|
||||
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))
|
||||
f'Only JPEG and PNG image types are supported. {content_type} is not valid')
|
||||
|
||||
|
||||
class CourseRunImageField(serializers.ImageField): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
@@ -143,7 +139,7 @@ class CourseRunSerializer(CourseRunSerializerCommonFieldsMixin, CourseRunTeamSer
|
||||
with transaction.atomic():
|
||||
self.update_team(instance, team)
|
||||
|
||||
for attr, value in six.iteritems(validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
|
||||
modulestore().update_item(instance, self.context['request'].user.id)
|
||||
@@ -183,11 +179,11 @@ class CourseRunRerunSerializer(CourseRunSerializerCommonFieldsMixin, CourseRunTe
|
||||
new_course_run_key = store.make_course_key(course_run_key.org, number, run)
|
||||
except InvalidKeyError:
|
||||
raise serializers.ValidationError( # lint-amnesty, pylint: disable=raise-missing-from
|
||||
u'Invalid key supplied. Ensure there are no special characters in the Course Number.'
|
||||
'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(
|
||||
{'run': u'Course run {key} already exists'.format(key=new_course_run_key)}
|
||||
{'run': f'Course run {new_course_run_key} already exists'}
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import ddt
|
||||
import pytz
|
||||
from django.test import RequestFactory
|
||||
|
||||
from openedx.core.lib.courses import course_image_url
|
||||
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from openedx.core.lib.courses import course_image_url
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -21,7 +21,7 @@ from ..utils import serialize_datetime
|
||||
class CourseRunSerializerTests(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
|
||||
def setUp(self):
|
||||
super(CourseRunSerializerTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
|
||||
self.course_start = datetime.datetime.now(pytz.UTC)
|
||||
self.course_end = self.course_start + datetime.timedelta(days=30)
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
|
||||
import datetime
|
||||
from unittest.mock import patch # lint-amnesty, pylint: disable=unused-import
|
||||
|
||||
import ddt
|
||||
import pytz
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import RequestFactory, override_settings
|
||||
from django.urls import reverse
|
||||
from mock import patch # lint-amnesty, pylint: disable=unused-import
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from organizations.api import add_organization, get_course_organizations
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from openedx.core.lib.courses import course_image_url
|
||||
from common.djangoapps.student.models import CourseAccessRole
|
||||
from common.djangoapps.student.tests.factories import TEST_PASSWORD, AdminFactory, UserFactory
|
||||
from openedx.core.lib.courses import course_image_url
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.exceptions import NotFoundError
|
||||
@@ -35,7 +35,7 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
|
||||
list_url = reverse('api:v1:course_run-list')
|
||||
|
||||
def setUp(self):
|
||||
super(CourseRunViewSetTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.client = APIClient()
|
||||
user = AdminFactory()
|
||||
self.client.login(username=user.username, password=TEST_PASSWORD)
|
||||
@@ -388,7 +388,7 @@ 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)]}
|
||||
assert response.data == {'run': [f'Course run {course_run.id} already exists']}
|
||||
|
||||
def test_rerun_invalid_number(self):
|
||||
course_run = ToyCourseFactory()
|
||||
@@ -400,5 +400,5 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
|
||||
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.'
|
||||
'Invalid key supplied. Ensure there are no special characters in the Course Number.'
|
||||
]}
|
||||
|
||||
@@ -30,8 +30,8 @@ class CourseRunViewSet(viewsets.GenericViewSet): # lint-amnesty, pylint: disabl
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
|
||||
assert lookup_url_kwarg in self.kwargs, (
|
||||
u'Expected view %s to be called with a URL keyword argument '
|
||||
u'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||
'Expected view %s to be called with a URL keyword argument '
|
||||
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||
'attribute on the view correctly.' %
|
||||
(self.__class__.__name__, lookup_url_kwarg)
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ class CmsUserTasksConfig(AppConfig):
|
||||
"""
|
||||
Application Configuration for cms_user_tasks.
|
||||
"""
|
||||
name = u'cms.djangoapps.cms_user_tasks'
|
||||
name = 'cms.djangoapps.cms_user_tasks'
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
|
||||
@@ -4,10 +4,10 @@ Receivers of signals sent from django-user-tasks
|
||||
|
||||
|
||||
import logging
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from six.moves.urllib.parse import urljoin
|
||||
from user_tasks.models import UserTaskArtifact
|
||||
from user_tasks.signals import user_task_stopped
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ Celery tasks used by cms_user_tasks
|
||||
|
||||
|
||||
from boto.exception import NoAuthHandlerFound
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from celery import shared_task
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
@@ -45,10 +45,10 @@ def send_task_complete_email(self, task_name, task_state_text, dest_addr, detail
|
||||
|
||||
try:
|
||||
mail.send_mail(subject, message, from_address, [dest_addr], fail_silently=False)
|
||||
LOGGER.info(u"Task complete email has been sent to User %s", dest_addr)
|
||||
LOGGER.info("Task complete email has been sent to User %s", dest_addr)
|
||||
except NoAuthHandlerFound:
|
||||
LOGGER.info(
|
||||
u'Retrying sending email to user %s, attempt # %s of %s',
|
||||
'Retrying sending email to user %s, attempt # %s of %s',
|
||||
dest_addr,
|
||||
retries,
|
||||
TASK_COMPLETE_EMAIL_MAX_RETRIES
|
||||
@@ -57,14 +57,14 @@ def send_task_complete_email(self, task_name, task_state_text, dest_addr, detail
|
||||
self.retry(countdown=TASK_COMPLETE_EMAIL_TIMEOUT, max_retries=TASK_COMPLETE_EMAIL_MAX_RETRIES)
|
||||
except MaxRetriesExceededError:
|
||||
LOGGER.error(
|
||||
u'Unable to send task completion email to user from "%s" to "%s"',
|
||||
'Unable to send task completion email to user from "%s" to "%s"',
|
||||
from_address,
|
||||
dest_addr,
|
||||
exc_info=True
|
||||
)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
LOGGER.exception(
|
||||
u'Unable to send task completion email to user from "%s" to "%s"',
|
||||
'Unable to send task completion email to user from "%s" to "%s"',
|
||||
from_address,
|
||||
dest_addr,
|
||||
exc_info=True
|
||||
|
||||
@@ -4,9 +4,9 @@ Unit tests for integration of the django-user-tasks app and its REST API.
|
||||
|
||||
|
||||
import logging
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
import mock
|
||||
from boto.exception import NoAuthHandlerFound
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
@@ -80,7 +80,7 @@ class TestUserTasks(APITestCase):
|
||||
cls.artifact = UserTaskArtifact.objects.create(status=cls.status, text='Lorem ipsum')
|
||||
|
||||
def setUp(self):
|
||||
super(TestUserTasks, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.status.refresh_from_db()
|
||||
self.client.force_authenticate(self.user) # pylint: disable=no-member
|
||||
|
||||
@@ -152,7 +152,7 @@ class TestUserTaskStopped(APITestCase):
|
||||
total_steps=5)
|
||||
|
||||
def setUp(self):
|
||||
super(TestUserTaskStopped, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.status.refresh_from_db()
|
||||
self.client.force_authenticate(self.user) # pylint: disable=no-member
|
||||
|
||||
@@ -169,7 +169,7 @@ class TestUserTaskStopped(APITestCase):
|
||||
platform_name=settings.PLATFORM_NAME, studio_name=settings.STUDIO_NAME
|
||||
)
|
||||
body_fragments = [
|
||||
"Your {task_name} task has completed with the status".format(task_name=self.status.name.lower()),
|
||||
f"Your {self.status.name.lower()} task has completed with the status",
|
||||
"https://test.edx.org/",
|
||||
reverse('usertaskstatus-detail', args=[self.status.uuid])
|
||||
]
|
||||
@@ -202,7 +202,7 @@ class TestUserTaskStopped(APITestCase):
|
||||
platform_name=settings.PLATFORM_NAME, studio_name=settings.STUDIO_NAME
|
||||
)
|
||||
fragments = [
|
||||
"Your {task_name} task has completed with the status".format(task_name=self.status.name.lower()),
|
||||
f"Your {self.status.name.lower()} task has completed with the status",
|
||||
"Sign in to view the details of your task or download any files created."
|
||||
]
|
||||
|
||||
@@ -234,4 +234,4 @@ class TestUserTaskStopped(APITestCase):
|
||||
mock_delay.side_effect = NoAuthHandlerFound()
|
||||
user_task_stopped.send(sender=UserTaskStatus, status=self.status)
|
||||
self.assertTrue(mock_delay.called)
|
||||
self.assertEqual(hdlr.messages['error'][0], u'Unable to queue send_task_complete_email')
|
||||
self.assertEqual(hdlr.messages['error'][0], 'Unable to queue send_task_complete_email')
|
||||
|
||||
@@ -6,8 +6,8 @@ Base test case for the course API views.
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from lms.djangoapps.courseware.tests.factories import StaffFactory
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.tests.factories import StaffFactory
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
@@ -22,7 +22,7 @@ class BaseCourseViewTest(SharedModuleStoreTestCase, APITestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseCourseViewTest, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
|
||||
cls.course = CourseFactory.create(display_name='test course', run="Testing_course")
|
||||
cls.course_key = cls.course.id
|
||||
|
||||
@@ -13,8 +13,8 @@ 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 common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.tests.factories import StaffFactory
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -27,7 +27,7 @@ class CourseImportViewTest(SharedModuleStoreTestCase, APITestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CourseImportViewTest, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
|
||||
cls.course = CourseFactory.create(display_name='test course', run="Testing_course")
|
||||
cls.course_key = cls.course.id
|
||||
|
||||
@@ -9,8 +9,8 @@ from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from lms.djangoapps.courseware.tests.factories import StaffFactory
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.tests.factories import StaffFactory
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
@@ -23,7 +23,7 @@ class CourseValidationViewTest(SharedModuleStoreTestCase, APITestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CourseValidationViewTest, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
|
||||
cls.course = CourseFactory.create(display_name='test course', run="Testing_course")
|
||||
cls.course_key = cls.course.id
|
||||
@@ -49,7 +49,7 @@ class CourseValidationViewTest(SharedModuleStoreTestCase, APITestCase):
|
||||
update_key.course_key,
|
||||
update_key.block_type,
|
||||
block_id=update_key.block_id,
|
||||
fields=dict(data=u"<ol><li><h2>Date</h2>Hello world!</li></ol>"),
|
||||
fields=dict(data="<ol><li><h2>Date</h2>Hello world!</li></ol>"),
|
||||
)
|
||||
|
||||
section = ItemFactory.create(
|
||||
|
||||
@@ -9,10 +9,10 @@ from cms.djangoapps.contentstore.api.views import course_import, course_quality,
|
||||
app_name = 'contentstore'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^v0/import/{course_id}/$'.format(course_id=settings.COURSE_ID_PATTERN,),
|
||||
url(fr'^v0/import/{settings.COURSE_ID_PATTERN}/$',
|
||||
course_import.CourseImportView.as_view(), name='course_import'),
|
||||
url(r'^v1/validation/{course_id}/$'.format(course_id=settings.COURSE_ID_PATTERN,),
|
||||
url(fr'^v1/validation/{settings.COURSE_ID_PATTERN}/$',
|
||||
course_validation.CourseValidationView.as_view(), name='course_validation'),
|
||||
url(r'^v1/quality/{course_id}/$'.format(course_id=settings.COURSE_ID_PATTERN,),
|
||||
url(fr'^v1/quality/{settings.COURSE_ID_PATTERN}/$',
|
||||
course_quality.CourseQualityView.as_view(), name='course_quality'),
|
||||
]
|
||||
|
||||
@@ -14,7 +14,6 @@ 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 cms.djangoapps.contentstore.storage import course_import_export_storage
|
||||
@@ -35,7 +34,7 @@ class CourseImportExportViewMixin(DeveloperErrorViewMixin):
|
||||
"""
|
||||
Ensures that the user is authenticated (e.g. not an AnonymousUser)
|
||||
"""
|
||||
super(CourseImportExportViewMixin, self).perform_authentication(request) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().perform_authentication(request)
|
||||
if request.user.is_anonymous:
|
||||
raise AuthenticationFailed
|
||||
|
||||
@@ -135,18 +134,18 @@ class CourseImportView(CourseImportExportViewMixin, GenericAPIView):
|
||||
if not course_dir.isdir():
|
||||
os.mkdir(course_dir)
|
||||
|
||||
log.debug(u'importing course to {0}'.format(temp_filepath))
|
||||
log.debug(f'importing course to {temp_filepath}')
|
||||
with open(temp_filepath, "wb+") as temp_file:
|
||||
for chunk in request.FILES['course_data'].chunks():
|
||||
temp_file.write(chunk)
|
||||
|
||||
log.info(u"Course import %s: Upload complete", course_key)
|
||||
log.info("Course import %s: Upload complete", course_key)
|
||||
with open(temp_filepath, 'rb') as local_file:
|
||||
django_file = File(local_file)
|
||||
storage_path = course_import_export_storage.save(u'olx_import/' + filename, django_file)
|
||||
storage_path = course_import_export_storage.save('olx_import/' + filename, django_file)
|
||||
|
||||
async_result = import_olx.delay(
|
||||
request.user.id, text_type(course_key), storage_path, filename, request.LANGUAGE_CODE)
|
||||
request.user.id, str(course_key), storage_path, filename, request.LANGUAGE_CODE)
|
||||
return Response({
|
||||
'task_id': async_result.task_id
|
||||
})
|
||||
@@ -166,7 +165,7 @@ class CourseImportView(CourseImportExportViewMixin, GenericAPIView):
|
||||
try:
|
||||
task_id = request.GET['task_id']
|
||||
filename = request.GET['filename']
|
||||
args = {u'course_key_string': str(course_key), u'archive_name': filename}
|
||||
args = {'course_key_string': str(course_key), 'archive_name': filename}
|
||||
name = CourseImportTask.generate_name(args)
|
||||
task_status = UserTaskStatus.objects.filter(name=name, task_id=task_id).first()
|
||||
return Response({
|
||||
|
||||
@@ -3,7 +3,6 @@ import logging
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import six
|
||||
from edxval.api import get_videos_for_course
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
@@ -91,7 +90,7 @@ class CourseQualityView(DeveloperErrorViewMixin, GenericAPIView):
|
||||
if log_time:
|
||||
start_time = time.time()
|
||||
output = func(*args)
|
||||
log.info(u'[%s] completed in [%f]', func.__name__, (time.time() - start_time))
|
||||
log.info('[%s] completed in [%f]', func.__name__, (time.time() - start_time))
|
||||
else:
|
||||
output = func(*args)
|
||||
return output
|
||||
@@ -154,25 +153,25 @@ class CourseQualityView(DeveloperErrorViewMixin, GenericAPIView):
|
||||
def _subsections_quality(self, course, request): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
subsection_unit_dict = self._get_subsections_and_units(course, request)
|
||||
num_block_types_per_subsection_dict = {}
|
||||
for subsection_key, unit_dict in six.iteritems(subsection_unit_dict):
|
||||
for subsection_key, unit_dict in subsection_unit_dict.items():
|
||||
leaf_block_types_in_subsection = (
|
||||
unit_info['leaf_block_types']
|
||||
for unit_info in six.itervalues(unit_dict)
|
||||
for unit_info in unit_dict.values()
|
||||
)
|
||||
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(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))),
|
||||
num_with_one_block_type=list(num_block_types_per_subsection_dict.values()).count(1),
|
||||
num_block_types=self._stats_dict(list(num_block_types_per_subsection_dict.values())),
|
||||
)
|
||||
|
||||
def _units_quality(self, course, request): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
subsection_unit_dict = self._get_subsections_and_units(course, request)
|
||||
num_leaf_blocks_per_unit = [
|
||||
unit_info['num_leaf_blocks']
|
||||
for unit_dict in six.itervalues(subsection_unit_dict)
|
||||
for unit_info in six.itervalues(unit_dict)
|
||||
for unit_dict in subsection_unit_dict.values()
|
||||
for unit_info in unit_dict.values()
|
||||
]
|
||||
return dict(
|
||||
total_visible=len(num_leaf_blocks_per_unit),
|
||||
@@ -215,7 +214,7 @@ class CourseQualityView(DeveloperErrorViewMixin, GenericAPIView):
|
||||
leaf_blocks = cls._get_leaf_blocks(unit)
|
||||
unit_dict[unit.location] = dict(
|
||||
num_leaf_blocks=len(leaf_blocks),
|
||||
leaf_block_types=set(block.location.block_type for block in leaf_blocks),
|
||||
leaf_block_types={block.location.block_type for block in leaf_blocks},
|
||||
)
|
||||
|
||||
subsection_dict[subsection.location] = unit_dict
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import logging
|
||||
|
||||
import dateutil
|
||||
import six
|
||||
from pytz import UTC
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
@@ -118,7 +117,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
|
||||
]
|
||||
assignments_with_dates_before_start = (
|
||||
[
|
||||
{'id': six.text_type(a.location), 'display_name': a.display_name}
|
||||
{'id': str(a.location), 'display_name': a.display_name}
|
||||
for a in assignments_with_dates
|
||||
if a.due < course.start
|
||||
]
|
||||
@@ -128,7 +127,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
|
||||
|
||||
assignments_with_dates_after_end = (
|
||||
[
|
||||
{'id': six.text_type(a.location), 'display_name': a.display_name}
|
||||
{'id': str(a.location), 'display_name': a.display_name}
|
||||
for a in assignments_with_dates
|
||||
if a.due > course.end
|
||||
]
|
||||
@@ -144,7 +143,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
|
||||
]
|
||||
assignments_with_dates_before_start = (
|
||||
[
|
||||
{'id': six.text_type(a.location), 'display_name': a.display_name}
|
||||
{'id': str(a.location), 'display_name': a.display_name}
|
||||
for a in assignments_with_dates
|
||||
if a.due < course.start
|
||||
]
|
||||
@@ -154,7 +153,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
|
||||
|
||||
assignments_with_dates_after_end = (
|
||||
[
|
||||
{'id': six.text_type(a.location), 'display_name': a.display_name}
|
||||
{'id': str(a.location), 'display_name': a.display_name}
|
||||
for a in assignments_with_dates
|
||||
if a.due > course.end
|
||||
]
|
||||
@@ -175,14 +174,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': six.text_type(parent_assignment.location),
|
||||
'id': str(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': six.text_type(parent_assignment.location),
|
||||
'id': str(parent_assignment.location),
|
||||
'display_name': parent_assignment.display_name
|
||||
})
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import status
|
||||
from rest_framework.generics import GenericAPIView
|
||||
|
||||
from common.djangoapps.student.auth import has_course_author_access
|
||||
from openedx.core.djangoapps.util.forms import to_bool
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from common.djangoapps.student.auth import has_course_author_access
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class ContentstoreConfig(AppConfig):
|
||||
"""
|
||||
Application Configuration for Contentstore.
|
||||
"""
|
||||
name = u'cms.djangoapps.contentstore'
|
||||
name = 'cms.djangoapps.contentstore'
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
|
||||
@@ -5,39 +5,40 @@ waffle switches for the contentstore app.
|
||||
|
||||
|
||||
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace, LegacyWaffleSwitchNamespace
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
|
||||
# Namespace
|
||||
WAFFLE_NAMESPACE = u'studio'
|
||||
WAFFLE_NAMESPACE = 'studio'
|
||||
|
||||
# Switches
|
||||
ENABLE_ACCESSIBILITY_POLICY_PAGE = u'enable_policy_page'
|
||||
ENABLE_ACCESSIBILITY_POLICY_PAGE = 'enable_policy_page'
|
||||
|
||||
|
||||
def waffle():
|
||||
"""
|
||||
Returns the namespaced, cached, audited Waffle Switch class for Studio pages.
|
||||
"""
|
||||
return LegacyWaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Studio: ')
|
||||
return LegacyWaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix='Studio: ')
|
||||
|
||||
|
||||
def waffle_flags():
|
||||
"""
|
||||
Returns the namespaced, cached, audited Waffle Flag class for Studio pages.
|
||||
"""
|
||||
return LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Studio: ')
|
||||
return LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix='Studio: ')
|
||||
|
||||
|
||||
# TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment.
|
||||
ENABLE_CHECKLISTS_QUALITY = CourseWaffleFlag(
|
||||
waffle_namespace=waffle_flags(),
|
||||
flag_name=u'enable_checklists_quality',
|
||||
flag_name='enable_checklists_quality',
|
||||
module_name=__name__,
|
||||
)
|
||||
|
||||
SHOW_REVIEW_RULES_FLAG = CourseWaffleFlag(
|
||||
waffle_namespace=waffle_flags(),
|
||||
flag_name=u'show_review_rules',
|
||||
flag_name='show_review_rules',
|
||||
module_name=__name__,
|
||||
)
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ from collections import defaultdict
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from cms.djangoapps.contentstore.utils import reverse_usage_url
|
||||
from common.djangoapps.util.db import MYSQL_MAX_INT, generate_int_id
|
||||
from lms.lib.utils import get_parent_unit
|
||||
from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
|
||||
from common.djangoapps.util.db import MYSQL_MAX_INT, generate_int_id
|
||||
from xmodule.partitions.partitions import MINIMUM_STATIC_PARTITION_ID, ReadOnlyUserPartitionError, UserPartition
|
||||
from xmodule.partitions.partitions_service import get_all_partitions_for_course
|
||||
from xmodule.split_test_module import get_split_user_partitions
|
||||
@@ -39,7 +39,7 @@ class GroupConfigurationsValidationError(Exception):
|
||||
pass # lint-amnesty, pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
class GroupConfiguration(object):
|
||||
class GroupConfiguration:
|
||||
"""
|
||||
Prepare Group Configuration for the course.
|
||||
"""
|
||||
@@ -102,7 +102,7 @@ class GroupConfiguration(object):
|
||||
"""
|
||||
Return a list of IDs that already in use.
|
||||
"""
|
||||
return set([p.id for p in get_all_partitions_for_course(course)]) # lint-amnesty, pylint: disable=consider-using-set-comprehension
|
||||
return {p.id for p in get_all_partitions_for_course(course)}
|
||||
|
||||
def get_user_partition(self):
|
||||
"""
|
||||
@@ -144,7 +144,7 @@ class GroupConfiguration(object):
|
||||
course.location.course_key.make_usage_key(unit_for_url.location.block_type, unit_for_url.location.block_id)
|
||||
)
|
||||
|
||||
usage_dict = {'label': u"{} / {}".format(unit.display_name, item.display_name), 'url': unit_url}
|
||||
usage_dict = {'label': f"{unit.display_name} / {item.display_name}", 'url': unit_url}
|
||||
if scheme_name == RANDOM_SCHEME:
|
||||
validation_summary = item.general_validation_message()
|
||||
usage_dict.update({'validation': validation_summary.to_json() if validation_summary else None})
|
||||
@@ -196,7 +196,7 @@ class GroupConfiguration(object):
|
||||
for split_test in split_tests:
|
||||
unit = split_test.get_parent()
|
||||
if not unit:
|
||||
log.warning(u"Unable to find parent for split_test %s", split_test.location)
|
||||
log.warning("Unable to find parent for split_test %s", split_test.location)
|
||||
# Make sure that this user_partition appears in the output even though it has no content
|
||||
usage_info[split_test.user_partition_id] = []
|
||||
continue
|
||||
@@ -236,7 +236,7 @@ class GroupConfiguration(object):
|
||||
for item, partition_id, group_id in GroupConfiguration._iterate_items_and_group_ids(course, items):
|
||||
unit = item.get_parent()
|
||||
if not unit:
|
||||
log.warning(u"Unable to find parent for component %s", item.location)
|
||||
log.warning("Unable to find parent for component %s", item.location)
|
||||
continue
|
||||
|
||||
usage_info[partition_id][group_id].append(GroupConfiguration._get_usage_dict(
|
||||
|
||||
@@ -11,7 +11,6 @@ from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from eventtracking import tracker
|
||||
from search.search_engine_base import SearchEngine
|
||||
from six import add_metaclass, string_types, text_type
|
||||
|
||||
from cms.djangoapps.contentstore.course_group_config import GroupConfiguration
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
@@ -57,12 +56,11 @@ class SearchIndexingError(Exception):
|
||||
""" Indicates some error(s) occured during indexing """
|
||||
|
||||
def __init__(self, message, error_list):
|
||||
super(SearchIndexingError, self).__init__(message) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(message)
|
||||
self.error_list = error_list
|
||||
|
||||
|
||||
@add_metaclass(ABCMeta)
|
||||
class SearchIndexerBase(object, metaclass=ABCMeta):
|
||||
class SearchIndexerBase(metaclass=ABCMeta):
|
||||
"""
|
||||
Base class to perform indexing for courseware or library search from different modulestores
|
||||
"""
|
||||
@@ -192,22 +190,22 @@ class SearchIndexerBase(object, metaclass=ABCMeta):
|
||||
for split_test_child in item.get_children():
|
||||
if split_partition:
|
||||
for group in split_partition.groups:
|
||||
group_id = text_type(group.id)
|
||||
group_id = str(group.id)
|
||||
child_location = item.group_id_to_child.get(group_id, None)
|
||||
if child_location == split_test_child.location:
|
||||
groups_usage_info.update({
|
||||
text_type(get_item_location(split_test_child)): [group_id],
|
||||
str(get_item_location(split_test_child)): [group_id],
|
||||
})
|
||||
for component in split_test_child.get_children():
|
||||
groups_usage_info.update({
|
||||
text_type(get_item_location(component)): [group_id]
|
||||
str(get_item_location(component)): [group_id]
|
||||
})
|
||||
|
||||
if groups_usage_info:
|
||||
item_location = get_item_location(item)
|
||||
item_content_groups = groups_usage_info.get(text_type(item_location), None)
|
||||
item_content_groups = groups_usage_info.get(str(item_location), None)
|
||||
|
||||
item_id = text_type(cls._id_modifier(item.scope_ids.usage_id))
|
||||
item_id = str(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
|
||||
@@ -244,8 +242,8 @@ class SearchIndexerBase(object, metaclass=ABCMeta):
|
||||
return item_content_groups
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
# broad exception so that index operation does not fail on one item of many
|
||||
log.warning(u'Could not index item: %s - %r', item.location, err)
|
||||
error_list.append(_(u'Could not index item: {}').format(item.location))
|
||||
log.warning('Could not index item: %s - %r', item.location, err)
|
||||
error_list.append(_('Could not index item: {}').format(item.location))
|
||||
|
||||
try:
|
||||
with modulestore.branch_setting(ModuleStoreEnum.RevisionOption.published_only):
|
||||
@@ -263,7 +261,7 @@ class SearchIndexerBase(object, metaclass=ABCMeta):
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
# broad exception so that index operation does not prevent the rest of the application from working
|
||||
log.exception(
|
||||
u"Indexing error encountered, courseware index may be out of date %s - %r",
|
||||
"Indexing error encountered, courseware index may be out of date %s - %r",
|
||||
structure_key,
|
||||
err
|
||||
)
|
||||
@@ -365,7 +363,7 @@ class CoursewareSearchIndexer(SearchIndexerBase):
|
||||
@classmethod
|
||||
def _get_location_info(cls, normalized_structure_key):
|
||||
""" Builds location info dictionary """
|
||||
return {"course": text_type(normalized_structure_key), "org": normalized_structure_key.org}
|
||||
return {"course": str(normalized_structure_key), "org": normalized_structure_key.org}
|
||||
|
||||
@classmethod
|
||||
def do_course_reindex(cls, modulestore, course_key):
|
||||
@@ -405,7 +403,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 = text_type(kwargs['usage_key_string'])
|
||||
usage_key_string = str(kwargs['usage_key_string'])
|
||||
if groups_usage_dict.get(usage_key_string, None):
|
||||
groups_usage_dict[usage_key_string].append(name)
|
||||
else:
|
||||
@@ -434,7 +432,7 @@ class CoursewareSearchIndexer(SearchIndexerBase):
|
||||
while parent is not None:
|
||||
path_component_name = parent.display_name
|
||||
if not path_component_name:
|
||||
path_component_name = text_type(cls.UNNAMED_MODULE_NAME)
|
||||
path_component_name = str(cls.UNNAMED_MODULE_NAME)
|
||||
location_path.append(path_component_name)
|
||||
parent = parent.get_parent()
|
||||
location_path.reverse()
|
||||
@@ -469,7 +467,7 @@ class LibrarySearchIndexer(SearchIndexerBase):
|
||||
@classmethod
|
||||
def _get_location_info(cls, normalized_structure_key):
|
||||
""" Builds location info dictionary """
|
||||
return {"library": text_type(normalized_structure_key)}
|
||||
return {"library": str(normalized_structure_key)}
|
||||
|
||||
@classmethod
|
||||
def _id_modifier(cls, usage_id):
|
||||
@@ -484,7 +482,7 @@ class LibrarySearchIndexer(SearchIndexerBase):
|
||||
return cls._do_reindex(modulestore, library_key)
|
||||
|
||||
|
||||
class AboutInfo(object):
|
||||
class AboutInfo:
|
||||
""" About info structure to contain
|
||||
1) Property name to use
|
||||
2) Where to add in the index (using flags above)
|
||||
@@ -605,7 +603,7 @@ class CourseAboutSearchIndexer(CoursewareSearchIndexer):
|
||||
if not searcher:
|
||||
return
|
||||
|
||||
course_id = text_type(course.id)
|
||||
course_id = str(course.id)
|
||||
course_info = {
|
||||
'id': course_id,
|
||||
'course': course_id,
|
||||
@@ -631,7 +629,7 @@ class CourseAboutSearchIndexer(CoursewareSearchIndexer):
|
||||
except: # pylint: disable=bare-except
|
||||
section_content = None
|
||||
log.warning(
|
||||
u"Course discovery could not collect property %s for course %s",
|
||||
"Course discovery could not collect property %s for course %s",
|
||||
about_information.property_name,
|
||||
course_id,
|
||||
exc_info=True,
|
||||
@@ -640,7 +638,7 @@ class CourseAboutSearchIndexer(CoursewareSearchIndexer):
|
||||
if section_content:
|
||||
if about_information.index_flags & AboutInfo.ANALYSE:
|
||||
analyse_content = section_content
|
||||
if isinstance(section_content, string_types):
|
||||
if isinstance(section_content, str):
|
||||
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:
|
||||
@@ -651,20 +649,20 @@ class CourseAboutSearchIndexer(CoursewareSearchIndexer):
|
||||
searcher.index([course_info])
|
||||
except:
|
||||
log.exception(
|
||||
u"Course discovery indexing error encountered, course discovery index may be out of date %s",
|
||||
"Course discovery indexing error encountered, course discovery index may be out of date %s",
|
||||
course_id,
|
||||
)
|
||||
raise
|
||||
|
||||
log.debug(
|
||||
u"Successfully added %s course to the course discovery index",
|
||||
"Successfully added %s course to the course discovery index",
|
||||
course_id
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_location_info(cls, normalized_structure_key):
|
||||
""" Builds location info dictionary """
|
||||
return {"course": text_type(normalized_structure_key), "org": normalized_structure_key.org}
|
||||
return {"course": str(normalized_structure_key), "org": normalized_structure_key.org}
|
||||
|
||||
@classmethod
|
||||
def remove_deleted_items(cls, structure_key): # lint-amnesty, pylint: disable=arguments-differ
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.core.files.uploadhandler import FileUploadHandler
|
||||
|
||||
class DebugFileUploader(FileUploadHandler): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
def __init__(self, request=None):
|
||||
super(DebugFileUploader, self).__init__(request) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(request)
|
||||
self.count = 0
|
||||
|
||||
def receive_data_chunk(self, raw_data, start):
|
||||
|
||||
@@ -7,13 +7,12 @@ committing and pushing the changes.
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -32,9 +31,9 @@ class GitExportError(Exception):
|
||||
|
||||
def __init__(self, message):
|
||||
# Force the lazy i18n values to turn into actual unicode objects
|
||||
super().__init__(six.text_type(message))
|
||||
super().__init__(str(message))
|
||||
|
||||
NO_EXPORT_DIR = _(u"GIT_REPO_EXPORT_DIR not set or path {0} doesn't exist, "
|
||||
NO_EXPORT_DIR = _("GIT_REPO_EXPORT_DIR not set or path {0} doesn't exist, "
|
||||
"please create it, or configure a different path with "
|
||||
"GIT_REPO_EXPORT_DIR").format(GIT_REPO_EXPORT_DIR)
|
||||
URL_BAD = _('Non writable git url provided. Expecting something like:'
|
||||
@@ -61,9 +60,9 @@ def cmd_log(cmd, cwd):
|
||||
command doesn't return 0, and returns the command's output.
|
||||
"""
|
||||
output = subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT)
|
||||
log.debug(u'Command was: {0!r}. '
|
||||
u'Working directory was: {1!r}'.format(' '.join(cmd), cwd))
|
||||
log.debug(u'Command output was: {0!r}'.format(output))
|
||||
log.debug('Command was: {!r}. '
|
||||
'Working directory was: {!r}'.format(' '.join(cmd), cwd))
|
||||
log.debug(f'Command output was: {output!r}')
|
||||
return output
|
||||
|
||||
|
||||
@@ -92,11 +91,11 @@ def export_to_git(course_id, repo, user='', rdir=None):
|
||||
else:
|
||||
rdir = repo.rsplit('/', 1)[-1].rsplit('.git', 1)[0]
|
||||
|
||||
log.debug(u"rdir = %s", rdir)
|
||||
log.debug("rdir = %s", rdir)
|
||||
|
||||
# Pull or clone repo before exporting to xml
|
||||
# and update url in case origin changed.
|
||||
rdirp = '{0}/{1}'.format(GIT_REPO_EXPORT_DIR, rdir)
|
||||
rdirp = f'{GIT_REPO_EXPORT_DIR}/{rdir}'
|
||||
branch = None
|
||||
if os.path.exists(rdirp):
|
||||
log.info('Directory already exists, doing a git reset and pull '
|
||||
@@ -107,13 +106,13 @@ def export_to_git(course_id, repo, user='', rdir=None):
|
||||
try:
|
||||
branch = cmd_log(cmd, cwd).decode('utf-8').strip('\n')
|
||||
except subprocess.CalledProcessError as ex:
|
||||
log.exception(u'Failed to get branch: %r', ex.output)
|
||||
log.exception('Failed to get branch: %r', ex.output)
|
||||
raise GitExportError(GitExportError.DETACHED_HEAD) from ex
|
||||
|
||||
cmds = [
|
||||
['git', 'remote', 'set-url', 'origin', repo],
|
||||
['git', 'fetch', 'origin'],
|
||||
['git', 'reset', '--hard', 'origin/{0}'.format(branch)],
|
||||
['git', 'reset', '--hard', f'origin/{branch}'],
|
||||
['git', 'pull'],
|
||||
['git', 'clean', '-d', '-f'],
|
||||
]
|
||||
@@ -126,7 +125,7 @@ def export_to_git(course_id, repo, user='', rdir=None):
|
||||
try:
|
||||
cmd_log(cmd, cwd)
|
||||
except subprocess.CalledProcessError as ex:
|
||||
log.exception(u'Failed to pull git repository: %r', ex.output)
|
||||
log.exception('Failed to pull git repository: %r', ex.output)
|
||||
raise GitExportError(GitExportError.CANNOT_PULL) from ex
|
||||
|
||||
# export course as xml before commiting and pushing
|
||||
@@ -135,7 +134,7 @@ def export_to_git(course_id, repo, user='', rdir=None):
|
||||
try:
|
||||
export_course_to_xml(modulestore(), contentstore(), course_id,
|
||||
root_dir, course_dir)
|
||||
except (EnvironmentError, AttributeError):
|
||||
except (OSError, AttributeError):
|
||||
log.exception('Failed export to xml')
|
||||
raise GitExportError(GitExportError.XML_EXPORT_FAIL) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
@@ -145,7 +144,7 @@ def export_to_git(course_id, repo, user='', rdir=None):
|
||||
try:
|
||||
branch = cmd_log(cmd, os.path.abspath(rdirp)).decode('utf-8').strip('\n')
|
||||
except subprocess.CalledProcessError as ex:
|
||||
log.exception(u'Failed to get branch from freshly cloned repo: %r',
|
||||
log.exception('Failed to get branch from freshly cloned repo: %r',
|
||||
ex.output)
|
||||
raise GitExportError(GitExportError.MISSING_BRANCH) from ex
|
||||
|
||||
@@ -161,23 +160,23 @@ def export_to_git(course_id, repo, user='', rdir=None):
|
||||
ident = GIT_EXPORT_DEFAULT_IDENT
|
||||
time_stamp = timezone.now()
|
||||
cwd = os.path.abspath(rdirp)
|
||||
commit_msg = u"Export from Studio at {time_stamp}".format(
|
||||
commit_msg = "Export from Studio at {time_stamp}".format(
|
||||
time_stamp=time_stamp,
|
||||
)
|
||||
try:
|
||||
cmd_log(['git', 'config', 'user.email', ident['email']], cwd)
|
||||
cmd_log(['git', 'config', 'user.name', ident['name']], cwd)
|
||||
except subprocess.CalledProcessError as ex:
|
||||
log.exception(u'Error running git configure commands: %r', ex.output)
|
||||
log.exception('Error running git configure commands: %r', ex.output)
|
||||
raise GitExportError(GitExportError.CONFIG_ERROR) from ex
|
||||
try:
|
||||
cmd_log(['git', 'add', '.'], cwd)
|
||||
cmd_log(['git', 'commit', '-a', '-m', commit_msg], cwd)
|
||||
except subprocess.CalledProcessError as ex:
|
||||
log.exception(u'Unable to commit changes: %r', ex.output)
|
||||
log.exception('Unable to commit changes: %r', ex.output)
|
||||
raise GitExportError(GitExportError.CANNOT_COMMIT) from ex
|
||||
try:
|
||||
cmd_log(['git', 'push', '-q', 'origin', branch], cwd)
|
||||
except subprocess.CalledProcessError as ex:
|
||||
log.exception(u'Error running git push command: %r', ex.output)
|
||||
log.exception('Error running git push command: %r', ex.output)
|
||||
raise GitExportError(GitExportError.CANNOT_PUSH) from ex
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
@@ -32,7 +29,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('profile_whitelist', models.TextField(help_text=u'A comma-separated list of names of profiles to include in video encoding downloads.', blank=True)),
|
||||
('profile_whitelist', models.TextField(help_text='A comma-separated list of names of profiles to include in video encoding downloads.', blank=True)),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
options={
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.22 on 2019-07-26 20:12
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from cms.djangoapps.contentstore.config.waffle import ENABLE_CHECKLISTS_QUALITY
|
||||
|
||||
@@ -15,7 +15,7 @@ class VideoUploadConfig(ConfigurationModel):
|
||||
"""
|
||||
profile_whitelist = TextField(
|
||||
blank=True,
|
||||
help_text=u"A comma-separated list of names of profiles to include in video encoding downloads."
|
||||
help_text="A comma-separated list of names of profiles to include in video encoding downloads."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -14,7 +14,7 @@ from openedx.core.djangoapps.content.learning_sequences.data import (
|
||||
CourseSectionData,
|
||||
CourseVisibility,
|
||||
ExamData,
|
||||
VisibilityData,
|
||||
VisibilityData
|
||||
)
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -5,7 +5,6 @@ Code related to the handling of Proctored Exams in Studio
|
||||
|
||||
import logging
|
||||
|
||||
import six
|
||||
from django.conf import settings
|
||||
from edx_proctoring.api import (
|
||||
create_exam,
|
||||
@@ -39,7 +38,7 @@ def register_special_exams(course_key):
|
||||
|
||||
course = modulestore().get_course(course_key)
|
||||
if course is None:
|
||||
raise ItemNotFoundError(u"Course {} does not exist", six.text_type(course_key)) # lint-amnesty, pylint: disable=raising-format-tuple
|
||||
raise ItemNotFoundError("Course {} does not exist", str(course_key)) # lint-amnesty, pylint: disable=raising-format-tuple
|
||||
|
||||
if not course.enable_proctored_exams and not course.enable_timed_exams:
|
||||
# likewise if course does not have these features turned on
|
||||
@@ -68,8 +67,8 @@ def register_special_exams(course_key):
|
||||
# add/update any exam entries in edx-proctoring
|
||||
for timed_exam in timed_exams:
|
||||
msg = (
|
||||
u'Found {location} as a timed-exam in course structure. Inspecting...'.format(
|
||||
location=six.text_type(timed_exam.location)
|
||||
'Found {location} as a timed-exam in course structure. Inspecting...'.format(
|
||||
location=str(timed_exam.location)
|
||||
)
|
||||
)
|
||||
log.info(msg)
|
||||
@@ -87,20 +86,20 @@ def register_special_exams(course_key):
|
||||
}
|
||||
|
||||
try:
|
||||
exam = get_exam_by_content_id(six.text_type(course_key), six.text_type(timed_exam.location))
|
||||
exam = get_exam_by_content_id(str(course_key), str(timed_exam.location))
|
||||
# update case, make sure everything is synced
|
||||
exam_metadata['exam_id'] = exam['id']
|
||||
|
||||
exam_id = update_exam(**exam_metadata)
|
||||
msg = u'Updated timed exam {exam_id}'.format(exam_id=exam['id'])
|
||||
msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id'])
|
||||
log.info(msg)
|
||||
|
||||
except ProctoredExamNotFoundException:
|
||||
exam_metadata['course_id'] = six.text_type(course_key)
|
||||
exam_metadata['content_id'] = six.text_type(timed_exam.location)
|
||||
exam_metadata['course_id'] = str(course_key)
|
||||
exam_metadata['content_id'] = str(timed_exam.location)
|
||||
|
||||
exam_id = create_exam(**exam_metadata)
|
||||
msg = u'Created new timed exam {exam_id}'.format(exam_id=exam_id)
|
||||
msg = f'Created new timed exam {exam_id}'
|
||||
log.info(msg)
|
||||
|
||||
exam_review_policy_metadata = {
|
||||
@@ -116,7 +115,7 @@ def register_special_exams(course_key):
|
||||
except ProctoredExamReviewPolicyNotFoundException:
|
||||
if timed_exam.exam_review_rules: # won't save an empty rule.
|
||||
create_exam_review_policy(**exam_review_policy_metadata)
|
||||
msg = u'Created new exam review policy with exam_id {exam_id}'.format(exam_id=exam_id)
|
||||
msg = f'Created new exam review policy with exam_id {exam_id}'
|
||||
log.info(msg)
|
||||
else:
|
||||
try:
|
||||
@@ -135,12 +134,12 @@ def register_special_exams(course_key):
|
||||
|
||||
search = [
|
||||
timed_exam for timed_exam in timed_exams if
|
||||
six.text_type(timed_exam.location) == exam['content_id']
|
||||
str(timed_exam.location) == exam['content_id']
|
||||
]
|
||||
if not search:
|
||||
# This means it was turned off in Studio, we need to mark
|
||||
# the exam as inactive (we don't delete!)
|
||||
msg = u'Disabling timed exam {exam_id}'.format(exam_id=exam['id'])
|
||||
msg = 'Disabling timed exam {exam_id}'.format(exam_id=exam['id'])
|
||||
log.info(msg)
|
||||
update_exam(
|
||||
exam_id=exam['id'],
|
||||
|
||||
@@ -8,8 +8,8 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, InstructorFactory
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, InstructorFactory
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -77,7 +77,7 @@ class ProctoringExamSettingsTestMixin():
|
||||
response = self.make_request(course_id=course_id)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.data == {
|
||||
'detail': 'Course with course_id {} does not exist.'.format(course_id)
|
||||
'detail': f'Course with course_id {course_id} does not exist.'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ app_name = 'v1'
|
||||
|
||||
urlpatterns = [
|
||||
re_path(
|
||||
r'^proctored_exam_settings/{}$'.format(COURSE_ID_PATTERN),
|
||||
fr'^proctored_exam_settings/{COURSE_ID_PATTERN}$',
|
||||
views.ProctoredExamSettingsView.as_view(),
|
||||
name="proctored_exam_settings"
|
||||
),
|
||||
|
||||
@@ -169,7 +169,7 @@ class ProctoredExamSettingsView(APIView):
|
||||
|
||||
if not course_module:
|
||||
raise NotFound(
|
||||
'Course with course_id {} does not exist.'.format(course_id)
|
||||
f'Course with course_id {course_id} does not exist.'
|
||||
)
|
||||
|
||||
return course_module
|
||||
|
||||
@@ -5,22 +5,21 @@ import logging
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
|
||||
import six
|
||||
from django.core.cache import cache
|
||||
from django.dispatch import receiver
|
||||
from pytz import UTC
|
||||
|
||||
from cms.djangoapps.contentstore.courseware_index import (
|
||||
CoursewareSearchIndexer,
|
||||
CourseAboutSearchIndexer,
|
||||
CoursewareSearchIndexer,
|
||||
LibrarySearchIndexer
|
||||
)
|
||||
from cms.djangoapps.contentstore.proctoring import register_special_exams
|
||||
from common.djangoapps.track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
|
||||
from common.djangoapps.util.module_utils import yield_dynamic_descriptor_descendants
|
||||
from lms.djangoapps.grades.api import task_compute_all_grades_for_course
|
||||
from openedx.core.djangoapps.credit.signals import on_course_publish
|
||||
from openedx.core.lib.gating import api as gating_api
|
||||
from common.djangoapps.track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
|
||||
from common.djangoapps.util.module_utils import yield_dynamic_descriptor_descendants
|
||||
from xmodule.modulestore.django import SignalHandler, modulestore
|
||||
|
||||
from .signals import GRADING_POLICY_CHANGED
|
||||
@@ -36,7 +35,7 @@ def locked(expiry_seconds, key): # lint-amnesty, pylint: disable=missing-functi
|
||||
def wrapper(*args, **kwargs):
|
||||
cache_key = '{}-{}'.format(func.__name__, kwargs[key])
|
||||
if cache.add(cache_key, "true", expiry_seconds):
|
||||
log.info(u'Locking task in cache with key: %s for %s seconds', cache_key, expiry_seconds)
|
||||
log.info('Locking task in cache with key: %s for %s seconds', cache_key, expiry_seconds)
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
log.info('Task with key %s already exists in cache', cache_key)
|
||||
@@ -84,7 +83,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 cms.djangoapps.contentstore.tasks import update_library_index
|
||||
|
||||
update_library_index.delay(six.text_type(library_key), datetime.now(UTC).isoformat())
|
||||
update_library_index.delay(str(library_key), datetime.now(UTC).isoformat())
|
||||
|
||||
|
||||
@receiver(SignalHandler.item_deleted)
|
||||
@@ -122,13 +121,13 @@ def handle_grading_policy_changed(sender, **kwargs):
|
||||
Receives signal and kicks off celery task to recalculate grades
|
||||
"""
|
||||
kwargs = {
|
||||
'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()),
|
||||
'course_key': str(kwargs.get('course_key')),
|
||||
'grading_policy_hash': str(kwargs.get('grading_policy_hash')),
|
||||
'event_transaction_id': str(get_event_transaction_id()),
|
||||
'event_transaction_type': str(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(
|
||||
log.info("Grades: Created {task_name}[{task_id}] with arguments {kwargs}".format(
|
||||
task_name=task_compute_all_grades_for_course.name,
|
||||
task_id=result.task_id,
|
||||
kwargs=kwargs,
|
||||
|
||||
@@ -16,7 +16,7 @@ class ImportExportS3Storage(S3BotoStorage): # pylint: disable=abstract-method
|
||||
|
||||
def __init__(self):
|
||||
bucket = setting('COURSE_IMPORT_EXPORT_BUCKET', settings.AWS_STORAGE_BUCKET_NAME)
|
||||
super(ImportExportS3Storage, self).__init__(bucket=bucket, custom_domain=None, querystring_auth=True) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(bucket=bucket, custom_domain=None, querystring_auth=True)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
course_import_export_storage = get_storage_class(settings.COURSE_IMPORT_EXPORT_STORAGE)()
|
||||
|
||||
@@ -29,7 +29,6 @@ from organizations.api import add_organization_course, ensure_organization
|
||||
from organizations.models import OrganizationCourse
|
||||
from path import Path as path
|
||||
from pytz import UTC
|
||||
from six import iteritems, text_type
|
||||
from user_tasks.models import UserTaskArtifact, UserTaskStatus
|
||||
from user_tasks.tasks import UserTask
|
||||
|
||||
@@ -42,9 +41,9 @@ from cms.djangoapps.contentstore.storage import course_import_export_storage
|
||||
from cms.djangoapps.contentstore.utils import initialize_permissions, reverse_usage_url, translation_language
|
||||
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
|
||||
from common.djangoapps.course_action_state.models import CourseRerunState
|
||||
from common.djangoapps.student.auth import has_course_author_access
|
||||
from openedx.core.djangoapps.embargo.models import CountryAccessRule, RestrictedCourse
|
||||
from openedx.core.lib.extract_tar import safetar_extractall
|
||||
from common.djangoapps.student.auth import has_course_author_access
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.course_module import CourseFields
|
||||
from xmodule.exceptions import SerializationError
|
||||
@@ -53,6 +52,7 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml, export_library_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml, import_library_from_xml
|
||||
|
||||
from .outlines import update_outline_from_modulestore
|
||||
|
||||
User = get_user_model()
|
||||
@@ -76,7 +76,7 @@ def clone_instance(instance, field_values):
|
||||
"""
|
||||
instance.pk = None
|
||||
|
||||
for field, value in iteritems(field_values):
|
||||
for field, value in field_values.items():
|
||||
setattr(instance, field, value)
|
||||
|
||||
instance.save()
|
||||
@@ -136,14 +136,14 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
|
||||
except DuplicateCourseError:
|
||||
# do NOT delete the original course, only update the status
|
||||
CourseRerunState.objects.failed(course_key=destination_course_key)
|
||||
LOGGER.exception(u'Course Rerun Error')
|
||||
LOGGER.exception('Course Rerun Error')
|
||||
return "duplicate course"
|
||||
|
||||
# catch all exceptions so we can update the state and properly cleanup the course.
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
# update state: Failed
|
||||
CourseRerunState.objects.failed(course_key=destination_course_key)
|
||||
LOGGER.exception(u'Course Rerun Error')
|
||||
LOGGER.exception('Course Rerun Error')
|
||||
|
||||
try:
|
||||
# cleanup any remnants of the course
|
||||
@@ -152,12 +152,12 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
|
||||
# it's possible there was an error even before the course module was created
|
||||
pass
|
||||
|
||||
return u"exception: " + text_type(exc)
|
||||
return "exception: " + str(exc)
|
||||
|
||||
|
||||
def deserialize_fields(json_fields):
|
||||
fields = json.loads(json_fields)
|
||||
for field_name, value in iteritems(fields):
|
||||
for field_name, value in fields.items():
|
||||
fields[field_name] = getattr(CourseFields, field_name).from_json(value)
|
||||
return fields
|
||||
|
||||
@@ -183,7 +183,7 @@ def update_search_index(course_id, triggered_time_isoformat):
|
||||
# expensive (sometimes hours-long for really complex courses).
|
||||
if isinstance(course_key, CCXLocator):
|
||||
LOGGER.warning(
|
||||
u'Search indexing skipped for CCX Course %s (this is currently too slow to run in production)',
|
||||
'Search indexing skipped for CCX Course %s (this is currently too slow to run in production)',
|
||||
course_id
|
||||
)
|
||||
return
|
||||
@@ -193,13 +193,13 @@ def update_search_index(course_id, triggered_time_isoformat):
|
||||
except SearchIndexingError as exc:
|
||||
error_list = exc.error_list
|
||||
LOGGER.error(
|
||||
u"Search indexing error for complete course %s - %s - %s",
|
||||
"Search indexing error for complete course %s - %s - %s",
|
||||
course_id,
|
||||
text_type(exc),
|
||||
str(exc),
|
||||
error_list,
|
||||
)
|
||||
else:
|
||||
LOGGER.debug(u'Search indexing successful for complete course %s', course_id)
|
||||
LOGGER.debug('Search indexing successful for complete course %s', course_id)
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -211,9 +211,9 @@ def update_library_index(library_id, triggered_time_isoformat):
|
||||
LibrarySearchIndexer.index(modulestore(), library_key, triggered_at=(_parse_time(triggered_time_isoformat)))
|
||||
|
||||
except SearchIndexingError as exc:
|
||||
LOGGER.error(u'Search indexing error for library %s - %s', library_id, text_type(exc))
|
||||
LOGGER.error('Search indexing error for library %s - %s', library_id, str(exc))
|
||||
else:
|
||||
LOGGER.debug(u'Search indexing successful for library %s', library_id)
|
||||
LOGGER.debug('Search indexing successful for library %s', library_id)
|
||||
|
||||
|
||||
class CourseExportTask(UserTask): # pylint: disable=abstract-method
|
||||
@@ -244,8 +244,8 @@ class CourseExportTask(UserTask): # pylint: disable=abstract-method
|
||||
Returns:
|
||||
text_type: The generated name
|
||||
"""
|
||||
key = arguments_dict[u'course_key_string']
|
||||
return u'Export of {}'.format(key)
|
||||
key = arguments_dict['course_key_string']
|
||||
return f'Export of {key}'
|
||||
|
||||
|
||||
@shared_task(base=CourseExportTask, bind=True)
|
||||
@@ -262,11 +262,11 @@ def export_olx(self, user_id, course_key_string, language):
|
||||
user = User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
with translation_language(language):
|
||||
self.status.fail(_(u'Unknown User ID: {0}').format(user_id))
|
||||
self.status.fail(_('Unknown User ID: {0}').format(user_id))
|
||||
return
|
||||
if not has_course_author_access(user, courselike_key):
|
||||
with translation_language(language):
|
||||
self.status.fail(_(u'Permission denied'))
|
||||
self.status.fail(_('Permission denied'))
|
||||
return
|
||||
|
||||
if isinstance(courselike_key, LibraryLocator):
|
||||
@@ -275,16 +275,16 @@ def export_olx(self, user_id, course_key_string, language):
|
||||
courselike_module = modulestore().get_course(courselike_key)
|
||||
|
||||
try:
|
||||
self.status.set_state(u'Exporting')
|
||||
self.status.set_state('Exporting')
|
||||
tarball = create_export_tarball(courselike_module, courselike_key, {}, self.status)
|
||||
artifact = UserTaskArtifact(status=self.status, name=u'Output')
|
||||
artifact = UserTaskArtifact(status=self.status, name='Output')
|
||||
artifact.file.save(name=os.path.basename(tarball.name), content=File(tarball))
|
||||
artifact.save()
|
||||
# catch all exceptions so we can record useful error messages
|
||||
except Exception as exception: # pylint: disable=broad-except
|
||||
LOGGER.exception(u'Error exporting course %s', courselike_key, exc_info=True)
|
||||
LOGGER.exception('Error exporting course %s', courselike_key, exc_info=True)
|
||||
if self.status.state != UserTaskStatus.FAILED:
|
||||
self.status.fail({'raw_error_msg': text_type(exception)})
|
||||
self.status.fail({'raw_error_msg': str(exception)})
|
||||
return
|
||||
|
||||
|
||||
@@ -305,14 +305,14 @@ def create_export_tarball(course_module, course_key, context, status=None):
|
||||
export_course_to_xml(modulestore(), contentstore(), course_module.id, root_dir, name)
|
||||
|
||||
if status:
|
||||
status.set_state(u'Compressing')
|
||||
status.set_state('Compressing')
|
||||
status.increment_completed_steps()
|
||||
LOGGER.debug(u'tar file being generated at %s', export_file.name)
|
||||
LOGGER.debug('tar file being generated at %s', export_file.name)
|
||||
with tarfile.open(name=export_file.name, mode='w:gz') as tar_file:
|
||||
tar_file.add(root_dir / name, arcname=name)
|
||||
|
||||
except SerializationError as exc:
|
||||
LOGGER.exception(u'There was an error exporting %s', course_key, exc_info=True)
|
||||
LOGGER.exception('There was an error exporting %s', course_key, exc_info=True)
|
||||
parent = None
|
||||
try:
|
||||
failed_item = modulestore().get_item(exc.location)
|
||||
@@ -334,7 +334,7 @@ def create_export_tarball(course_module, course_key, context, status=None):
|
||||
'edit_unit_url': context['edit_unit_url']}))
|
||||
raise
|
||||
except Exception as exc:
|
||||
LOGGER.exception(u'There was an error exporting %s', course_key, exc_info=True)
|
||||
LOGGER.exception('There was an error exporting %s', course_key, exc_info=True)
|
||||
context.update({
|
||||
'in_err': True,
|
||||
'edit_unit_url': None,
|
||||
@@ -378,9 +378,9 @@ class CourseImportTask(UserTask): # pylint: disable=abstract-method
|
||||
Returns:
|
||||
text_type: The generated name
|
||||
"""
|
||||
key = arguments_dict[u'course_key_string']
|
||||
filename = arguments_dict[u'archive_name']
|
||||
return u'Import of {} from {}'.format(key, filename)
|
||||
key = arguments_dict['course_key_string']
|
||||
filename = arguments_dict['archive_name']
|
||||
return f'Import of {key} from {filename}'
|
||||
|
||||
|
||||
@shared_task(base=CourseImportTask, bind=True)
|
||||
@@ -396,11 +396,11 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan
|
||||
user = User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
with translation_language(language):
|
||||
self.status.fail(_(u'Unknown User ID: {0}').format(user_id))
|
||||
self.status.fail(_('Unknown User ID: {0}').format(user_id))
|
||||
return
|
||||
if not has_course_author_access(user, courselike_key):
|
||||
with translation_language(language):
|
||||
self.status.fail(_(u'Permission denied'))
|
||||
self.status.fail(_('Permission denied'))
|
||||
return
|
||||
|
||||
is_library = isinstance(courselike_key, LibraryLocator)
|
||||
@@ -420,24 +420,24 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan
|
||||
subdir = base64.urlsafe_b64encode(repr(courselike_key).encode('utf-8')).decode('utf-8')
|
||||
course_dir = data_root / subdir
|
||||
try:
|
||||
self.status.set_state(u'Unpacking')
|
||||
self.status.set_state('Unpacking')
|
||||
|
||||
if not archive_name.endswith(u'.tar.gz'):
|
||||
if not archive_name.endswith('.tar.gz'):
|
||||
with translation_language(language):
|
||||
self.status.fail(_(u'We only support uploading a .tar.gz file.'))
|
||||
self.status.fail(_('We only support uploading a .tar.gz file.'))
|
||||
return
|
||||
|
||||
temp_filepath = course_dir / get_valid_filename(archive_name)
|
||||
if not course_dir.isdir():
|
||||
os.mkdir(course_dir)
|
||||
|
||||
LOGGER.debug(u'importing course to {0}'.format(temp_filepath))
|
||||
LOGGER.debug(f'importing course to {temp_filepath}')
|
||||
|
||||
# Copy the OLX archive from where it was uploaded to (S3, Swift, file system, etc.)
|
||||
if not course_import_export_storage.exists(archive_path):
|
||||
LOGGER.info(u'Course import %s: Uploaded file %s not found', courselike_key, archive_path)
|
||||
LOGGER.info('Course import %s: Uploaded file %s not found', courselike_key, archive_path)
|
||||
with translation_language(language):
|
||||
self.status.fail(_(u'Tar file not found'))
|
||||
self.status.fail(_('Tar file not found'))
|
||||
return
|
||||
with course_import_export_storage.open(archive_path, 'rb') as source:
|
||||
with open(temp_filepath, 'wb') as destination:
|
||||
@@ -448,7 +448,7 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan
|
||||
return source.read(FILE_READ_CHUNK)
|
||||
for chunk in iter(read_chunk, b''):
|
||||
destination.write(chunk)
|
||||
LOGGER.info(u'Course import %s: Download from storage complete', courselike_key)
|
||||
LOGGER.info('Course import %s: Download from storage complete', courselike_key)
|
||||
# Delete from source location
|
||||
course_import_export_storage.delete(archive_path)
|
||||
|
||||
@@ -456,40 +456,40 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan
|
||||
# current course state before import.
|
||||
if is_course:
|
||||
if courselike_module.entrance_exam_enabled:
|
||||
fake_request = RequestFactory().get(u'/')
|
||||
fake_request = RequestFactory().get('/')
|
||||
fake_request.user = user
|
||||
from .views.entrance_exam import remove_entrance_exam_milestone_reference
|
||||
# TODO: Is this really ok? Seems dangerous for a live course
|
||||
remove_entrance_exam_milestone_reference(fake_request, courselike_key)
|
||||
LOGGER.info(
|
||||
u'entrance exam milestone content reference for course %s has been removed',
|
||||
'entrance exam milestone content reference for course %s has been removed',
|
||||
courselike_module.id
|
||||
)
|
||||
# Send errors to client with stage at which error occurred.
|
||||
except Exception as exception: # pylint: disable=broad-except
|
||||
if course_dir.isdir():
|
||||
shutil.rmtree(course_dir)
|
||||
LOGGER.info(u'Course import %s: Temp data cleared', courselike_key)
|
||||
LOGGER.info('Course import %s: Temp data cleared', courselike_key)
|
||||
|
||||
LOGGER.exception(u'Error importing course %s', courselike_key, exc_info=True)
|
||||
self.status.fail(text_type(exception))
|
||||
LOGGER.exception('Error importing course %s', courselike_key, exc_info=True)
|
||||
self.status.fail(str(exception))
|
||||
return
|
||||
|
||||
# try-finally block for proper clean up after receiving file.
|
||||
try:
|
||||
tar_file = tarfile.open(temp_filepath)
|
||||
try:
|
||||
safetar_extractall(tar_file, (course_dir + u'/'))
|
||||
safetar_extractall(tar_file, (course_dir + '/'))
|
||||
except SuspiciousOperation as exc:
|
||||
LOGGER.info(u'Course import %s: Unsafe tar file - %s', courselike_key, exc.args[0])
|
||||
LOGGER.info('Course import %s: Unsafe tar file - %s', courselike_key, exc.args[0])
|
||||
with translation_language(language):
|
||||
self.status.fail(_(u'Unsafe tar file. Aborting import.'))
|
||||
self.status.fail(_('Unsafe tar file. Aborting import.'))
|
||||
return
|
||||
finally:
|
||||
tar_file.close()
|
||||
|
||||
LOGGER.info(u'Course import %s: Uploaded file extracted', courselike_key)
|
||||
self.status.set_state(u'Verifying')
|
||||
LOGGER.info('Course import %s: Uploaded file extracted', courselike_key)
|
||||
self.status.set_state('Verifying')
|
||||
self.status.increment_completed_steps()
|
||||
|
||||
# find the 'course.xml' file
|
||||
@@ -516,14 +516,14 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan
|
||||
dirpath = get_dir_for_filename(course_dir, root_name)
|
||||
if not dirpath:
|
||||
with translation_language(language):
|
||||
self.status.fail(_(u'Could not find the {0} file in the package.').format(root_name))
|
||||
self.status.fail(_('Could not find the {0} file in the package.').format(root_name))
|
||||
return
|
||||
|
||||
dirpath = os.path.relpath(dirpath, data_root)
|
||||
LOGGER.debug(u'found %s at %s', root_name, dirpath)
|
||||
LOGGER.debug('found %s at %s', root_name, dirpath)
|
||||
|
||||
LOGGER.info(u'Course import %s: Extracted file verified', courselike_key)
|
||||
self.status.set_state(u'Updating')
|
||||
LOGGER.info('Course import %s: Extracted file verified', courselike_key)
|
||||
self.status.set_state('Updating')
|
||||
self.status.increment_completed_steps()
|
||||
|
||||
courselike_items = import_func(
|
||||
@@ -535,32 +535,32 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan
|
||||
)
|
||||
|
||||
new_location = courselike_items[0].location
|
||||
LOGGER.debug(u'new course at %s', new_location)
|
||||
LOGGER.debug('new course at %s', new_location)
|
||||
|
||||
LOGGER.info(u'Course import %s: Course import successful', courselike_key)
|
||||
LOGGER.info('Course import %s: Course import successful', courselike_key)
|
||||
except Exception as exception: # pylint: disable=broad-except
|
||||
LOGGER.exception(u'error importing course', exc_info=True)
|
||||
self.status.fail(text_type(exception))
|
||||
LOGGER.exception('error importing course', exc_info=True)
|
||||
self.status.fail(str(exception))
|
||||
finally:
|
||||
if course_dir.isdir():
|
||||
shutil.rmtree(course_dir)
|
||||
LOGGER.info(u'Course import %s: Temp data cleared', courselike_key)
|
||||
LOGGER.info('Course import %s: Temp data cleared', courselike_key)
|
||||
|
||||
if self.status.state == u'Updating' and is_course:
|
||||
if self.status.state == 'Updating' and is_course:
|
||||
# Reload the course so we have the latest state
|
||||
course = modulestore().get_course(courselike_key)
|
||||
if course.entrance_exam_enabled:
|
||||
entrance_exam_chapter = modulestore().get_items(
|
||||
course.id,
|
||||
qualifiers={u'category': u'chapter'},
|
||||
settings={u'is_entrance_exam': True}
|
||||
qualifiers={'category': 'chapter'},
|
||||
settings={'is_entrance_exam': True}
|
||||
)[0]
|
||||
|
||||
metadata = {u'entrance_exam_id': text_type(entrance_exam_chapter.location)}
|
||||
metadata = {'entrance_exam_id': str(entrance_exam_chapter.location)}
|
||||
CourseMetadata.update_from_dict(metadata, course, user)
|
||||
from .views.entrance_exam import add_entrance_exam_milestone
|
||||
add_entrance_exam_milestone(course.id, entrance_exam_chapter)
|
||||
LOGGER.info(u'Course %s Entrance exam imported', course.id)
|
||||
LOGGER.info('Course %s Entrance exam imported', course.id)
|
||||
|
||||
|
||||
@shared_task
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
CMS feature toggles.
|
||||
"""
|
||||
from edx_toggles.toggles import SettingDictToggle, LegacyWaffleFlag, LegacyWaffleFlagNamespace
|
||||
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace, SettingDictToggle
|
||||
|
||||
# .. toggle_name: FEATURES['ENABLE_EXPORT_GIT']
|
||||
# .. toggle_implementation: SettingDictToggle
|
||||
@@ -20,7 +20,7 @@ EXPORT_GIT = SettingDictToggle(
|
||||
|
||||
# Namespace for studio dashboard waffle flags.
|
||||
WAFFLE_NAMESPACE = 'contentstore'
|
||||
WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Contentstore: ')
|
||||
WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix='Contentstore: ')
|
||||
|
||||
# Waffle flag to split library to new view.
|
||||
# .. toggle_name: split_library_on_studio_dashboard
|
||||
|
||||
@@ -7,7 +7,6 @@ import logging
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils import translation
|
||||
@@ -15,17 +14,16 @@ from django.utils.translation import ugettext as _
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.locator import LibraryLocator
|
||||
from pytz import UTC
|
||||
from six import text_type
|
||||
|
||||
from common.djangoapps.student import auth
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from openedx.core.djangoapps.django_comment_common.models import assign_default_role
|
||||
from openedx.core.djangoapps.django_comment_common.utils import seed_permissions_roles
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
|
||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME
|
||||
from common.djangoapps.student import auth
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -100,7 +98,7 @@ def _remove_instructors(course_key):
|
||||
try:
|
||||
remove_all_instructors(course_key)
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except
|
||||
log.error(u"Error in deleting course groups for {0}: {1}".format(course_key, err))
|
||||
log.error(f"Error in deleting course groups for {course_key}: {err}")
|
||||
|
||||
|
||||
def get_lms_link_for_item(location, preview=False):
|
||||
@@ -132,10 +130,10 @@ def get_lms_link_for_item(location, preview=False):
|
||||
settings.FEATURES.get('PREVIEW_LMS_BASE')
|
||||
)
|
||||
|
||||
return u"//{lms_base}/courses/{course_key}/jump_to/{location}".format(
|
||||
return "//{lms_base}/courses/{course_key}/jump_to/{location}".format(
|
||||
lms_base=lms_base,
|
||||
course_key=text_type(location.course_key),
|
||||
location=text_type(location),
|
||||
course_key=str(location.course_key),
|
||||
location=str(location),
|
||||
)
|
||||
|
||||
|
||||
@@ -151,9 +149,9 @@ def get_lms_link_for_certificate_web_view(course_key, mode):
|
||||
if lms_base is None:
|
||||
return None
|
||||
|
||||
return u"//{certificate_web_base}/certificates/course/{course_id}?preview={mode}".format(
|
||||
return "//{certificate_web_base}/certificates/course/{course_id}?preview={mode}".format(
|
||||
certificate_web_base=lms_base,
|
||||
course_id=six.text_type(course_key),
|
||||
course_id=str(course_key),
|
||||
mode=mode
|
||||
)
|
||||
|
||||
@@ -292,7 +290,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: six.text_type(key_value)} if key_name else None
|
||||
kwargs_for_reverse = {key_name: str(key_value)} if key_name else None
|
||||
if kwargs:
|
||||
kwargs_for_reverse.update(kwargs)
|
||||
return reverse(handler_name, kwargs=kwargs_for_reverse)
|
||||
@@ -332,7 +330,7 @@ def get_split_group_display_name(xblock, course):
|
||||
"""
|
||||
for user_partition in get_user_partition_info(xblock, schemes=['random'], course=course):
|
||||
for group in user_partition['groups']:
|
||||
if u'Group ID {group_id}'.format(group_id=group['id']) == xblock.display_name_with_default:
|
||||
if 'Group ID {group_id}'.format(group_id=group['id']) == xblock.display_name_with_default:
|
||||
return group['name']
|
||||
|
||||
|
||||
@@ -400,7 +398,7 @@ def get_user_partition_info(xblock, schemes=None, course=None):
|
||||
|
||||
if course is None:
|
||||
log.warning(
|
||||
u"Could not find course %s to retrieve user partition information",
|
||||
"Could not find course %s to retrieve user partition information",
|
||||
xblock.location.course_key
|
||||
)
|
||||
return []
|
||||
@@ -432,7 +430,7 @@ def get_user_partition_info(xblock, schemes=None, course=None):
|
||||
})
|
||||
|
||||
# Next, add any groups set on the XBlock that have been deleted
|
||||
all_groups = set(g.id for g in p.groups)
|
||||
all_groups = {g.id for g in p.groups}
|
||||
missing_group_ids = selected_groups - all_groups
|
||||
for gid in missing_group_ids:
|
||||
groups.append({
|
||||
@@ -445,7 +443,7 @@ def get_user_partition_info(xblock, schemes=None, course=None):
|
||||
# Put together the entire partition dictionary
|
||||
partitions.append({
|
||||
"id": p.id,
|
||||
"name": six.text_type(p.name), # Convert into a string in case ugettext_lazy was used
|
||||
"name": str(p.name), # Convert into a string in case ugettext_lazy was used
|
||||
"scheme": p.scheme.name,
|
||||
"groups": groups,
|
||||
})
|
||||
@@ -502,7 +500,7 @@ def get_visibility_partition_info(xblock, course=None):
|
||||
else:
|
||||
# Translators: This is building up a list of groups. It is marked for translation because of the
|
||||
# comma, which is used as a separator between each group.
|
||||
selected_groups_label = _(u'{previous_groups}, {current_group}').format(
|
||||
selected_groups_label = _('{previous_groups}, {current_group}').format(
|
||||
previous_groups=selected_groups_label,
|
||||
current_group=group['name']
|
||||
)
|
||||
@@ -527,7 +525,7 @@ def get_xblock_aside_instance(usage_key):
|
||||
if aside.scope_ids.block_type == usage_key.aside_type:
|
||||
return aside
|
||||
except ItemNotFoundError:
|
||||
log.warning(u'Unable to load item %s', usage_key.usage_key)
|
||||
log.warning('Unable to load item %s', usage_key.usage_key)
|
||||
|
||||
|
||||
def is_self_paced(course):
|
||||
@@ -567,7 +565,7 @@ def get_sibling_urls(subsection):
|
||||
# section.get_parent SHOULD return the course, but for some reason, it might not
|
||||
sections = section.get_parent().get_children()
|
||||
except AttributeError:
|
||||
log.error(u"URL Retrieval Error # 1: subsection {subsection} included in section {section}".format(
|
||||
log.error("URL Retrieval Error # 1: subsection {subsection} included in section {section}".format(
|
||||
section=section.location,
|
||||
subsection=subsection.location
|
||||
))
|
||||
@@ -583,7 +581,7 @@ def get_sibling_urls(subsection):
|
||||
try:
|
||||
sections = section.get_parent().get_children()
|
||||
except AttributeError:
|
||||
log.error(u"URL Retrieval Error # 2: subsection {subsection} included in section {section}".format(
|
||||
log.error("URL Retrieval Error # 2: subsection {subsection} included in section {section}".format(
|
||||
section=section.location,
|
||||
subsection=subsection.location
|
||||
))
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
"""
|
||||
Utils related to the videos.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
from urllib.parse import urljoin
|
||||
|
||||
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
|
||||
|
||||
# Youtube thumbnail sizes.
|
||||
# https://img.youtube.com/vi/{youtube_id}/{thumbnail_quality}.jpg
|
||||
@@ -43,15 +41,15 @@ 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 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(
|
||||
error = _('This image file type is not supported. Supported file types are {supported_file_formats}.').format(
|
||||
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(
|
||||
error = _('This image file must be smaller than {image_max_size}.').format(
|
||||
image_max_size=settings.VIDEO_IMAGE_MAX_FILE_SIZE_MB
|
||||
)
|
||||
elif image_file.size < settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES']:
|
||||
error = _(u'This image file must be larger than {image_min_size}.').format(
|
||||
error = _('This image file must be larger than {image_min_size}.').format(
|
||||
image_min_size=settings.VIDEO_IMAGE_MIN_FILE_SIZE_KB
|
||||
)
|
||||
else:
|
||||
@@ -63,15 +61,15 @@ def validate_video_image(image_file, skip_aspect_ratio=False):
|
||||
return _('There is a problem with this image file. Try to upload a different file.')
|
||||
image_file_aspect_ratio = abs(image_file_width / float(image_file_height) - settings.VIDEO_IMAGE_ASPECT_RATIO)
|
||||
if image_file_width < settings.VIDEO_IMAGE_MIN_WIDTH or image_file_height < settings.VIDEO_IMAGE_MIN_HEIGHT:
|
||||
error = _(u'Recommended image resolution is {image_file_max_width}x{image_file_max_height}. '
|
||||
u'The minimum resolution is {image_file_min_width}x{image_file_min_height}.').format(
|
||||
error = _('Recommended image resolution is {image_file_max_width}x{image_file_max_height}. '
|
||||
'The minimum resolution is {image_file_min_width}x{image_file_min_height}.').format(
|
||||
image_file_max_width=settings.VIDEO_IMAGE_MAX_WIDTH,
|
||||
image_file_max_height=settings.VIDEO_IMAGE_MAX_HEIGHT,
|
||||
image_file_min_width=settings.VIDEO_IMAGE_MIN_WIDTH,
|
||||
image_file_min_height=settings.VIDEO_IMAGE_MIN_HEIGHT
|
||||
)
|
||||
elif not skip_aspect_ratio and image_file_aspect_ratio > settings.VIDEO_IMAGE_ASPECT_RATIO_ERROR_MARGIN:
|
||||
error = _(u'This image file must have an aspect ratio of {video_image_aspect_ratio_text}.').format(
|
||||
error = _('This image file must have an aspect ratio of {video_image_aspect_ratio_text}.').format(
|
||||
video_image_aspect_ratio_text=settings.VIDEO_IMAGE_ASPECT_RATIO_TEXT
|
||||
)
|
||||
else:
|
||||
@@ -108,7 +106,7 @@ def validate_and_update_video_image(course_key_string, edx_video_id, image_file,
|
||||
error = validate_video_image(image_file, skip_aspect_ratio=True)
|
||||
if error:
|
||||
LOGGER.info(
|
||||
u'VIDEOS: Scraping youtube video thumbnail failed for edx_video_id [%s] in course [%s] with error: %s',
|
||||
'VIDEOS: Scraping youtube video thumbnail failed for edx_video_id [%s] in course [%s] with error: %s',
|
||||
edx_video_id,
|
||||
course_key_string,
|
||||
error
|
||||
@@ -117,7 +115,7 @@ def validate_and_update_video_image(course_key_string, edx_video_id, image_file,
|
||||
|
||||
update_video_image(edx_video_id, course_key_string, image_file, image_filename)
|
||||
LOGGER.info(
|
||||
u'VIDEOS: Scraping youtube video thumbnail for edx_video_id [%s] in course [%s]', edx_video_id, course_key_string # lint-amnesty, pylint: disable=line-too-long
|
||||
'VIDEOS: Scraping youtube video thumbnail for edx_video_id [%s] in course [%s]', edx_video_id, course_key_string # lint-amnesty, pylint: disable=line-too-long
|
||||
)
|
||||
|
||||
|
||||
@@ -128,7 +126,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 six.iteritems(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS)}
|
||||
supported_content_types = {v: k for k, v in settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.items()}
|
||||
image_filename = '{youtube_id}{image_extention}'.format(
|
||||
youtube_id=youtube_id,
|
||||
image_extention=supported_content_types.get(
|
||||
|
||||
Reference in New Issue
Block a user