Merge commit 'bbadf48b8e19a0c9f1e1ca750f5ad8b26856bcc1' into feanil/merge_more_of_master
Sorry for the merge commits but release was in a really bad state and so we need to get changes out piecemeal on the release-candidate. Slowly mergig master into RC.
This commit is contained in:
@@ -67,7 +67,7 @@ class Command(BaseCommand):
|
||||
|
||||
def _get_results(self, filename):
|
||||
"""Load results from file"""
|
||||
with open(filename) as f:
|
||||
with open(filename, 'rb') as f: # pylint: disable=open-builtin
|
||||
results = f.read()
|
||||
os.remove(filename)
|
||||
return results
|
||||
|
||||
@@ -89,6 +89,13 @@ class TestCourseExportOlx(ModuleStoreTestCase):
|
||||
with tarfile.open(filename) as tar_file:
|
||||
self.check_export_file(tar_file, test_course_key)
|
||||
|
||||
# There is a bug in the underlying management/base code that tries to make
|
||||
# all manageent command output be unicode. This management command
|
||||
# outputs the binary tar file data and so breaks in python3. In python2
|
||||
# the code is happy to pass bytes back and forth and in later versions of
|
||||
# django this is fixed. Howevere it's not possible to get this test to
|
||||
# pass in Python3 and django 1.11
|
||||
@unittest.skip("Bug in django 1.11 prevents this from working in python3. Re-enable after django 2.x upgrade.")
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_export_course_stdout(self, store_type):
|
||||
test_course_key = self.create_dummy_course(store_type)
|
||||
|
||||
@@ -749,7 +749,7 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan
|
||||
# Locate the uploaded OLX archive (and download it from S3 if necessary)
|
||||
# Do everything in a try-except block to make sure everything is properly cleaned up.
|
||||
data_root = path(settings.GITHUB_REPO_ROOT)
|
||||
subdir = base64.urlsafe_b64encode(repr(courselike_key))
|
||||
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')
|
||||
|
||||
@@ -32,6 +32,8 @@ class FakeTranslations(ModuleI18nService):
|
||||
"""
|
||||
return self.translations.get(msgid, msgid)
|
||||
|
||||
gettext = ugettext
|
||||
|
||||
@staticmethod
|
||||
def translator(locales_map): # pylint: disable=method-hidden
|
||||
"""Build mock translator for the given locales.
|
||||
@@ -123,6 +125,7 @@ class TestModuleI18nService(ModuleStoreTestCase):
|
||||
self.assertEqual(i18n_service.ugettext(self.test_language), 'dummy language')
|
||||
|
||||
@mock.patch('django.utils.translation.ugettext', mock.Mock(return_value='XYZ-TEST-LANGUAGE'))
|
||||
@mock.patch('django.utils.translation.gettext', mock.Mock(return_value='XYZ-TEST-LANGUAGE'))
|
||||
def test_django_translator_in_use_with_empty_block(self):
|
||||
"""
|
||||
Test: Django default translator should in use if we have an empty block
|
||||
|
||||
@@ -319,7 +319,7 @@ class ScrapeVideoThumbnailsTestCase(CourseTestCase):
|
||||
)
|
||||
),
|
||||
(
|
||||
'dummy-content',
|
||||
b'dummy-content',
|
||||
None,
|
||||
u'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())
|
||||
|
||||
@@ -335,8 +335,10 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
is turned off
|
||||
"""
|
||||
response = self.client.get(reverse('login'))
|
||||
self.assertNotIn('<a href="/signup" class="action action-signin">Don't have a Studio Account? Sign up!</a>',
|
||||
response.content)
|
||||
self.assertNotContains(
|
||||
response,
|
||||
'<a href="/signup" class="action action-signin">Don't have a Studio Account? Sign up!</a>'
|
||||
)
|
||||
|
||||
|
||||
class ForumTestCase(CourseTestCase):
|
||||
|
||||
@@ -117,7 +117,7 @@ def _write_chunk(request, courselike_key):
|
||||
"""
|
||||
# Upload .tar.gz to local filesystem for one-server installations not using S3 or Swift
|
||||
data_root = path(settings.GITHUB_REPO_ROOT)
|
||||
subdir = base64.urlsafe_b64encode(repr(courselike_key))
|
||||
subdir = base64.urlsafe_b64encode(repr(courselike_key).encode('utf-8')).decode('utf-8')
|
||||
course_dir = data_root / subdir
|
||||
filename = request.FILES['course-data'].name
|
||||
|
||||
|
||||
@@ -295,8 +295,8 @@ class CertificatesListHandlerTestCase(
|
||||
|
||||
# in html response
|
||||
result = self.client.get_html(self._url())
|
||||
self.assertIn('Test certificate', result.content)
|
||||
self.assertIn('Test description', result.content)
|
||||
self.assertContains(result, 'Test certificate')
|
||||
self.assertContains(result, 'Test description')
|
||||
|
||||
# in JSON response
|
||||
response = self.client.get_json(self._url())
|
||||
@@ -320,7 +320,7 @@ class CertificatesListHandlerTestCase(
|
||||
|
||||
# in html response
|
||||
result = self.client.get_html(self._url())
|
||||
self.assertNotIn('Test certificate', result.content)
|
||||
self.assertNotContains(result, 'Test certificate')
|
||||
|
||||
def test_unsupported_http_accept_header(self):
|
||||
"""
|
||||
|
||||
@@ -62,7 +62,7 @@ class CourseUpdateTest(CourseTestCase):
|
||||
# refetch using provided id
|
||||
refetched = self.client.get_json(first_update_url)
|
||||
self.assertHTMLEqual(
|
||||
content, json.loads(refetched.content)['content'], "get w/ provided id"
|
||||
content, json.loads(refetched.content.decode('utf-8'))['content'], "get w/ provided id"
|
||||
)
|
||||
|
||||
# now put in an evil update
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestSubsectionGating(CourseTestCase):
|
||||
{'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)
|
||||
resp = json.loads(self.client.get_json(self.seq2_url).content.decode('utf-8'))
|
||||
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)
|
||||
|
||||
@@ -94,7 +94,7 @@ class ImportEntranceExamTestCase(CourseTestCase, MilestonesTestCaseMixin):
|
||||
self.assertIsNotNone(course)
|
||||
self.assertEquals(course.entrance_exam_enabled, False)
|
||||
|
||||
with open(self.entrance_exam_tar) as gtar:
|
||||
with open(self.entrance_exam_tar, 'rb') as gtar: # pylint: disable=open-builtin
|
||||
args = {"name": self.entrance_exam_tar, "course-data": [gtar]}
|
||||
resp = self.client.post(self.url, args)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
@@ -126,7 +126,7 @@ class ImportEntranceExamTestCase(CourseTestCase, MilestonesTestCaseMixin):
|
||||
self.assertTrue(len(content_milestones))
|
||||
|
||||
# Now import entrance exam course
|
||||
with open(self.entrance_exam_tar) as gtar:
|
||||
with open(self.entrance_exam_tar, 'rb') as gtar: # pylint: disable=open-builtin
|
||||
args = {"name": self.entrance_exam_tar, "course-data": [gtar]}
|
||||
resp = self.client.post(self.url, args)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
@@ -186,7 +186,7 @@ class ImportTestCase(CourseTestCase):
|
||||
Check that the response for a tar.gz import without a course.xml is
|
||||
correct.
|
||||
"""
|
||||
with open(self.bad_tar) as btar:
|
||||
with open(self.bad_tar, 'rb') as btar: # pylint: disable=open-builtin
|
||||
resp = self.client.post(
|
||||
self.url,
|
||||
{
|
||||
@@ -204,14 +204,14 @@ class ImportTestCase(CourseTestCase):
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEquals(json.loads(resp_status.content)["ImportStatus"], -2)
|
||||
self.assertEquals(json.loads(resp_status.content.decode('utf-8'))["ImportStatus"], -2)
|
||||
|
||||
def test_with_coursexml(self):
|
||||
"""
|
||||
Check that the response for a tar.gz import with a course.xml is
|
||||
correct.
|
||||
"""
|
||||
with open(self.good_tar) as gtar:
|
||||
with open(self.good_tar, 'rb') as gtar: # pylint: disable=open-builtin
|
||||
args = {"name": self.good_tar, "course-data": [gtar]}
|
||||
resp = self.client.post(self.url, args)
|
||||
|
||||
@@ -230,7 +230,7 @@ class ImportTestCase(CourseTestCase):
|
||||
display_name_before_import = course.display_name
|
||||
|
||||
# Check that global staff user can import course
|
||||
with open(self.good_tar) as gtar:
|
||||
with open(self.good_tar, 'rb') as gtar: # pylint: disable=open-builtin
|
||||
args = {"name": self.good_tar, "course-data": [gtar]}
|
||||
resp = self.client.post(self.url, args)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
@@ -248,7 +248,7 @@ class ImportTestCase(CourseTestCase):
|
||||
|
||||
# Now course staff user can also successfully import course
|
||||
self.client.login(username=nonstaff_user.username, password='foo')
|
||||
with open(self.good_tar) as gtar:
|
||||
with open(self.good_tar, 'rb') as gtar: # pylint: disable=open-builtin
|
||||
args = {"name": self.good_tar, "course-data": [gtar]}
|
||||
resp = self.client.post(self.url, args)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
@@ -342,7 +342,7 @@ class ImportTestCase(CourseTestCase):
|
||||
|
||||
def try_tar(tarpath):
|
||||
""" Attempt to tar an unacceptable file """
|
||||
with open(tarpath) as tar:
|
||||
with open(tarpath, 'rb') as tar: # pylint: disable=open-builtin
|
||||
args = {"name": tarpath, "course-data": [tar]}
|
||||
resp = self.client.post(self.url, args)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
@@ -376,7 +376,7 @@ class ImportTestCase(CourseTestCase):
|
||||
kwargs={'filename': os.path.split(self.good_tar)[1]}
|
||||
)
|
||||
)
|
||||
import_status = json.loads(resp_status.content)["ImportStatus"]
|
||||
import_status = json.loads(resp_status.content.decode('utf-8'))["ImportStatus"]
|
||||
self.assertIn(import_status, (0, 3))
|
||||
|
||||
def test_library_import(self):
|
||||
|
||||
@@ -307,7 +307,7 @@ class TranscriptUploadTest(CourseTestCase):
|
||||
"""
|
||||
Tests that transcript upload handler works as expected.
|
||||
"""
|
||||
transcript_file_stream = BytesIO('0\n00:00:00,010 --> 00:00:00,100\nПривіт, edX вітає вас.\n\n')
|
||||
transcript_file_stream = six.StringIO('0\n00:00:00,010 --> 00:00:00,100\nПривіт, edX вітає вас.\n\n')
|
||||
# Make request to transcript upload handler
|
||||
response = self.client.post(
|
||||
self.view_url,
|
||||
@@ -422,7 +422,7 @@ class TranscriptUploadTest(CourseTestCase):
|
||||
"""
|
||||
Tests the transcript upload handler with an invalid transcript file.
|
||||
"""
|
||||
transcript_file_stream = BytesIO('An invalid transcript SubRip file content')
|
||||
transcript_file_stream = six.StringIO('An invalid transcript SubRip file content')
|
||||
# Make request to transcript upload handler
|
||||
response = self.client.post(
|
||||
self.view_url,
|
||||
|
||||
@@ -35,7 +35,7 @@ from xmodule.video_module.transcripts_utils import (
|
||||
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
|
||||
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
|
||||
|
||||
SRT_TRANSCRIPT_CONTENT = b"""0
|
||||
SRT_TRANSCRIPT_CONTENT = u"""0
|
||||
00:00:10,500 --> 00:00:13,000
|
||||
Elephant's Dream
|
||||
|
||||
@@ -160,7 +160,7 @@ class TestUploadTranscripts(BaseTranscripts):
|
||||
super(TestUploadTranscripts, self).setUp()
|
||||
self.contents = {
|
||||
'good': SRT_TRANSCRIPT_CONTENT,
|
||||
'bad': b'Some BAD data',
|
||||
'bad': u'Some BAD data',
|
||||
}
|
||||
# Create temporary transcript files
|
||||
self.good_srt_file = self.create_transcript_file(content=self.contents['good'], suffix='.srt')
|
||||
@@ -186,7 +186,7 @@ class TestUploadTranscripts(BaseTranscripts):
|
||||
Setup a transcript file with suffix and content.
|
||||
"""
|
||||
transcript_file = tempfile.NamedTemporaryFile(suffix=suffix)
|
||||
wrapped_content = textwrap.dedent(content.decode('utf-8'))
|
||||
wrapped_content = textwrap.dedent(content)
|
||||
if include_bom:
|
||||
wrapped_content = wrapped_content.encode('utf-8-sig')
|
||||
# Verify that ufeff(BOM) character is in content.
|
||||
@@ -791,7 +791,7 @@ class TestDownloadTranscripts(BaseTranscripts):
|
||||
"""
|
||||
self.assertEqual(response.status_code, expected_status_code)
|
||||
if expected_content:
|
||||
self.assertEqual(response.content, expected_content)
|
||||
assert response.content.decode('utf-8') == expected_content
|
||||
|
||||
def test_download_youtube_transcript_success(self):
|
||||
"""
|
||||
|
||||
@@ -229,7 +229,7 @@ def transcript_upload_handler(request):
|
||||
# Convert SRT transcript into an SJSON format
|
||||
# and upload it to S3.
|
||||
sjson_subs = Transcript.convert(
|
||||
content=transcript_file.read(),
|
||||
content=transcript_file.read().decode('utf-8'),
|
||||
input_format=Transcript.SRT,
|
||||
output_format=Transcript.SJSON
|
||||
)
|
||||
|
||||
@@ -219,7 +219,7 @@ def upload_transcripts(request):
|
||||
# Convert 'srt' transcript into the 'sjson' and upload it to
|
||||
# configured transcript storage. For example, S3.
|
||||
sjson_subs = Transcript.convert(
|
||||
content=transcript_file.read(),
|
||||
content=transcript_file.read().decode('utf-8'),
|
||||
input_format=Transcript.SRT,
|
||||
output_format=Transcript.SJSON
|
||||
)
|
||||
@@ -322,7 +322,7 @@ def check_transcripts(request):
|
||||
filename = 'subs_{0}.srt.sjson'.format(item.sub)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
local_transcripts = contentstore().find(content_location).data
|
||||
local_transcripts = contentstore().find(content_location).data.decode('utf-8')
|
||||
transcripts_presence['current_item_subs'] = item.sub
|
||||
except NotFoundError:
|
||||
pass
|
||||
@@ -336,7 +336,7 @@ def check_transcripts(request):
|
||||
filename = 'subs_{0}.srt.sjson'.format(youtube_id)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
try:
|
||||
local_transcripts = contentstore().find(content_location).data
|
||||
local_transcripts = contentstore().find(content_location).data.decode('utf-8')
|
||||
transcripts_presence['youtube_local'] = True
|
||||
except NotFoundError:
|
||||
log.debug(u"Can't find transcripts in storage for youtube id: %s", youtube_id)
|
||||
|
||||
@@ -37,7 +37,8 @@ from student.models import (
|
||||
RegistrationCookieConfiguration,
|
||||
UserAttribute,
|
||||
UserProfile,
|
||||
UserTestGroup
|
||||
UserTestGroup,
|
||||
BulkUnenrollConfiguration
|
||||
)
|
||||
from student.roles import REGISTERED_ACCESS_ROLES
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -445,6 +446,7 @@ admin.site.register(Registration)
|
||||
admin.site.register(PendingNameChange)
|
||||
admin.site.register(DashboardConfiguration, ConfigurationModelAdmin)
|
||||
admin.site.register(RegistrationCookieConfiguration, ConfigurationModelAdmin)
|
||||
admin.site.register(BulkUnenrollConfiguration, ConfigurationModelAdmin)
|
||||
|
||||
|
||||
# We must first un-register the User model since it may also be registered by the auth app.
|
||||
|
||||
@@ -5,11 +5,9 @@ import logging
|
||||
import unicodecsv
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Q
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from student.models import CourseEnrollment, User
|
||||
from student.models import CourseEnrollment, User, BulkUnenrollConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
||||
|
||||
@@ -27,39 +25,48 @@ class Command(BaseCommand):
|
||||
parser.add_argument('-p', '--csv_path',
|
||||
metavar='csv_path',
|
||||
dest='csv_path',
|
||||
required=True,
|
||||
required=False,
|
||||
help='Path to CSV file.')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
csv_path = options['csv_path']
|
||||
with open(csv_path, 'rb') as csvfile:
|
||||
reader = unicodecsv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
username = row['username']
|
||||
email = row['email']
|
||||
course_key = row['course_id']
|
||||
try:
|
||||
user = User.objects.get(Q(username=username) | Q(email=email))
|
||||
except ObjectDoesNotExist:
|
||||
user = None
|
||||
msg = 'User with username {} or email {} does not exist'.format(username, email)
|
||||
logger.warning(msg)
|
||||
if csv_path:
|
||||
with open(csv_path) as csv_file:
|
||||
self.unenroll_users(csv_file)
|
||||
else:
|
||||
csv_file = BulkUnenrollConfiguration.current().csv_file
|
||||
self.unenroll_users(csv_file)
|
||||
|
||||
try:
|
||||
course_id = CourseKey.from_string(course_key)
|
||||
except InvalidKeyError:
|
||||
course_id = None
|
||||
msg = 'Invalid course id {course_id}, skipping un-enrollement for {username}, {email}'.format(**row)
|
||||
logger.warning(msg)
|
||||
def unenroll_users(self, csv_file):
|
||||
reader = list(unicodecsv.DictReader(csv_file))
|
||||
users_unenrolled = {}
|
||||
for row in reader:
|
||||
username = row['username']
|
||||
course_key = row['course_id']
|
||||
|
||||
if user and course_id:
|
||||
enrollment = CourseEnrollment.get_enrollment(user, course_id)
|
||||
if not enrollment:
|
||||
msg = 'Enrollment for the user {} in course {} does not exist!'.format(username, course_key)
|
||||
logger.info(msg)
|
||||
else:
|
||||
try:
|
||||
CourseEnrollment.unenroll(user, course_id, skip_refund=True)
|
||||
except Exception as err:
|
||||
msg = 'Error un-enrolling User {} from course {}: '.format(username, course_key, err)
|
||||
logger.error(msg, exc_info=True)
|
||||
try:
|
||||
course_id = CourseKey.from_string(row['course_id'])
|
||||
except InvalidKeyError:
|
||||
msg = 'Invalid course id {course_id}, skipping un-enrollement for {username}, {email}'.format(**row)
|
||||
logger.warning(msg)
|
||||
continue
|
||||
|
||||
try:
|
||||
enrollment = CourseEnrollment.objects.get(user__username=username, course_id=course_id)
|
||||
enrollment.update_enrollment(is_active=False, skip_refund=True)
|
||||
if username in users_unenrolled:
|
||||
users_unenrolled[username].append(course_key.encode())
|
||||
else:
|
||||
users_unenrolled[username] = [course_key.encode()]
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
msg = 'Enrollment for the user {} in course {} does not exist!'.format(username, course_key)
|
||||
logger.info(msg)
|
||||
|
||||
except Exception as err:
|
||||
msg = 'Error un-enrolling User {} from course {}: '.format(username, course_key, err)
|
||||
logger.error(msg, exc_info=True)
|
||||
|
||||
logger.info("Following users have been unenrolled successfully from the following courses: {users_unenrolled}"
|
||||
.format(users_unenrolled=["{}:{}".format(k, v) for k, v in users_unenrolled.items()]))
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from django.core.management import call_command
|
||||
from testfixtures import LogCapture
|
||||
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from student.models import CourseEnrollment
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.management import call_command
|
||||
from student.models import CourseEnrollment, BulkUnenrollConfiguration
|
||||
from student.tests.factories import UserFactory
|
||||
from testfixtures import LogCapture
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -47,21 +47,6 @@ class BulkUnenrollTests(SharedModuleStoreTestCase):
|
||||
csv.seek(0)
|
||||
return csv
|
||||
|
||||
def test_user_not_exist(self):
|
||||
"""Verify that warning user not exist is logged for non existing user."""
|
||||
with NamedTemporaryFile() as csv:
|
||||
csv = self._write_test_csv(csv, lines=["111,test,test@example.com,course-v1:edX+DemoX+Demo_Course\n"])
|
||||
|
||||
with LogCapture(LOGGER_NAME) as log:
|
||||
call_command("bulk_unenroll", "--csv_path={}".format(csv.name))
|
||||
log.check(
|
||||
(
|
||||
LOGGER_NAME,
|
||||
'WARNING',
|
||||
'User with username {} or email {} does not exist'.format('test', 'test@example.com')
|
||||
)
|
||||
)
|
||||
|
||||
def test_invalid_course_key(self):
|
||||
"""Verify in case of invalid course key warning is logged."""
|
||||
with NamedTemporaryFile() as csv:
|
||||
@@ -69,13 +54,11 @@ class BulkUnenrollTests(SharedModuleStoreTestCase):
|
||||
|
||||
with LogCapture(LOGGER_NAME) as log:
|
||||
call_command("bulk_unenroll", "--csv_path={}".format(csv.name))
|
||||
log.check(
|
||||
(
|
||||
LOGGER_NAME,
|
||||
'WARNING',
|
||||
'Invalid course id {}, skipping un-enrollement for {}, {}'.format(
|
||||
'test_course', 'amy', 'amy@pond.com')
|
||||
)
|
||||
expected_message = 'Invalid course id {}, skipping un-enrollement for {}, {}'.\
|
||||
format('test_course', 'amy', 'amy@pond.com')
|
||||
|
||||
log.check_present(
|
||||
(LOGGER_NAME, 'WARNING', expected_message)
|
||||
)
|
||||
|
||||
def test_user_not_enrolled(self):
|
||||
@@ -85,13 +68,11 @@ class BulkUnenrollTests(SharedModuleStoreTestCase):
|
||||
|
||||
with LogCapture(LOGGER_NAME) as log:
|
||||
call_command("bulk_unenroll", "--csv_path={}".format(csv.name))
|
||||
log.check(
|
||||
(
|
||||
LOGGER_NAME,
|
||||
'INFO',
|
||||
'Enrollment for the user {} in course {} does not exist!'.format(
|
||||
'amy', 'course-v1:edX+DemoX+Demo_Course')
|
||||
)
|
||||
expected_message = 'Enrollment for the user {} in course {} does not exist!'.\
|
||||
format('amy', 'course-v1:edX+DemoX+Demo_Course')
|
||||
|
||||
log.check_present(
|
||||
(LOGGER_NAME, 'INFO', expected_message)
|
||||
)
|
||||
|
||||
def test_bulk_un_enroll(self):
|
||||
@@ -107,3 +88,46 @@ class BulkUnenrollTests(SharedModuleStoreTestCase):
|
||||
call_command("bulk_unenroll", "--csv_path={}".format(csv.name))
|
||||
for enrollment in CourseEnrollment.objects.all():
|
||||
self.assertEqual(enrollment.is_active, False)
|
||||
|
||||
def test_bulk_unenroll_from_config_model(self):
|
||||
"""Verify users are unenrolled using the command."""
|
||||
lines = "user_id,username,email,course_id\n"
|
||||
for enrollment in self.enrollments:
|
||||
lines += str(enrollment.user.id) + "," + enrollment.user.username + "," + \
|
||||
enrollment.user.email + "," + str(enrollment.course.id) + "\n"
|
||||
|
||||
csv_file = SimpleUploadedFile(name='test.csv', content=lines, content_type='text/csv')
|
||||
BulkUnenrollConfiguration.objects.create(enabled=True, csv_file=csv_file)
|
||||
|
||||
call_command("bulk_unenroll")
|
||||
for enrollment in CourseEnrollment.objects.all():
|
||||
self.assertEqual(enrollment.is_active, False)
|
||||
|
||||
def test_users_unenroll_successfully_logged(self):
|
||||
"""Verify users unenrolled are logged """
|
||||
lines = "user_id,username,email,course_id\n"
|
||||
users_unenrolled = {}
|
||||
for enrollment in self.enrollments:
|
||||
username = enrollment.user.username
|
||||
if username in users_unenrolled:
|
||||
users_unenrolled[username].append(str(enrollment.course.id))
|
||||
else:
|
||||
users_unenrolled[username] = [str(enrollment.course.id)]
|
||||
|
||||
lines += str(enrollment.user.id) + "," + username + "," + \
|
||||
enrollment.user.email + "," + str(enrollment.course.id) + "\n"
|
||||
|
||||
csv_file = SimpleUploadedFile(name='test.csv', content=lines, content_type='text/csv')
|
||||
BulkUnenrollConfiguration.objects.create(enabled=True, csv_file=csv_file)
|
||||
|
||||
with LogCapture(LOGGER_NAME) as log:
|
||||
call_command("bulk_unenroll")
|
||||
log.check(
|
||||
(
|
||||
LOGGER_NAME,
|
||||
'INFO',
|
||||
'Following users have been unenrolled successfully from the following courses:'
|
||||
' {users_unenrolled}'.format(users_unenrolled=["{}:{}".format(k, v) for k, v in
|
||||
users_unenrolled.items()])
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.24 on 2019-09-19 19:51
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('student', '0022_indexing_in_courseenrollment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BulkUnenrollConfiguration',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('csv_file', models.FileField(help_text='It expect that the data will be provided in a csv file format with first row being the header and columns will be as follows: user_id, username, email, course_id, is_verified, verification_date', upload_to=b'', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=[b'csv'])])),
|
||||
('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-change_date',),
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -18,6 +18,7 @@ import logging
|
||||
import uuid
|
||||
from collections import OrderedDict, defaultdict, namedtuple
|
||||
from datetime import datetime, timedelta
|
||||
from django.core.validators import FileExtensionValidator
|
||||
from functools import total_ordering
|
||||
from importlib import import_module
|
||||
|
||||
@@ -2845,6 +2846,18 @@ class RegistrationCookieConfiguration(ConfigurationModel):
|
||||
)
|
||||
|
||||
|
||||
class BulkUnenrollConfiguration(ConfigurationModel):
|
||||
"""
|
||||
|
||||
"""
|
||||
csv_file = models.FileField(
|
||||
validators=[FileExtensionValidator(allowed_extensions=['csv'])],
|
||||
help_text=_("It expect that the data will be provided in a csv file format with \
|
||||
first row being the header and columns will be as follows: \
|
||||
user_id, username, email, course_id, is_verified, verification_date")
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class UserAttribute(TimeStampedModel):
|
||||
"""
|
||||
|
||||
@@ -19,7 +19,7 @@ import inspect
|
||||
import json
|
||||
import logging
|
||||
import numbers
|
||||
import random
|
||||
import random2 as random
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import absolute_import
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import random2 as random
|
||||
import textwrap
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import absolute_import, print_function
|
||||
|
||||
import textwrap
|
||||
import unittest
|
||||
import six
|
||||
|
||||
from capa.responsetypes import LoncapaProblemError
|
||||
from capa.tests.helpers import new_loncapa_problem, test_capa_system
|
||||
@@ -59,10 +58,7 @@ class CapaShuffleTest(unittest.TestCase):
|
||||
response = list(problem.responders.values())[0]
|
||||
self.assertFalse(response.has_mask())
|
||||
self.assertTrue(response.has_shuffle())
|
||||
if six.PY2:
|
||||
self.assertEqual(response.unmask_order(), ['choice_0', 'choice_aaa', 'choice_1', 'choice_ddd'])
|
||||
else:
|
||||
self.assertEqual(response.unmask_order(), ['choice_1', 'choice_aaa', 'choice_0', 'choice_ddd'])
|
||||
self.assertEqual(response.unmask_order(), ['choice_0', 'choice_aaa', 'choice_1', 'choice_ddd'])
|
||||
|
||||
def test_shuffle_different_seed(self):
|
||||
xml_str = textwrap.dedent("""
|
||||
|
||||
@@ -101,12 +101,19 @@ class MongoContentStore(ContentStore):
|
||||
locked=getattr(content, 'locked', False)) as fp:
|
||||
|
||||
# It seems that this code thought that only some specific object would have the `__iter__` attribute
|
||||
# but the bytes object in python 3 has one and should not use the chunking logic.
|
||||
if hasattr(content.data, '__iter__') and not isinstance(content.data, six.binary_type):
|
||||
# but many more objects have this in python3 and shouldn't be using the chunking logic. For string and
|
||||
# byte streams we write them directly to gridfs and convert them to byetarrys if necessary.
|
||||
if hasattr(content.data, '__iter__') and not isinstance(content.data, (six.binary_type, six.string_types)):
|
||||
for chunk in content.data:
|
||||
fp.write(chunk)
|
||||
else:
|
||||
fp.write(content.data)
|
||||
# Ideally we could just ensure that we don't get strings in here and only byte streams
|
||||
# but being confident of that wolud be a lot more work than we have time for so we just
|
||||
# handle both cases here.
|
||||
if isinstance(content.data, six.text_type):
|
||||
fp.write(content.data.encode('utf-8'))
|
||||
else:
|
||||
fp.write(content.data)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ def empty_asset_trashcan(course_locs):
|
||||
thumbs = store.get_all_content_thumbnails_for_course(course_loc)
|
||||
for thumb in thumbs:
|
||||
print("Deleting {0}...".format(thumb))
|
||||
store.delete(thumb['_id'])
|
||||
store.delete(thumb['asset_key'])
|
||||
|
||||
# then delete all of the assets
|
||||
assets, __ = store.get_all_content_for_course(course_loc)
|
||||
for asset in assets:
|
||||
print("Deleting {0}...".format(asset))
|
||||
store.delete(asset['_id'])
|
||||
store.delete(asset['asset_key'])
|
||||
|
||||
|
||||
def restore_asset_from_trashcan(location):
|
||||
|
||||
@@ -145,7 +145,7 @@ def youtube_video_transcript_name(youtube_text_api):
|
||||
# http://video.google.com/timedtext?type=list&v={VideoId}
|
||||
youtube_response = requests.get('http://' + youtube_text_api['url'], params=transcripts_param)
|
||||
if youtube_response.status_code == 200 and youtube_response.text:
|
||||
youtube_data = etree.fromstring(youtube_response.content.encode('utf-8'), parser=utf8_parser)
|
||||
youtube_data = etree.fromstring(youtube_response.text.encode('utf-8'), parser=utf8_parser)
|
||||
# iterate all transcripts information from youtube server
|
||||
for element in youtube_data:
|
||||
# search specific language code such as 'en' in transcripts info list
|
||||
@@ -346,7 +346,7 @@ def copy_or_rename_transcript(new_name, old_name, item, delete_old=False, user=N
|
||||
"""
|
||||
filename = u'subs_{0}.srt.sjson'.format(old_name)
|
||||
content_location = StaticContent.compute_location(item.location.course_key, filename)
|
||||
transcripts = contentstore().find(content_location).data
|
||||
transcripts = contentstore().find(content_location).data.decode('utf-8')
|
||||
save_subs_to_store(json.loads(transcripts), new_name, item)
|
||||
item.sub = new_name
|
||||
item.save_with_metadata(user)
|
||||
@@ -579,6 +579,8 @@ def get_video_transcript_content(edx_video_id, language_code):
|
||||
edx_video_id = clean_video_id(edx_video_id)
|
||||
if edxval_api and edx_video_id:
|
||||
transcript = edxval_api.get_video_transcript_data(edx_video_id, language_code)
|
||||
if transcript and 'content' in transcript:
|
||||
transcript['content'] = transcript['content'].decode('utf-8')
|
||||
|
||||
return transcript
|
||||
|
||||
@@ -654,7 +656,7 @@ class Transcript(object):
|
||||
if input_format == 'srt':
|
||||
|
||||
if output_format == 'txt':
|
||||
text = SubRipFile.from_string(content.decode('utf-8')).text
|
||||
text = SubRipFile.from_string(content).text
|
||||
return HTMLParser().unescape(text)
|
||||
|
||||
elif output_format == 'sjson':
|
||||
@@ -663,7 +665,7 @@ class Transcript(object):
|
||||
# the exception if something went wrong in parsing the transcript.
|
||||
srt_subs = SubRipFile.from_string(
|
||||
# Skip byte order mark(BOM) character
|
||||
content.decode('utf-8-sig') if six.PY2 else content.encode('utf-8').decode('utf-8-sig'),
|
||||
content.encode('utf-8').decode('utf-8-sig'),
|
||||
error_handling=SubRipFile.ERROR_RAISE
|
||||
)
|
||||
except Error as ex: # Base exception from pysrt
|
||||
@@ -925,11 +927,11 @@ def get_transcript_for_video(video_location, subs_id, file_name, language):
|
||||
try:
|
||||
if subs_id is None:
|
||||
raise NotFoundError
|
||||
content = Transcript.asset(video_location, subs_id, language).data
|
||||
content = Transcript.asset(video_location, subs_id, language).data.decode('utf-8')
|
||||
base_name = subs_id
|
||||
input_format = Transcript.SJSON
|
||||
except NotFoundError:
|
||||
content = Transcript.asset(video_location, None, language, file_name).data
|
||||
content = Transcript.asset(video_location, None, language, file_name).data.decode('utf-8')
|
||||
base_name = os.path.splitext(file_name)[0]
|
||||
input_format = Transcript.SRT
|
||||
|
||||
|
||||
@@ -466,7 +466,7 @@ class VideoStudioViewHandlers(object):
|
||||
# Convert SRT transcript into an SJSON format
|
||||
# and upload it to S3.
|
||||
sjson_subs = Transcript.convert(
|
||||
content=transcript_file.read(),
|
||||
content=transcript_file.read().decode('utf-8'),
|
||||
input_format=Transcript.SRT,
|
||||
output_format=Transcript.SJSON
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
5b2b96c1f6ef523876a5a900158a35c2658264a2
|
||||
9ad3d121db0c71754dc37475498270555f1d163f
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -366,7 +366,7 @@ CREATE TABLE `auth_permission` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `auth_permission_content_type_id_codename_01ab375a_uniq` (`content_type_id`,`codename`),
|
||||
CONSTRAINT `auth_permission_content_type_id_2f476e4b_fk_django_co` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2363 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2366 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `auth_registration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -2419,7 +2419,7 @@ CREATE TABLE `django_content_type` (
|
||||
`model` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `django_content_type_app_label_model_76bd3d3b_uniq` (`app_label`,`model`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=785 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=786 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_migrations`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -2430,7 +2430,7 @@ CREATE TABLE `django_migrations` (
|
||||
`name` varchar(255) NOT NULL,
|
||||
`applied` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=595 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=600 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_openid_auth_association`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -2634,8 +2634,10 @@ CREATE TABLE `edx_when_datepolicy` (
|
||||
`created` datetime(6) NOT NULL,
|
||||
`modified` datetime(6) NOT NULL,
|
||||
`abs_date` datetime(6) DEFAULT NULL,
|
||||
`rel_date` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `edx_when_datepolicy_abs_date_1a510cd3` (`abs_date`)
|
||||
KEY `edx_when_datepolicy_abs_date_1a510cd3` (`abs_date`),
|
||||
KEY `edx_when_datepolicy_rel_date_836d6051` (`rel_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `edx_when_userdate`;
|
||||
@@ -2646,7 +2648,7 @@ CREATE TABLE `edx_when_userdate` (
|
||||
`created` datetime(6) NOT NULL,
|
||||
`modified` datetime(6) NOT NULL,
|
||||
`abs_date` datetime(6) DEFAULT NULL,
|
||||
`rel_date` int(11) DEFAULT NULL,
|
||||
`rel_date` bigint(20) DEFAULT NULL,
|
||||
`reason` longtext NOT NULL,
|
||||
`actor_id` int(11) DEFAULT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
@@ -2655,6 +2657,7 @@ CREATE TABLE `edx_when_userdate` (
|
||||
KEY `edx_when_userdate_user_id_46e8cc36_fk_auth_user_id` (`user_id`),
|
||||
KEY `edx_when_userdate_content_date_id_35c5e2e2_fk_edx_when_` (`content_date_id`),
|
||||
KEY `edx_when_userdate_actor_id_cbef1cdc_fk_auth_user_id` (`actor_id`),
|
||||
KEY `edx_when_userdate_rel_date_954ee5b4` (`rel_date`),
|
||||
CONSTRAINT `edx_when_userdate_actor_id_cbef1cdc_fk_auth_user_id` FOREIGN KEY (`actor_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `edx_when_userdate_content_date_id_35c5e2e2_fk_edx_when_` FOREIGN KEY (`content_date_id`) REFERENCES `edx_when_contentdate` (`id`),
|
||||
CONSTRAINT `edx_when_userdate_user_id_46e8cc36_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
@@ -5456,6 +5459,20 @@ CREATE TABLE `student_anonymoususerid` (
|
||||
CONSTRAINT `student_anonymoususerid_user_id_0fb2ad5c_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `student_bulkunenrollconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `student_bulkunenrollconfiguration` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime(6) NOT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`csv_file` varchar(100) NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `student_bulkunenroll_changed_by_id_7b6131b9_fk_auth_user` (`changed_by_id`),
|
||||
CONSTRAINT `student_bulkunenroll_changed_by_id_7b6131b9_fk_auth_user` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `student_courseaccessrole`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
|
||||
@@ -36,7 +36,7 @@ CREATE TABLE `django_migrations` (
|
||||
`name` varchar(255) NOT NULL,
|
||||
`applied` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=595 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=600 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ Helpers for courseware tests.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import ast
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
|
||||
import six
|
||||
@@ -17,10 +19,10 @@ from six import text_type
|
||||
from six.moves import range
|
||||
from xblock.field_data import DictFieldData
|
||||
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.masquerade import handle_ajax, setup_masquerade
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
|
||||
from lms.djangoapps.courseware.masquerade import handle_ajax, setup_masquerade
|
||||
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.lib.url_utils import quote_slashes
|
||||
@@ -404,3 +406,15 @@ def get_expiration_banner_text(user, course, language='en'):
|
||||
expiration_date=formatted_expiration_date
|
||||
)
|
||||
return bannerText
|
||||
|
||||
|
||||
def get_context_dict_from_string(data):
|
||||
"""
|
||||
Retrieve dictionary from string.
|
||||
"""
|
||||
# Replace tuple and un-necessary info from inside string and get the dictionary.
|
||||
cleaned_data = ast.literal_eval(data.split('((\'video.html\',')[1].replace("),\n {})", '').strip()) # pylint: disable=unicode-format-string
|
||||
cleaned_data['metadata'] = OrderedDict(
|
||||
sorted(json.loads(cleaned_data['metadata']).items(), key=lambda t: t[0])
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
@@ -5,7 +5,6 @@ Video xmodule tests in mongo.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import ast
|
||||
import io
|
||||
import json
|
||||
import shutil
|
||||
@@ -39,6 +38,7 @@ from mock import MagicMock, Mock, patch
|
||||
from path import Path as path
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from lms.djangoapps.courseware.tests.helpers import get_context_dict_from_string
|
||||
from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE, waffle_flags
|
||||
from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
@@ -134,8 +134,10 @@ class TestVideoYouTube(TestVideo): # pylint: disable=test-inherits-tests
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
|
||||
get_context_dict_from_string(context),
|
||||
get_context_dict_from_string(
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -214,10 +216,15 @@ class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests
|
||||
'poster': 'null',
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
context,
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
|
||||
expected_result = get_context_dict_from_string(
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
self.assertEqual(
|
||||
get_context_dict_from_string(context),
|
||||
expected_result
|
||||
)
|
||||
self.assertEqual(expected_result['download_video_link'], 'example.mp4')
|
||||
self.assertEqual(expected_result['display_name'], 'A Name')
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -383,8 +390,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
self.get_context_dict_from_string(context),
|
||||
self.get_context_dict_from_string(
|
||||
get_context_dict_from_string(context),
|
||||
get_context_dict_from_string(
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
)
|
||||
@@ -493,8 +500,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
self.get_context_dict_from_string(context),
|
||||
self.get_context_dict_from_string(
|
||||
get_context_dict_from_string(context),
|
||||
get_context_dict_from_string(
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
)
|
||||
@@ -634,23 +641,12 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
self.get_context_dict_from_string(context),
|
||||
self.get_context_dict_from_string(
|
||||
get_context_dict_from_string(context),
|
||||
get_context_dict_from_string(
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
)
|
||||
|
||||
def get_context_dict_from_string(self, data):
|
||||
"""
|
||||
Retrieve dictionary from string.
|
||||
"""
|
||||
# Replace tuple and un-necessary info from inside string and get the dictionary.
|
||||
cleaned_data = ast.literal_eval(data.split('((\'video.html\',')[1].replace("),\n {})", '').strip()) # pylint: disable=unicode-format-string
|
||||
cleaned_data['metadata'] = OrderedDict(
|
||||
sorted(json.loads(cleaned_data['metadata']).items(), key=lambda t: t[0])
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
def test_get_html_with_existing_edx_video_id(self):
|
||||
"""
|
||||
Tests the `VideoBlock` `get_html` where `edx_video_id` is given and related video is found
|
||||
@@ -676,8 +672,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
# expected_context, a dict to assert with context
|
||||
context, expected_context = self.helper_get_html_with_edx_video_id(data)
|
||||
self.assertEqual(
|
||||
self.get_context_dict_from_string(context),
|
||||
self.get_context_dict_from_string(
|
||||
get_context_dict_from_string(context),
|
||||
get_context_dict_from_string(
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
)
|
||||
@@ -710,8 +706,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
context, expected_context = self.helper_get_html_with_edx_video_id(data)
|
||||
|
||||
self.assertEqual(
|
||||
self.get_context_dict_from_string(context),
|
||||
self.get_context_dict_from_string(
|
||||
get_context_dict_from_string(context),
|
||||
get_context_dict_from_string(
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
)
|
||||
@@ -921,8 +917,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
self.get_context_dict_from_string(context),
|
||||
self.get_context_dict_from_string(
|
||||
get_context_dict_from_string(context),
|
||||
get_context_dict_from_string(
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
)
|
||||
@@ -1026,8 +1022,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
self.get_context_dict_from_string(context),
|
||||
self.get_context_dict_from_string(
|
||||
get_context_dict_from_string(context),
|
||||
get_context_dict_from_string(
|
||||
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
)
|
||||
@@ -1083,7 +1079,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
self.assertIn('"streams": "1.00:https://yt.com/?v=v0TFmdO4ZP0"', context)
|
||||
self.assertEqual(
|
||||
sorted(["https://webm.com/dw.webm", "https://mp4.com/dm.mp4", "https://hls.com/hls.m3u8"]),
|
||||
sorted(self.get_context_dict_from_string(context)['metadata']['sources'])
|
||||
sorted(get_context_dict_from_string(context)['metadata']['sources'])
|
||||
)
|
||||
|
||||
def test_get_html_hls_no_video_id(self):
|
||||
@@ -2266,7 +2262,9 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
|
||||
}
|
||||
|
||||
expected_content = self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
self.assertEqual(content, expected_content)
|
||||
self.assertEqual(
|
||||
get_context_dict_from_string(content), get_context_dict_from_string(expected_content)
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -2358,7 +2356,7 @@ class TestAutoAdvanceVideo(TestVideo):
|
||||
with override_settings(FEATURES=self.FEATURES):
|
||||
expected_content = self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
|
||||
|
||||
self.assertEqual(content, expected_content)
|
||||
self.assertEqual(get_context_dict_from_string(content), get_context_dict_from_string(expected_content))
|
||||
|
||||
def change_course_setting_autoadvance(self, new_value):
|
||||
"""
|
||||
|
||||
@@ -292,7 +292,7 @@ def get_library_block_olx(usage_key):
|
||||
bundle_uuid=definition_key.bundle_uuid, # pylint: disable=no-member
|
||||
path=definition_key.olx_path, # pylint: disable=no-member
|
||||
use_draft=DRAFT_NAME,
|
||||
)
|
||||
).decode('utf-8')
|
||||
return xml_str
|
||||
|
||||
|
||||
@@ -314,7 +314,7 @@ def set_library_block_olx(usage_key, new_olx_str):
|
||||
raise ValueError("Invalid root tag in OLX, expected {}".format(block_type))
|
||||
# Write the new XML/OLX file into the library bundle's draft
|
||||
draft = get_or_create_bundle_draft(metadata.def_key.bundle_uuid, DRAFT_NAME)
|
||||
write_draft_file(draft.uuid, metadata.def_key.olx_path, new_olx_str)
|
||||
write_draft_file(draft.uuid, metadata.def_key.olx_path, new_olx_str.encode('utf-8'))
|
||||
# Clear the bundle cache so everyone sees the new block immediately:
|
||||
BundleCache(metadata.def_key.bundle_uuid, draft_name=DRAFT_NAME).clear()
|
||||
|
||||
@@ -347,7 +347,7 @@ def create_library_block(library_key, block_type, definition_id):
|
||||
path = "{}/{}/definition.xml".format(block_type, definition_id)
|
||||
# Write the new XML/OLX file into the library bundle's draft
|
||||
draft = get_or_create_bundle_draft(ref.bundle_uuid, DRAFT_NAME)
|
||||
write_draft_file(draft.uuid, path, new_definition_xml)
|
||||
write_draft_file(draft.uuid, path, new_definition_xml.encode('utf-8'))
|
||||
# Clear the bundle cache so everyone sees the new block immediately:
|
||||
BundleCache(ref.bundle_uuid, draft_name=DRAFT_NAME).clear()
|
||||
# Now return the metadata about the new block:
|
||||
|
||||
@@ -254,7 +254,7 @@ class ContentLibrariesTest(APITestCase):
|
||||
new_olx = """
|
||||
<problem display_name="New Multi Choice Question" max_attempts="5">
|
||||
<multiplechoiceresponse>
|
||||
<p>This is a normal capa problem. It has "maximum attempts" set to **5**.</p>
|
||||
<p>This is a normal capa problem with unicode 🔥. It has "maximum attempts" set to **5**.</p>
|
||||
<label>Blockstore is designed to store.</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">XBlock metadata only</choice>
|
||||
|
||||
@@ -272,7 +272,7 @@ class ContentStoreToyCourseTest(SharedModuleStoreTestCase):
|
||||
416 Requested Range Not Satisfiable.
|
||||
"""
|
||||
resp = self.client.get(self.url_unlocked, HTTP_RANGE='bytes={first}-{last}'.format(
|
||||
first=(self.length_unlocked / 2), last=(self.length_unlocked / 4)))
|
||||
first=(self.length_unlocked // 2), last=(self.length_unlocked // 4)))
|
||||
self.assertEqual(resp.status_code, 416)
|
||||
|
||||
def test_range_request_malformed_out_of_bounds(self):
|
||||
|
||||
@@ -138,6 +138,7 @@ pytz # Time zone information database
|
||||
PyYAML # Used to parse XModule resource templates
|
||||
redis==2.10.6 # celery task broker
|
||||
requests-oauthlib # Simplifies use of OAuth via the requests library, used for CCX and LTI
|
||||
random2
|
||||
rules # Django extension for rules-based authorization checks
|
||||
sailthru-client==2.2.3 # For Sailthru integration
|
||||
Shapely # Geometry library, used for image click regions in capa
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
|
||||
-e git+https://github.com/edx/acid-block.git@98aecba94ecbfa934e2d00262741c0ea9f557fc9#egg=acid-xblock
|
||||
-e common/lib/capa
|
||||
-e git+https://github.com/edx/codejail.git@ed3d36c27913254a23273da95ad627a1bbbffa44#egg=codejail
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.23#egg=django-wiki
|
||||
@@ -207,6 +207,7 @@ python3-saml==1.5.0
|
||||
pytz==2019.2
|
||||
pyuca==1.1
|
||||
pyyaml==5.1.2
|
||||
random2==1.0.1
|
||||
recommender-xblock==1.4.4
|
||||
redis==2.10.6
|
||||
requests-oauthlib==1.1.0
|
||||
@@ -250,7 +251,7 @@ web-fragments==0.3.0
|
||||
webencodings==0.5.1 # via html5lib
|
||||
webob==1.8.5 # via xblock
|
||||
wrapt==1.10.5
|
||||
git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.2.4#egg=xblock-drag-and-drop-v2==2.2.4
|
||||
git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.2.6#egg=xblock-drag-and-drop-v2==2.2.6
|
||||
git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1
|
||||
xblock-utils==1.2.2
|
||||
xblock==1.2.6
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
|
||||
-e git+https://github.com/edx/acid-block.git@98aecba94ecbfa934e2d00262741c0ea9f557fc9#egg=acid-xblock
|
||||
-e common/lib/capa
|
||||
-e git+https://github.com/edx/codejail.git@ed3d36c27913254a23273da95ad627a1bbbffa44#egg=codejail
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.23#egg=django-wiki
|
||||
@@ -279,6 +279,7 @@ pytz==2019.2
|
||||
pyuca==1.1
|
||||
pyyaml==5.1.2
|
||||
radon==4.0.0
|
||||
random2==1.0.1
|
||||
recommender-xblock==1.4.4
|
||||
recommonmark==0.6.0
|
||||
redis==2.10.6
|
||||
@@ -339,7 +340,7 @@ webob==1.8.5
|
||||
websocket-client==0.56.0
|
||||
werkzeug==0.16.0
|
||||
wrapt==1.10.5
|
||||
git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.2.4#egg=xblock-drag-and-drop-v2==2.2.4
|
||||
git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.2.6#egg=xblock-drag-and-drop-v2==2.2.6
|
||||
git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1
|
||||
xblock-utils==1.2.2
|
||||
xblock==1.2.6
|
||||
|
||||
@@ -80,7 +80,7 @@ git+https://github.com/edx/bridgekeeper.git@4e34894e4ac5d0467ed1901811a81fd87ee0
|
||||
|
||||
# Our libraries:
|
||||
-e git+https://github.com/edx/codejail.git@ed3d36c27913254a23273da95ad627a1bbbffa44#egg=codejail
|
||||
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
|
||||
-e git+https://github.com/edx/acid-block.git@98aecba94ecbfa934e2d00262741c0ea9f557fc9#egg=acid-xblock
|
||||
git+https://github.com/edx/edx-ora2.git@2.3.1#egg=ora2==2.3.1
|
||||
git+https://github.com/edx/crowdsourcehinter.git@a7ffc85b134b7d8909bf1fefd23dbdb8eb28e467#egg=crowdsourcehinter-xblock==0.2
|
||||
-e git+https://github.com/edx/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock
|
||||
@@ -94,4 +94,4 @@ git+https://github.com/edx/xblock-lti-consumer.git@v1.1.8#egg=lti_consumer-xbloc
|
||||
git+https://github.com/joestump/python-oauth2.git@b94f69b1ad195513547924e380d9265133e995fa#egg=oauth2
|
||||
git+https://github.com/mitodl/edx-sga.git@237ad328ba3f03d189c421073c85e48091041f8b#egg=edx-sga==0.0
|
||||
git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1
|
||||
git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.2.4#egg=xblock-drag-and-drop-v2==2.2.4
|
||||
git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.2.6#egg=xblock-drag-and-drop-v2==2.2.6
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
|
||||
-e git+https://github.com/edx/acid-block.git@98aecba94ecbfa934e2d00262741c0ea9f557fc9#egg=acid-xblock
|
||||
-e common/lib/capa
|
||||
-e git+https://github.com/edx/codejail.git@ed3d36c27913254a23273da95ad627a1bbbffa44#egg=codejail
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.23#egg=django-wiki
|
||||
@@ -270,6 +270,7 @@ pytz==2019.2
|
||||
pyuca==1.1
|
||||
pyyaml==5.1.2
|
||||
radon==4.0.0
|
||||
random2==1.0.1
|
||||
recommender-xblock==1.4.4
|
||||
redis==2.10.6
|
||||
requests-oauthlib==1.1.0
|
||||
@@ -325,7 +326,7 @@ webob==1.8.5
|
||||
websocket-client==0.56.0 # via docker
|
||||
werkzeug==0.16.0 # via moto
|
||||
wrapt==1.10.5
|
||||
git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.2.4#egg=xblock-drag-and-drop-v2==2.2.4
|
||||
git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.2.6#egg=xblock-drag-and-drop-v2==2.2.6
|
||||
git+https://github.com/open-craft/xblock-poll@add89e14558c30f3c8dc7431e5cd6536fff6d941#egg=xblock-poll==1.5.1
|
||||
xblock-utils==1.2.2
|
||||
xblock==1.2.6
|
||||
|
||||
Reference in New Issue
Block a user