-
Notifications
You must be signed in to change notification settings - Fork 4.3k
[SE-3329] Expose banner image url in course API #25045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Generated by Django 2.2.16 on 2020-09-22 12:45 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('course_overviews', '0022_courseoverviewtab_is_hidden'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='courseoverview', | ||
| name='banner_image_url', | ||
| field=models.TextField(), | ||
| ), | ||
| migrations.AddField( | ||
| model_name='historicalcourseoverview', | ||
| name='banner_image_url', | ||
| field=models.TextField(), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,7 +63,7 @@ class Meta(object): | |
| app_label = 'course_overviews' | ||
|
|
||
| # IMPORTANT: Bump this whenever you modify this model and/or add a migration. | ||
| VERSION = 11 # this one goes to eleven | ||
| VERSION = 12 # this one goes to thirteen | ||
|
|
||
| # Cache entry versioning. | ||
| version = IntegerField() | ||
|
|
@@ -86,6 +86,8 @@ class Meta(object): | |
| announcement = DateTimeField(null=True) | ||
|
|
||
| # URLs | ||
| # Not allowing null per django convention; not sure why many TextFields in this model do allow null | ||
| banner_image_url = TextField() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for this platform enhancement.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@nasthagiri I see some context here, but not sure that it qualifies as "documentation" : https://github.com/edx/edx-platform/blob/7afee25ce2150443eefe0ae60b81f3e4f6e61900/common/lib/xmodule/xmodule/course_module.py#L589-L609
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From a consumer perspective, it would be useful to know when the difference between the 2 images. Perhaps it would help to describe where each image is displayed?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ping @gabor-boros - can you answer that question ^ ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nasthagiri & @bradenmacdonald Apologies for replying that late, I probably missed the notification about this comment. As far as I can see, the From consumer perspective - in my mind - the difference is that the course image is kind of a thumbnail of a course while the banner image is like a cover image. So something like this: |
||
| course_image_url = TextField() | ||
| social_sharing_url = TextField(null=True) | ||
| end_of_course_survey_url = TextField(null=True) | ||
|
|
@@ -196,6 +198,7 @@ def _create_or_update(cls, course): | |
| course_overview.advertised_start = course.advertised_start | ||
| course_overview.announcement = course.announcement | ||
|
|
||
| course_overview.banner_image_url = course_image_url(course, 'banner_image') | ||
| course_overview.course_image_url = course_image_url(course) | ||
| course_overview.social_sharing_url = course.social_sharing_url | ||
|
|
||
|
|
@@ -728,6 +731,22 @@ def closest_released_language(self): | |
| """ | ||
| return get_closest_released_language(self.language) if self.language else None | ||
|
|
||
| def apply_cdn_to_url(self, image_url): | ||
| """ | ||
| Applies a new CDN/base URL to the given URLs if CDN configuration is | ||
| enabled. | ||
|
|
||
| If CDN does not exist or is disabled, just returns the original. The | ||
| URL that we store in CourseOverviewImageSet is already top level path, | ||
| so we don't need to go through the /static remapping magic that happens | ||
| with other course assets. We just need to add the CDN server if appropriate. | ||
| """ | ||
| cdn_config = AssetBaseUrlConfig.current() | ||
| if not cdn_config.enabled: | ||
| return image_url | ||
|
|
||
| return self._apply_cdn_to_url(image_url, cdn_config.base_url) | ||
|
|
||
| def apply_cdn_to_urls(self, image_urls): | ||
| """ | ||
| Given a dict of resolutions -> urls, return a copy with CDN applied. | ||
|
|
@@ -738,14 +757,8 @@ def apply_cdn_to_urls(self, image_urls): | |
| happens with other course assets. We just need to add the CDN server if | ||
| appropriate. | ||
| """ | ||
| cdn_config = AssetBaseUrlConfig.current() | ||
| if not cdn_config.enabled: | ||
| return image_urls | ||
|
|
||
| base_url = cdn_config.base_url | ||
|
|
||
| return { | ||
| resolution: self._apply_cdn_to_url(url, base_url) | ||
| resolution: self.apply_cdn_to_url(url) | ||
| for resolution, url in image_urls.items() | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -382,7 +382,7 @@ def test_malformed_grading_policy(self): | |
| course_overview = CourseOverview._create_or_update(course) # pylint: disable=protected-access | ||
| self.assertEqual(course_overview.lowest_passing_grade, None) | ||
|
|
||
| @ddt.data((ModuleStoreEnum.Type.mongo, 4, 4), (ModuleStoreEnum.Type.split, 3, 4)) | ||
| @ddt.data((ModuleStoreEnum.Type.mongo, 4, 4), (ModuleStoreEnum.Type.split, 3, 3)) | ||
| @ddt.unpack | ||
| def test_versioning(self, modulestore_type, min_mongo_calls, max_mongo_calls): | ||
| """ | ||
|
|
@@ -789,6 +789,28 @@ def test_cdn_with_external_image(self, modulestore_type): | |
| self.assertTrue(modified_urls['small'].startswith(expected_cdn_url)) | ||
| self.assertEqual(modified_urls['large'], start_urls['large']) | ||
|
|
||
| @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) | ||
| def test_cdn_with_a_single_external_image(self, modulestore_type): | ||
| """ | ||
| Test CDN is applied for a URL when apply_cdn_to_url called directly. | ||
|
|
||
| Apply CDN/base URL to the given URL if CDN configuration is enabled | ||
| and the URL is not absolute. | ||
| """ | ||
| with self.store.default_store(modulestore_type): | ||
| course = CourseFactory.create(default_store=modulestore_type) | ||
| overview = CourseOverview.get_from_id(course.id) | ||
|
|
||
| # Now enable the CDN... | ||
| AssetBaseUrlConfig.objects.create(enabled=True, base_url='fakecdn.edx.org') | ||
| expected_cdn_url = "//fakecdn.edx.org" | ||
|
|
||
| start_url = "/static/overview.png" | ||
| modified_url = overview.apply_cdn_to_url(start_url) | ||
|
|
||
| self.assertNotEqual(start_url, modified_url) | ||
| self.assertTrue(modified_url.startswith(expected_cdn_url)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test has the same docstring as the previous test and it's hard to tell the difference. It took me a while to figure out that this is testing a slightly different method. Plus it doesn't actually test what it says in the docstring, because this test case doesn't test the case where Maybe combine the tests, or change the docstring to say "Test the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, it seems I overlooked the docstring here. Thanks for pointing out! I'll fix this as well
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you forgot to address this review comment ^
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, you are totally right. I'm doing it just right now. Sorry about that. |
||
|
|
||
| @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) | ||
| def test_error_generating_thumbnails(self, modulestore_type): | ||
| """ | ||
|
|
||

Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this hack is required because you're declaring the field within this method, instead of as a member of this serializer.
I think that if you just declare the field like this:
then it should work without
contexthacks.Edit: Hmm, never mind I guess that won't work because it won't call the method on the serializer. Maybe you can override
__init__to getdataand callapply_cdn_to_urlon the data there? I think that's cleaner than overriding__call__and passing the context around like this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, this cannot be done, because the
BaseSerializerdoes not set theself.initial_dataif data argument was empty. Although serializers has aget_initialmethod, the method will return an emptyOrderedDictsince the serializer did not receive data at all. So we need to pass the context around, but I'm opened to other ideas as well. When I did a research in this topic when I opened the PR, I did not find any other solution which is cleaner.