Skip to content
Merged
8 changes: 8 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,11 @@
##### EMBARGO #####
EMBARGO_SITE_REDIRECT_URL = None

##### custom vendor plugin variables #####
# JavaScript code can access this data using `process.env.JS_ENV_EXTRA_CONFIG`
# One of the current use cases for this is enabling custom TinyMCE plugins
JS_ENV_EXTRA_CONFIG = {}

############################### PIPELINE #######################################

PIPELINE = {
Expand Down Expand Up @@ -1509,6 +1514,9 @@
'openedx.core.djangoapps.schedules',
'rest_framework_jwt',

# GDPR user retirement
'lms.djangoapps.gdpr_user_retirement',

# Learning Sequence Navigation
'openedx.core.djangoapps.content.learning_sequences.apps.LearningSequencesConfig',

Expand Down
1 change: 1 addition & 0 deletions cms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
url(r'^api/val/v0/', include('edxval.urls')),
url(r'^api/tasks/v0/', include('user_tasks.urls')),
url(r'^accessibility$', contentstore_views.accessibility, name='accessibility'),
url(r'', include('lms.djangoapps.gdpr_user_retirement.urls')),
]

if not settings.DISABLE_DEPRECATED_SIGNIN_URL:
Expand Down
Empty file.
24 changes: 24 additions & 0 deletions common/djangoapps/third_party_auth/config/waffle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
This module contains various configuration settings via
waffle switches for third party authentication.
"""


from edx_toggles.toggles.__future__ import WaffleSwitch


WAFFLE_NAMESPACE = 'third_party_auth'

# .. toggle_name: ALWAYS_ASSOCIATE_USER_BY_EMAIL
# .. toggle_implementation: WaffleSwitch
# .. toggle_default: False
# .. toggle_description: Always associates current social auth user with
# the user with the same email address in the database, which verifies
# that only a single database user is associated with the email.
# .. toggle_use_cases: opt_in
# .. toggle_creation_date: 2020-12-23
# .. toggle_tickets: https://openedx.atlassian.net/browse/OSPR-5312
ALWAYS_ASSOCIATE_USER_BY_EMAIL = WaffleSwitch(
f'{WAFFLE_NAMESPACE}.always_associate_user_by_email',
module_name=__name__,
)
4 changes: 2 additions & 2 deletions common/djangoapps/third_party_auth/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
def xframe_allow_whitelisted(view_func):
"""
Modifies a view function so that its response has the X-Frame-Options HTTP header
set to 'DENY' if the request HTTP referrer is not from a whitelisted hostname.
set to `settings.X_FRAME_OPTIONS` if the request HTTP referrer is not from a whitelisted hostname.
"""

def wrapped_view(request, *args, **kwargs):
""" Modify the response with the correct X-Frame-Options. """
resp = view_func(request, *args, **kwargs)
x_frame_option = 'DENY'
x_frame_option = settings.X_FRAME_OPTIONS
if settings.FEATURES['ENABLE_THIRD_PARTY_AUTH']:
referer = request.META.get('HTTP_REFERER')
if referer is not None:
Expand Down
14 changes: 9 additions & 5 deletions common/djangoapps/third_party_auth/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def B(*args, **kwargs):
from django.urls import reverse
from social_core.exceptions import AuthException
from social_core.pipeline import partial
from social_core.pipeline.social_auth import associate_by_email
from social_core.pipeline.social_auth import associate_by_email as _associate_by_email
from social_core.utils import module_member, slugify

from common.djangoapps import third_party_auth
Expand All @@ -87,6 +87,7 @@ def B(*args, **kwargs):
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api import accounts
from openedx.core.djangoapps.user_authn import cookies as user_authn_cookies
from common.djangoapps.third_party_auth.config.waffle import ALWAYS_ASSOCIATE_USER_BY_EMAIL
from common.djangoapps.third_party_auth.utils import user_exists
from common.djangoapps.track import segment
from common.djangoapps.util.json_request import JsonResponse
Expand Down Expand Up @@ -712,16 +713,19 @@ def login_analytics(strategy, auth_entry, current_partial=None, *args, **kwargs)


@partial.partial
def associate_by_email_if_login_api(auth_entry, backend, details, user, current_partial=None, *args, **kwargs):
def associate_user_by_email(auth_entry, backend, details, user, *args, current_partial=None, **kwargs):
"""
This pipeline step associates the current social auth with the user with the
same email address in the database. It defers to the social library's associate_by_email
implementation, which verifies that only a single database user is associated with the email.

This association is done ONLY if the user entered the pipeline through a LOGIN API.
This association is done ONLY if:
the user entered the pipeline through a LOGIN API.
OR
the `third_party_auth.always_associate_user_by_email` Waffle Switch is Active
"""
if auth_entry == AUTH_ENTRY_LOGIN_API:
association_response = associate_by_email(backend, details, user, *args, **kwargs)
if auth_entry == AUTH_ENTRY_LOGIN_API or ALWAYS_ASSOCIATE_USER_BY_EMAIL.is_enabled():
association_response = _associate_by_email(backend, details, user, *args, **kwargs)
if (
association_response and
association_response.get('user') and
Expand Down
2 changes: 1 addition & 1 deletion common/djangoapps/third_party_auth/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def apply_settings(django_settings):
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'social_core.pipeline.social_auth.social_user',
'common.djangoapps.third_party_auth.pipeline.associate_by_email_if_login_api',
'common.djangoapps.third_party_auth.pipeline.associate_user_by_email',
'common.djangoapps.third_party_auth.pipeline.get_username',
'common.djangoapps.third_party_auth.pipeline.set_pipeline_timeout',
'common.djangoapps.third_party_auth.pipeline.ensure_user_information',
Expand Down
56 changes: 56 additions & 0 deletions common/djangoapps/third_party_auth/tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import unittest

import ddt
from edx_toggles.toggles.testutils import override_waffle_switch
import mock

from common.djangoapps.third_party_auth import pipeline
from common.djangoapps.third_party_auth.pipeline import ALWAYS_ASSOCIATE_USER_BY_EMAIL
from common.djangoapps.third_party_auth.tests import testutil
from common.djangoapps.third_party_auth.tests.specs.base import IntegrationTestMixin
from common.djangoapps.third_party_auth.tests.specs.test_testshib import SamlIntegrationTestUtilities
Expand Down Expand Up @@ -99,3 +101,57 @@ def test_get_username_in_pipeline(self, idp_username, expected_username, already
mock_uuid.return_value = uuid4
final_username = pipeline.get_username(strategy, details, self.provider.backend_class())
self.assertEqual(expected_username, final_username['username'])

@ddt.data(
('login', False),
('login_api', True),
)
@ddt.unpack
@mock.patch('common.djangoapps.third_party_auth.pipeline._associate_by_email')
def test_associate_user_by_email_in_pipeline_auth_entry(
self,
auth_entry,
called_expected,
mock_associate_by_email,
):
"""
Tests associate_user_by_email method of running pipeline
"""
pipeline.associate_user_by_email(
strategy=mock.MagicMock(),
pipeline_index=0,
auth_entry=auth_entry,
backend=self.provider.backend_class(),
details=None,
user=None,
)

self.assertEqual(mock_associate_by_email.called, called_expected)

@ddt.data(
(ALWAYS_ASSOCIATE_USER_BY_EMAIL, True, True),
(ALWAYS_ASSOCIATE_USER_BY_EMAIL, False, False),
)
@ddt.unpack
@mock.patch('common.djangoapps.third_party_auth.pipeline._associate_by_email')
def test_associate_user_by_email_in_pipeline_waffle_switches(
self,
waffle_switch,
switch_is_active,
called_expected,
mock_associate_by_email,
):
"""
Tests associate_user_by_email method of running pipeline
"""
with override_waffle_switch(waffle_switch, switch_is_active):
pipeline.associate_user_by_email(
strategy=mock.MagicMock(),
pipeline_index=0,
auth_entry='login',
backend=self.provider.backend_class(),
details=None,
user=None,
)

self.assertEqual(mock_associate_by_email.called, called_expected)
32 changes: 31 additions & 1 deletion common/lib/xmodule/xmodule/js/spec/video/completion_spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(function() {
'use strict';
describe('VideoPlayer completion', function() {
var state, oldOTBD, completionAjaxCall;
var state, oldOTBD, completionAjaxCall, time;

beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
Expand Down Expand Up @@ -67,5 +67,35 @@
state.el.trigger('ended');
expect(state.completionHandler.markCompletion).toHaveBeenCalled();
});

it('triggers progress', function(done) {
var duration = 0;
jasmine.waitUntil(function() {
duration = state.videoPlayer.duration();
return duration > 0;
}).then(function() {
spyOn(state.completionHandler, 'computeProgress').and.callThrough();
spyOn(state.completionHandler, 'triggerProgress').and.callThrough();
// 4 percents should be equivalent to 0
time = 4 * duration / 100;
state.el.trigger('timeupdate', time);
expect(state.completionHandler.computeProgress).toHaveBeenCalled();
expect(state.completionHandler.triggerProgress).toHaveBeenCalled();
state.completionHandler.computeProgress.calls.reset();
state.completionHandler.triggerProgress.calls.reset();
// 8 percents should be equivalent to 5
time = 8 * duration / 100;
state.el.trigger('timeupdate', time);
expect(state.completionHandler.computeProgress).toHaveBeenCalled();
expect(state.completionHandler.triggerProgress).toHaveBeenCalled();
state.completionHandler.computeProgress.calls.reset();
state.completionHandler.triggerProgress.calls.reset();
// Another timeupdate in the same 5-range should not trigger "triggerProgress"
time = 9 * duration / 100;
state.el.trigger('timeupdate', time);
expect(state.completionHandler.computeProgress).toHaveBeenCalled();
expect(state.completionHandler.triggerProgress).not.toHaveBeenCalled();
}).always(done);
});
});
}).call(this);
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ import '../helper.js'
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
});

it('can emit "progress_video" event', function() {
state.el.trigger('progress', [10]);
expect(Logger.log).toHaveBeenCalledWith('progress_video', {
id: 'id',
code: this.code,
percentage: 10,
duration: this.duration
});
});

it('can emit "speed_change_video" event', function() {
state.el.trigger('speedchange', ['2.0', '1.0']);
expect(Logger.log).toHaveBeenCalledWith('speed_change_video', {
Expand Down Expand Up @@ -204,6 +214,7 @@ import '../helper.js'
skip: plugin.onSkip,
speedchange: plugin.onSpeedChange,
autoadvancechange: plugin.onAutoAdvanceChange,
progress: plugin.onProgress,
'language_menu:show': plugin.onShowLanguageMenu,
'language_menu:hide': plugin.onHideLanguageMenu,
'transcript:show': plugin.onShowTranscript,
Expand Down
39 changes: 37 additions & 2 deletions common/lib/xmodule/xmodule/js/src/html/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
tinyMCE incorrectly decides that the suffix should be "", which means it fails to load files.
*/
tinyMCE.suffix = ".min";
this.tiny_mce_textarea = $(".tiny-mce", this.element).tinymce({

var tinyMceConfig = {
script_url: baseUrl + "js/vendor/tinymce/js/tinymce/tinymce.full.min.js",
font_formats: _getFonts(),
theme: "modern",
Expand Down Expand Up @@ -171,7 +172,41 @@
*/
init_instance_callback: this.initInstanceCallback,
browser_spellcheck: true
});
};

if (typeof process != "undefined" && process.env.JS_ENV_EXTRA_CONFIG) {
var tinyMceAdditionalPlugins = process.env.JS_ENV_EXTRA_CONFIG.TINYMCE_ADDITIONAL_PLUGINS;
// check if we have any additional plugins passed
if (tinyMceAdditionalPlugins) {
// go over each plugin
tinyMceAdditionalPlugins.forEach(function (tinyMcePlugin) {
// check if plugins is not empty (ie there are existing plugins)
if (tinyMceConfig.plugins.trim()) {
tinyMceConfig.plugins += ', ';
}

// add the plugin to the list of plugins
tinyMceConfig.plugins += tinyMcePlugin.name;

// check if the plugin should be included in the toolbar
if (tinyMcePlugin.toolbar) {
// check if toolbar is not empty (ie there are already items in the toolbar)
if (tinyMceConfig.toolbar.trim()) {
tinyMceConfig.toolbar += ' | ';
}

tinyMceConfig.toolbar += tinyMcePlugin.name;
}

// add the additional settings for each plugin (if there is any)
if (tinyMcePlugin.extra_settings) {
tinyMceConfig[tinyMcePlugin.name] = tinyMcePlugin.extra_settings;
}
});
}
}

this.tiny_mce_textarea = $(".tiny-mce", this.element).tinymce(tinyMceConfig);
tinymce.addI18n('en', {

/*
Expand Down
28 changes: 27 additions & 1 deletion common/lib/xmodule/xmodule/js/src/video/09_completion.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
// the beginning of the video, except for lastSentTime, which refers to a
// timestamp in seconds since the Unix epoch.
this.lastSentTime = undefined;
this.lastProgressPercentage = undefined;
this.isComplete = false;
this.completionPercentage = this.state.config.completionPercentage;
this.startTime = this.state.config.startTime;
Expand Down Expand Up @@ -102,14 +103,20 @@

/** Handler to call when a timeupdate event is triggered */
handleTimeUpdate: function(currentTime) {
var duration;
var duration = this.state.videoPlayer.duration();
if (this.isComplete) {
return;
}
if (this.lastSentTime !== undefined && currentTime - this.lastSentTime < this.repostDelaySeconds()) {
// Throttle attempts to submit in case of network issues
return;
}

// Duration may not be available at initialization time
if (duration) {
this.computeProgress(currentTime, duration);
}

if (this.completeAfterTime === undefined) {
// Duration is not available at initialization time
duration = this.state.videoPlayer.duration();
Expand All @@ -126,6 +133,25 @@
}
},

/** Compute current video progression and trigger event if needed */
computeProgress: function(currentTime, duration) {
// Compute current progress percentage
var currentProgressPercentage = Math.floor(currentTime * 100 / duration / 5) * 5;
// Check if last "lastProgressPercentage" and current percentage are in the same 5-range
var newRange = currentProgressPercentage > this.lastProgressPercentage;
// If no previous "lastProgressPercentage" or different 5-range, trigger the event
if (this.lastProgressPercentage === undefined || newRange) {
this.triggerProgress(Math.floor(currentProgressPercentage));
// Save the lastProgressPercentage value
this.lastProgressPercentage = currentProgressPercentage;
}
},

/** Trigger progress event */
triggerProgress: function(percentage) {
this.state.el.trigger('progress', [percentage]);
},

/** Submit completion to the LMS */
markCompletion: function(currentTime) {
var self = this;
Expand Down
8 changes: 7 additions & 1 deletion common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
return new EventsPlugin(state, i18n, options);
}

_.bindAll(this, 'onReady', 'onPlay', 'onPause', 'onEnded', 'onSeek',
// eslint-disable-next-line no-undef
_.bindAll(this, 'onReady', 'onPlay', 'onPause', 'onEnded', 'onSeek', 'onProgress',
'onSpeedChange', 'onAutoAdvanceChange', 'onShowLanguageMenu', 'onHideLanguageMenu',
'onSkip', 'onShowTranscript', 'onHideTranscript', 'onShowCaptions', 'onHideCaptions',
'destroy');
Expand Down Expand Up @@ -46,6 +47,7 @@
skip: this.onSkip,
speedchange: this.onSpeedChange,
autoadvancechange: this.onAutoAdvanceChange,
progress: this.onProgress,
'language_menu:show': this.onShowLanguageMenu,
'language_menu:hide': this.onHideLanguageMenu,
'transcript:show': this.onShowTranscript,
Expand Down Expand Up @@ -136,6 +138,10 @@
this.log('edx.video.closed_captions.hidden', {current_time: this.getCurrentTime()});
},

onProgress: function(event, percentage) {
this.log('progress_video', {percentage: percentage});
},

getCurrentTime: function() {
var player = this.state.videoPlayer;
return player ? player.currentTime : 0;
Expand Down
Loading