Commit b9655bb1 by Clinton Blackburn Committed by Clinton Blackburn

Added management command to update course images

This command downloads images from the course's card_image_url field
value, and stores them in the image field.

LEARNER-2463
parent 3900dda2
import logging
import requests
from django.core.files.base import ContentFile
from django.core.management import BaseCommand
from course_discovery.apps.course_metadata.models import Course
logger = logging.getLogger(__name__)
IMAGE_TYPES = {
'image/jpeg': 'jpg',
'image/png': 'png',
}
class Command(BaseCommand):
help = 'Download course images to this server. This is intended to migrate image data from the edx.org ' \
'marketing site to Discovery.'
def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
'--overwrite',
action='store_true',
dest='overwrite_existing',
help='Overwrite existing image content'
)
def handle(self, *args, **options):
courses = Course.objects.filter(card_image_url__isnull=False).exclude(card_image_url='').order_by('key')
if not options['overwrite_existing']:
courses = courses.filter(image='')
count = courses.count()
if count < 1:
logger.info('All courses are up to date.')
return
logger.info('Retrieving images for [%d] courses...', count)
for course in courses:
logger.info('Retrieving image for course [%s] from [%s]...', course.key, course.card_image_url)
response = requests.get(course.card_image_url)
if response.status_code == requests.codes.ok: # pylint: disable=no-member
content_type = response.headers['Content-Type'].lower()
extension = IMAGE_TYPES.get(content_type)
if extension:
filename = '{uuid}.{extension}'.format(uuid=str(course.uuid), extension=extension)
course.image.save(filename, ContentFile(response.content))
logger.info('Image for course [%s] successfully updated.', course.key)
else:
# pylint: disable=line-too-long
msg = 'Image retrieved for course [%s] from [%s] has an unknown content type [%s] and will not be saved.'
logger.error(msg, course.key, course.card_image_url, content_type)
else:
msg = 'Failed to download image for course [%s] from [%s]! Response was [%d]:\n%s'
logger.error(msg, course.key, course.card_image_url, response.status_code, response.content)
import mock
import pytest
import responses
from django.core.management import call_command
from course_discovery.apps.course_metadata.tests.factories import CourseFactory
@pytest.mark.django_db
class TestDownloadCourseImages:
LOGGER_PATH = 'course_discovery.apps.course_metadata.management.commands.download_course_images.logger'
def mock_image_response(self, status=200, body=None, content_type='image/jpeg'):
# PNG. Single black pixel
body = body or b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00' \
b'\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xf6\x178U\x00\x00\x00\x00' \
b'IEND\xaeB`\x82'
image_url = 'https://example.com/image.jpg'
responses.add(
responses.GET,
image_url,
body=body,
status=status,
content_type=content_type
)
return image_url, body
@responses.activate
def test_download(self):
image_url, image_content = self.mock_image_response()
course = CourseFactory(card_image_url=image_url, image=None)
call_command('download_course_images')
assert len(responses.calls) == 1
course.refresh_from_db()
assert course.image.read() == image_content
@responses.activate
def test_download_with_overwrite_of_existing_data(self):
image_url, image_content = self.mock_image_response()
course = CourseFactory(card_image_url=image_url)
assert course.image.read() != image_content
call_command('download_course_images', '--overwrite')
assert len(responses.calls) == 1
course.refresh_from_db()
assert course.image.read() == image_content
@responses.activate
def test_download_with_invalid_content_type(self):
content_type = 'text/plain'
image_url, __ = self.mock_image_response(content_type=content_type)
course = CourseFactory(card_image_url=image_url, image=None)
with mock.patch(self.LOGGER_PATH) as mock_logger:
call_command('download_course_images')
mock_logger.error.assert_called_with(
'Image retrieved for course [%s] from [%s] has an unknown content type [%s] and will not be saved.',
course.key,
image_url,
content_type
)
assert len(responses.calls) == 1
course.refresh_from_db()
assert not bool(course.image)
@responses.activate
def test_download_with_invalid_status_code(self):
status = 500
body = b'Oops!'
image_url, __ = self.mock_image_response(status=status, body=body)
course = CourseFactory(card_image_url=image_url, image=None)
with mock.patch(self.LOGGER_PATH) as mock_logger:
call_command('download_course_images')
mock_logger.error.assert_called_with(
'Failed to download image for course [%s] from [%s]! Response was [%d]:\n%s',
course.key,
image_url,
status,
body
)
assert len(responses.calls) == 1
course.refresh_from_db()
assert not bool(course.image)
def test_download_without_courses(self):
with mock.patch(self.LOGGER_PATH) as mock_logger:
call_command('download_course_images')
mock_logger.info.assert_called_with('All courses are up to date.')
......@@ -72,6 +72,7 @@ class CourseFactory(factory.DjangoModelFactory):
prerequisites_raw = FuzzyText()
syllabus_raw = FuzzyText()
outcome = FuzzyText()
image = factory.django.ImageField()
class Meta:
model = Course
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment