Commit 9e2494b2 by Chris Rodriguez

Video player branding addition for XuetangX

parent 8fb739ee
...@@ -312,3 +312,7 @@ VIDEO_UPLOAD_PIPELINE = ENV_TOKENS.get('VIDEO_UPLOAD_PIPELINE', VIDEO_UPLOAD_PIP ...@@ -312,3 +312,7 @@ VIDEO_UPLOAD_PIPELINE = ENV_TOKENS.get('VIDEO_UPLOAD_PIPELINE', VIDEO_UPLOAD_PIP
#date format the api will be formatting the datetime values #date format the api will be formatting the datetime values
API_DATE_FORMAT = '%Y-%m-%d' API_DATE_FORMAT = '%Y-%m-%d'
API_DATE_FORMAT = ENV_TOKENS.get('API_DATE_FORMAT', API_DATE_FORMAT) API_DATE_FORMAT = ENV_TOKENS.get('API_DATE_FORMAT', API_DATE_FORMAT)
# Video Caching. Pairing country codes with CDN URLs.
# Example: {'CN': 'http://api.xuetangx.com/edx/video?s3_url='}
VIDEO_CDN_URL = ENV_TOKENS.get('VIDEO_CDN_URL', {})
# -*- coding: utf-8 -*-
""" """
This config file runs the simplest dev environment using sqlite, and db-based This config file runs the simplest dev environment using sqlite, and db-based
sessions. Assumes structure: sessions. Assumes structure:
...@@ -249,3 +250,7 @@ FEATURES['MILESTONES_APP'] = True ...@@ -249,3 +250,7 @@ FEATURES['MILESTONES_APP'] = True
# ENTRANCE EXAMS # ENTRANCE EXAMS
FEATURES['ENTRANCE_EXAMS'] = True FEATURES['ENTRANCE_EXAMS'] = True
ENTRANCE_EXAM_MIN_SCORE_PCT = 50 ENTRANCE_EXAM_MIN_SCORE_PCT = 50
VIDEO_CDN_URL = {
'CN': 'http://api.xuetangx.com/edx/video?s3_url='
}
...@@ -72,6 +72,31 @@ div.video { ...@@ -72,6 +72,31 @@ div.video {
border-radius: 3px; border-radius: 3px;
} }
} }
.branding {
display: inline-block;
float: right;
margin: 15px 0 0 10px;
vertical-align: top;
.host-tag {
@include margin-right($baseline/2);
position: absolute;
left: -9999em;
display: inline-block;
vertical-align: middle;
font-size: 70%;
color: #777;
}
.brand-logo {
display: inline-block;
max-width: 100%;
max-height: ($baseline*2);
padding: ($baseline/4) 0;
vertical-align: middle;
}
}
} }
article.video-wrapper { article.video-wrapper {
...@@ -741,7 +766,7 @@ div.video { ...@@ -741,7 +766,7 @@ div.video {
bottom: 0; bottom: 0;
left: 0; left: 0;
position: absolute; position: absolute;
width: 100%; width: 100%;
} }
} }
......
...@@ -2,14 +2,12 @@ ...@@ -2,14 +2,12 @@
# pylint: disable=abstract-method # pylint: disable=abstract-method
"""Video is ungraded Xmodule for support video content. """Video is ungraded Xmodule for support video content.
It's new improved video module, which support additional feature: It's new improved video module, which support additional feature:
- Can play non-YouTube video sources via in-browser HTML5 video player. - Can play non-YouTube video sources via in-browser HTML5 video player.
- YouTube defaults to HTML5 mode from the start. - YouTube defaults to HTML5 mode from the start.
- Speed changes in both YouTube and non-YouTube videos happen via - Speed changes in both YouTube and non-YouTube videos happen via
in-browser HTML5 video method (when in HTML5 mode). in-browser HTML5 video method (when in HTML5 mode).
- Navigational subtitles can be disabled altogether via an attribute - Navigational subtitles can be disabled altogether via an attribute
in XML. in XML.
Examples of html5 videos for manual testing: Examples of html5 videos for manual testing:
https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp4 https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp4
https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm
...@@ -73,6 +71,11 @@ try: ...@@ -73,6 +71,11 @@ try:
except ImportError: except ImportError:
edxval_api = None edxval_api = None
try:
from branding.models import BrandingInfoConfig
except ImportError:
BrandingInfoConfig = None
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_ = lambda text: text _ = lambda text: text
...@@ -80,7 +83,6 @@ _ = lambda text: text ...@@ -80,7 +83,6 @@ _ = lambda text: text
class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, XModule): class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, XModule):
""" """
XML source example: XML source example:
<video show_captions="true" <video show_captions="true"
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg" youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
url_name="lecture_21_3" display_name="S19V3: Vacancies" url_name="lecture_21_3" display_name="S19V3: Vacancies"
...@@ -129,12 +131,9 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -129,12 +131,9 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
def get_transcripts_for_student(self): def get_transcripts_for_student(self):
"""Return transcript information necessary for rendering the XModule student view. """Return transcript information necessary for rendering the XModule student view.
This is more or less a direct extraction from `get_html`. This is more or less a direct extraction from `get_html`.
Returns: Returns:
Tuple of (track_url, transcript_language, sorted_languages) Tuple of (track_url, transcript_language, sorted_languages)
track_url -> subtitle download url track_url -> subtitle download url
transcript_language -> default transcript language transcript_language -> default transcript language
sorted_languages -> dictionary of available transcript languages sorted_languages -> dictionary of available transcript languages
...@@ -175,6 +174,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -175,6 +174,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
sources = filter(None, self.html5_sources) sources = filter(None, self.html5_sources)
download_video_link = None download_video_link = None
branding_info = None
youtube_streams = "" youtube_streams = ""
# If we have an edx_video_id, we prefer its values over what we store # If we have an edx_video_id, we prefer its values over what we store
...@@ -213,8 +213,9 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -213,8 +213,9 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
# Video caching is disabled for Studio. User_location is always None in Studio. # Video caching is disabled for Studio. User_location is always None in Studio.
# CountryMiddleware disabled for Studio. # CountryMiddleware disabled for Studio.
cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(self.system.user_location) cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(self.system.user_location)
if getattr(self, 'video_speed_optimizations', True) and cdn_url: if getattr(self, 'video_speed_optimizations', True) and cdn_url:
branding_info = BrandingInfoConfig.get_config().get(self.system.user_location)
for index, source_url in enumerate(sources): for index, source_url in enumerate(sources):
new_url = get_video_from_cdn(cdn_url, source_url) new_url = get_video_from_cdn(cdn_url, source_url)
if new_url: if new_url:
...@@ -233,6 +234,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -233,6 +234,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
return self.system.render_template('video.html', { return self.system.render_template('video.html', {
'ajax_url': self.system.ajax_url + '/save_user_state', 'ajax_url': self.system.ajax_url + '/save_user_state',
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False), 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
'branding_info': branding_info,
# This won't work when we move to data that # This won't work when we move to data that
# isn't on the filesystem # isn't on the filesystem
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
...@@ -286,7 +288,6 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -286,7 +288,6 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" """
Mostly handles backward compatibility issues. Mostly handles backward compatibility issues.
`source` is deprecated field. `source` is deprecated field.
a) If `source` exists and `source` is not `html5_sources`: show `source` a) If `source` exists and `source` is not `html5_sources`: show `source`
field on front-end as not-editable but clearable. Dropdown is a new field on front-end as not-editable but clearable. Dropdown is a new
...@@ -331,21 +332,16 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -331,21 +332,16 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
def editor_saved(self, user, old_metadata, old_content): def editor_saved(self, user, old_metadata, old_content):
""" """
Used to update video values during `self`:save method from CMS. Used to update video values during `self`:save method from CMS.
old_metadata: dict, values of fields of `self` with scope=settings which were explicitly set by user. old_metadata: dict, values of fields of `self` with scope=settings which were explicitly set by user.
old_content, same as `old_metadata` but for scope=content. old_content, same as `old_metadata` but for scope=content.
Due to nature of code flow in item.py::_save_item, before current function is called, Due to nature of code flow in item.py::_save_item, before current function is called,
fields of `self` instance have been already updated, but not yet saved. fields of `self` instance have been already updated, but not yet saved.
To obtain values, which were changed by user input, To obtain values, which were changed by user input,
one should compare own_metadata(self) and old_medatada. one should compare own_metadata(self) and old_medatada.
Video player has two tabs, and due to nature of sync between tabs, Video player has two tabs, and due to nature of sync between tabs,
metadata from Basic tab is always sent when video player is edited and saved first time, for example: metadata from Basic tab is always sent when video player is edited and saved first time, for example:
{'youtube_id_1_0': u'OEoXaMPEzfM', 'display_name': u'Video', 'sub': u'OEoXaMPEzfM', 'html5_sources': []}, {'youtube_id_1_0': u'OEoXaMPEzfM', 'display_name': u'Video', 'sub': u'OEoXaMPEzfM', 'html5_sources': []},
that's why these fields will always present in old_metadata after first save. This should be fixed. that's why these fields will always present in old_metadata after first save. This should be fixed.
At consequent save requests html5_sources are always sent too, disregard of their change by user. At consequent save requests html5_sources are always sent too, disregard of their change by user.
That means that html5_sources are always in list of fields that were changed (`metadata` param in save_item). That means that html5_sources are always in list of fields that were changed (`metadata` param in save_item).
This should be fixed too. This should be fixed too.
...@@ -390,7 +386,6 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler ...@@ -390,7 +386,6 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
""" """
Creates an instance of this descriptor from the supplied xml_data. Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for xml_data: A string of xml that will be translated into data and children for
this module this module
system: A DescriptorSystem for interacting with external resources system: A DescriptorSystem for interacting with external resources
......
# -*- coding: utf-8 -*-
""" """
Module contains utils specific for video_module but not for transcripts. Module contains utils specific for video_module but not for transcripts.
""" """
...@@ -33,6 +34,8 @@ def create_youtube_string(module): ...@@ -33,6 +34,8 @@ def create_youtube_string(module):
]) ])
# def get_video_from_cdn(cdn_base_url, original_video_url, cdn_branding_logo_url):
# Not sure if this third variable is necessary...
def get_video_from_cdn(cdn_base_url, original_video_url): def get_video_from_cdn(cdn_base_url, original_video_url):
""" """
Get video URL from CDN. Get video URL from CDN.
...@@ -46,7 +49,7 @@ def get_video_from_cdn(cdn_base_url, original_video_url): ...@@ -46,7 +49,7 @@ def get_video_from_cdn(cdn_base_url, original_video_url):
"http://cm12.c110.play.bokecc.com/flvs/ca/QxcVl/u39EQbA0Ra-20.mp4", "http://cm12.c110.play.bokecc.com/flvs/ca/QxcVl/u39EQbA0Ra-20.mp4",
"http://bm1.42.play.bokecc.com/flvs/ca/QxcVl/u39EQbA0Ra-20.mp4" "http://bm1.42.play.bokecc.com/flvs/ca/QxcVl/u39EQbA0Ra-20.mp4"
], ],
"s3_url": "http://s3.amazonaws.com/BESTech/CS169/download/CS169_v13_w5l2s3.mp4" "s3_url": "http://s3.amazonaws.com/BESTech/CS169/download/CS169_v13_w5l2s3.mp4",
} }
where `s3_url` is requested original video url and `sources` is the list of where `s3_url` is requested original video url and `sources` is the list of
alternative links. alternative links.
......
'''
Django admin pages for Video Branding Configuration.
'''
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
from .models import BrandingInfoConfig
admin.site.register(BrandingInfoConfig, ConfigurationModelAdmin)
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'BrandingInfoConfig'
db.create_table('branding_brandinginfoconfig', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)),
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
('configuration', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal('branding', ['BrandingInfoConfig'])
def backwards(self, orm):
# Deleting model 'BrandingInfoConfig'
db.delete_table('branding_brandinginfoconfig')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'branding.brandinginfoconfig': {
'Meta': {'object_name': 'BrandingInfoConfig'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'configuration': ('django.db.models.fields.TextField', [], {}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['branding']
"""
Model used by Video module for Branding configuration.
Includes:
BrandingInfoConfig: A ConfigurationModel for managing how Video Module will
use Branding.
"""
import json
from django.db.models import TextField
from django.core.exceptions import ValidationError
from config_models.models import ConfigurationModel
class BrandingInfoConfig(ConfigurationModel):
"""
Configuration for Branding.
Example of configuration that must be stored:
{
"CN": {
"url": "http://www.xuetangx.com",
"logo_src": "http://www.xuetangx.com/static/images/logo.png",
"logo_tag": "Video hosted by XuetangX.com"
}
}
"""
configuration = TextField(
help_text="JSON data of Configuration for Video Branding."
)
def clean(self):
"""
Validates configuration text field.
"""
try:
json.loads(self.configuration)
except ValueError:
raise ValidationError('Must be valid JSON string.')
@classmethod
def get_config(cls):
"""
Get the Video Branding Configuration.
"""
info = cls.current()
return json.loads(info.configuration) if info.enabled else {}
"""
Tests for the Video Branding configuration.
"""
from django.test import TestCase
from django.core.exceptions import ValidationError
from branding.models import BrandingInfoConfig
class BrandingInfoConfigTest(TestCase):
"""
Test the BrandingInfoConfig model.
"""
def setUp(self):
self.configuration_string = """{
"CN": {
"url": "http://www.xuetangx.com",
"logo_src": "http://www.xuetangx.com/static/images/logo.png",
"logo_tag": "Video hosted by XuetangX.com"
}
}"""
self.config = BrandingInfoConfig(configuration=self.configuration_string)
def test_create(self):
"""
Tests creation of configuration.
"""
self.config.save()
self.assertEquals(self.config.configuration, self.configuration_string)
def test_clean_bad_json(self):
"""
Tests if bad Json string was given.
"""
self.config = BrandingInfoConfig(configuration='{"bad":"test"')
self.assertRaises(ValidationError, self.config.clean)
def test_get(self):
"""
Tests get configuration from saved string.
"""
self.config.enabled = True
self.config.save()
expected_config = {
"CN": {
"url": "http://www.xuetangx.com",
"logo_src": "http://www.xuetangx.com/static/images/logo.png",
"logo_tag": "Video hosted by XuetangX.com"
}
}
self.assertEquals(self.config.get_config(), expected_config)
def test_get_not_enabled(self):
"""
Tests get configuration that is not enabled.
"""
self.config.enabled = False
self.config.save()
self.assertEquals(self.config.get_config(), {})
...@@ -112,7 +112,7 @@ class AnonymousIndexPageTest(ModuleStoreTestCase): ...@@ -112,7 +112,7 @@ class AnonymousIndexPageTest(ModuleStoreTestCase):
# Response should be instance of HttpResponseRedirect. # Response should be instance of HttpResponseRedirect.
self.assertIsInstance(response, HttpResponseRedirect) self.assertIsInstance(response, HttpResponseRedirect)
# Location should be "/login". # Location should be "/login".
self.assertEqual(response._headers.get("location")[1], "/login") self.assertEqual(response._headers.get("location")[1], "/login") # pylint: disable=protected-access
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
...@@ -198,7 +198,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase): ...@@ -198,7 +198,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
def test_course_cards_sorted_by_default_sorting(self): def test_course_cards_sorted_by_default_sorting(self):
response = self.client.get('/') response = self.client.get('/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
((template, context), _) = RENDER_MOCK.call_args ((template, context), _) = RENDER_MOCK.call_args # pylint: disable=unpacking-non-sequence
self.assertEqual(template, 'index.html') self.assertEqual(template, 'index.html')
# Now the courses will be stored in their announcement dates. # Now the courses will be stored in their announcement dates.
...@@ -209,7 +209,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase): ...@@ -209,7 +209,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
# check the /courses view # check the /courses view
response = self.client.get(reverse('branding.views.courses')) response = self.client.get(reverse('branding.views.courses'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
((template, context), _) = RENDER_MOCK.call_args ((template, context), _) = RENDER_MOCK.call_args # pylint: disable=unpacking-non-sequence
self.assertEqual(template, 'courseware/courses.html') self.assertEqual(template, 'courseware/courses.html')
# Now the courses will be stored in their announcement dates. # Now the courses will be stored in their announcement dates.
...@@ -223,7 +223,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase): ...@@ -223,7 +223,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
def test_course_cards_sorted_by_start_date_show_earliest_first(self): def test_course_cards_sorted_by_start_date_show_earliest_first(self):
response = self.client.get('/') response = self.client.get('/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
((template, context), _) = RENDER_MOCK.call_args ((template, context), _) = RENDER_MOCK.call_args # pylint: disable=unpacking-non-sequence
self.assertEqual(template, 'index.html') self.assertEqual(template, 'index.html')
# now the courses will be sorted by their creation dates, earliest first. # now the courses will be sorted by their creation dates, earliest first.
...@@ -234,7 +234,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase): ...@@ -234,7 +234,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
# check the /courses view as well # check the /courses view as well
response = self.client.get(reverse('branding.views.courses')) response = self.client.get(reverse('branding.views.courses'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
((template, context), _) = RENDER_MOCK.call_args ((template, context), _) = RENDER_MOCK.call_args # pylint: disable=unpacking-non-sequence
self.assertEqual(template, 'courseware/courses.html') self.assertEqual(template, 'courseware/courses.html')
# now the courses will be sorted by their creation dates, earliest first. # now the courses will be sorted by their creation dates, earliest first.
......
...@@ -39,6 +39,7 @@ class TestVideoYouTube(TestVideo): ...@@ -39,6 +39,7 @@ class TestVideoYouTube(TestVideo):
expected_context = { expected_context = {
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False), 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
'branding_info': None,
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'display_name': u'A Name', 'display_name': u'A Name',
'end': 3610.0, 'end': 3610.0,
...@@ -102,6 +103,7 @@ class TestVideoNonYouTube(TestVideo): ...@@ -102,6 +103,7 @@ class TestVideoNonYouTube(TestVideo):
expected_context = { expected_context = {
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'branding_info': None,
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
...@@ -204,6 +206,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -204,6 +206,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
sources = json.dumps([u'example.mp4', u'example.webm']) sources = json.dumps([u'example.mp4', u'example.webm'])
expected_context = { expected_context = {
'branding_info': None,
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
...@@ -320,6 +323,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -320,6 +323,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
] ]
initial_context = { initial_context = {
'branding_info': None,
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
...@@ -459,6 +463,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -459,6 +463,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
# Video found for edx_video_id # Video found for edx_video_id
initial_context = { initial_context = {
'branding_info': None,
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
...@@ -576,6 +581,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -576,6 +581,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
# Video found for edx_video_id # Video found for edx_video_id
initial_context = { initial_context = {
'branding_info': None,
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
...@@ -628,11 +634,22 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -628,11 +634,22 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context) self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
) )
# pylint: disable=invalid-name
@patch('xmodule.video_module.video_module.BrandingInfoConfig')
@patch('xmodule.video_module.video_module.get_video_from_cdn') @patch('xmodule.video_module.video_module.get_video_from_cdn')
def test_get_html_cdn_source(self, mocked_get_video): def test_get_html_cdn_source(self, mocked_get_video, mock_BrandingInfoConfig):
""" """
Test if sources got from CDN. Test if sources got from CDN.
""" """
mock_BrandingInfoConfig.get_config.return_value = {
"CN": {
'url': 'http://www.xuetangx.com',
'logo_src': 'http://www.xuetangx.com/static/images/logo.png',
'logo_tag': 'Video hosted by XuetangX.com'
}
}
def side_effect(*args, **kwargs): def side_effect(*args, **kwargs):
cdn = { cdn = {
'http://example.com/example.mp4': 'http://cdn_example.com/example.mp4', 'http://example.com/example.mp4': 'http://cdn_example.com/example.mp4',
...@@ -679,6 +696,11 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -679,6 +696,11 @@ class TestGetHtmlMethod(BaseTestXmodule):
] ]
initial_context = { initial_context = {
'branding_info': {
'logo_src': 'http://www.xuetangx.com/static/images/logo.png',
'logo_tag': 'Video hosted by XuetangX.com',
'url': 'http://www.xuetangx.com'
},
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
......
...@@ -1568,6 +1568,7 @@ INSTALLED_APPS = ( ...@@ -1568,6 +1568,7 @@ INSTALLED_APPS = (
'licenses', 'licenses',
'openedx.core.djangoapps.course_groups', 'openedx.core.djangoapps.course_groups',
'bulk_email', 'bulk_email',
'branding',
# External auth (OpenID, shib) # External auth (OpenID, shib)
'external_auth', 'external_auth',
......
# -*- coding: utf-8 -*-
""" """
This config file runs the simplest dev environment using sqlite, and db-based This config file runs the simplest dev environment using sqlite, and db-based
sessions. Assumes structure: sessions. Assumes structure:
......
...@@ -121,8 +121,8 @@ ...@@ -121,8 +121,8 @@
% else: % else:
<li class="a11y-menu-item"> <li class="a11y-menu-item">
% endif % endif
## This is necessary so we don't scrape 'display_name' as a string. ## This is necessary so we don't scrape 'display_name' as a string.
<% dname = item['display_name'] %> <% dname = item['display_name'] %>
<a class="a11y-menu-item-link" href="#${item['value']}" title="${_(dname)}" data-value="${item['value']}"> <a class="a11y-menu-item-link" href="#${item['value']}" title="${_(dname)}" data-value="${item['value']}">
${_(dname)} ${_(dname)}
</a> </a>
...@@ -141,5 +141,12 @@ ...@@ -141,5 +141,12 @@
${('<a href="%s" target="_blank">' + _('Download Handout') + '</a>') % handout} ${('<a href="%s" target="_blank">' + _('Download Handout') + '</a>') % handout}
</li> </li>
% endif % endif
% if branding_info:
<li id="branding" class="branding">
<span class="host-tag">${branding_info['logo_tag']}</span>
<a href="${branding_info['url']}" target="_blank" title="${branding_info['logo_tag']}"><img class="brand-logo" src="${branding_info['logo_src']}" alt="${branding_info['logo_tag']}" /></a>
</li>
% endif
</ul> </ul>
</div> </div>
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