Merge branch 'will/third-party-auth-fixes' into will/combine-reg-login-form
Conflicts: common/djangoapps/third_party_auth/__init__.py common/djangoapps/third_party_auth/pipeline.py
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,6 +55,7 @@ cover_html/
|
||||
reports/
|
||||
jscover.log
|
||||
jscover.log.*
|
||||
.tddium*
|
||||
|
||||
### Installation artifacts
|
||||
*.egg-info
|
||||
|
||||
@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Common: Add configurable reset button to units
|
||||
|
||||
LMS: Support adding cohorts from the instructor dashboard. TNL-162
|
||||
|
||||
LMS: Support adding students to a cohort via the instructor dashboard. TNL-163
|
||||
|
||||
@@ -13,6 +13,7 @@ MAXIMUM_ATTEMPTS = "Maximum Attempts"
|
||||
PROBLEM_WEIGHT = "Problem Weight"
|
||||
RANDOMIZATION = 'Randomization'
|
||||
SHOW_ANSWER = "Show Answer"
|
||||
SHOW_RESET_BUTTON = "Show Reset Button"
|
||||
TIMER_BETWEEN_ATTEMPTS = "Timer Between Attempts"
|
||||
MATLAB_API_KEY = "Matlab API key"
|
||||
|
||||
@@ -102,6 +103,7 @@ def i_see_advanced_settings_with_values(step):
|
||||
[PROBLEM_WEIGHT, "", False],
|
||||
[RANDOMIZATION, "Never", False],
|
||||
[SHOW_ANSWER, "Finished", False],
|
||||
[SHOW_RESET_BUTTON, "False", False],
|
||||
[TIMER_BETWEEN_ATTEMPTS, "0", False],
|
||||
])
|
||||
|
||||
|
||||
@@ -325,8 +325,7 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||
EMBARGO_SITE_REDIRECT_URL = None
|
||||
|
||||
############################### Pipeline #######################################
|
||||
|
||||
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
|
||||
STATICFILES_STORAGE = 'cms.lib.django_require.staticstorage.OptimizedCachedRequireJsStorage'
|
||||
|
||||
from rooted_paths import rooted_glob
|
||||
|
||||
@@ -457,6 +456,34 @@ STATICFILES_IGNORE_PATTERNS = (
|
||||
|
||||
PIPELINE_YUI_BINARY = 'yui-compressor'
|
||||
|
||||
################################# DJANGO-REQUIRE ###############################
|
||||
|
||||
# The baseUrl to pass to the r.js optimizer, relative to STATIC_ROOT.
|
||||
REQUIRE_BASE_URL = "./"
|
||||
|
||||
# The name of a build profile to use for your project, relative to REQUIRE_BASE_URL.
|
||||
# A sensible value would be 'app.build.js'. Leave blank to use the built-in default build profile.
|
||||
# Set to False to disable running the default profile (e.g. if only using it to build Standalone
|
||||
# Modules)
|
||||
REQUIRE_BUILD_PROFILE = "build.js"
|
||||
|
||||
# The name of the require.js script used by your project, relative to REQUIRE_BASE_URL.
|
||||
REQUIRE_JS = "js/vendor/require.js"
|
||||
|
||||
# A dictionary of standalone modules to build with almond.js.
|
||||
REQUIRE_STANDALONE_MODULES = {}
|
||||
|
||||
# Whether to run django-require in debug mode.
|
||||
REQUIRE_DEBUG = False
|
||||
|
||||
# A tuple of files to exclude from the compilation result of r.js.
|
||||
REQUIRE_EXCLUDE = ("build.txt",)
|
||||
|
||||
# The execution environment in which to run r.js: auto, node or rhino.
|
||||
# auto will autodetect the environment and make use of node if available and rhino if not.
|
||||
# It can also be a path to a custom class that subclasses require.environments.Environment and defines some "args" function that returns a list with the command arguments to execute.
|
||||
REQUIRE_ENVIRONMENT = "node"
|
||||
|
||||
################################# CELERY ######################################
|
||||
|
||||
# Message configuration
|
||||
@@ -563,6 +590,7 @@ INSTALLED_APPS = (
|
||||
'pipeline',
|
||||
'staticfiles',
|
||||
'static_replace',
|
||||
'require',
|
||||
|
||||
# comment common
|
||||
'django_comment_common',
|
||||
|
||||
0
cms/lib/django_require/__init__.py
Normal file
0
cms/lib/django_require/__init__.py
Normal file
13
cms/lib/django_require/staticstorage.py
Normal file
13
cms/lib/django_require/staticstorage.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
:class:`~django_require.staticstorage.OptimizedCachedRequireJsStorage`
|
||||
"""
|
||||
|
||||
from pipeline.storage import PipelineCachedStorage
|
||||
from require.storage import OptimizedFilesMixin
|
||||
|
||||
|
||||
class OptimizedCachedRequireJsStorage(OptimizedFilesMixin, PipelineCachedStorage):
|
||||
"""
|
||||
Custom storage backend that is used by Django-require.
|
||||
"""
|
||||
pass
|
||||
155
cms/static/build.js
Normal file
155
cms/static/build.js
Normal file
@@ -0,0 +1,155 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
var getModule = function (moduleName, excludeCommonDeps) {
|
||||
var module = {
|
||||
name: moduleName
|
||||
};
|
||||
|
||||
if (excludeCommonDeps) {
|
||||
module.exclude = ['js/factories/common_deps'];
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var getModulesList = function (modules) {
|
||||
var result = [getModule('js/factories/common_deps')];
|
||||
return result.concat(modules.map(function (moduleName) {
|
||||
return getModule(moduleName, true);
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* List the modules that will be optimized. All their immediate and deep
|
||||
* dependencies will be included in the module's file when the build is
|
||||
* done.
|
||||
*/
|
||||
modules: getModulesList([
|
||||
'js/factories/asset_index',
|
||||
'js/factories/base',
|
||||
'js/factories/checklists',
|
||||
'js/factories/container',
|
||||
'js/factories/course',
|
||||
'js/factories/course_create_rerun',
|
||||
'js/factories/course_info',
|
||||
'js/factories/edit_tabs',
|
||||
'js/factories/export',
|
||||
'js/factories/group_configurations',
|
||||
'js/factories/import',
|
||||
'js/factories/index',
|
||||
'js/factories/login',
|
||||
'js/factories/manage_users',
|
||||
'js/factories/outline',
|
||||
'js/factories/register',
|
||||
'js/factories/settings',
|
||||
'js/factories/settings_advanced',
|
||||
'js/factories/settings_graders',
|
||||
'js/factories/textbooks'
|
||||
]),
|
||||
/**
|
||||
* By default all the configuration for optimization happens from the command
|
||||
* line or by properties in the config file, and configuration that was
|
||||
* passed to requirejs as part of the app's runtime "main" JS file is *not*
|
||||
* considered. However, if you prefer the "main" JS file configuration
|
||||
* to be read for the build so that you do not have to duplicate the values
|
||||
* in a separate configuration, set this property to the location of that
|
||||
* main JS file. The first requirejs({}), require({}), requirejs.config({}),
|
||||
* or require.config({}) call found in that file will be used.
|
||||
* As of 2.1.10, mainConfigFile can be an array of values, with the last
|
||||
* value's config take precedence over previous values in the array.
|
||||
*/
|
||||
mainConfigFile: 'require-config.js',
|
||||
/**
|
||||
* Set paths for modules. If relative paths, set relative to baseUrl above.
|
||||
* If a special value of "empty:" is used for the path value, then that
|
||||
* acts like mapping the path to an empty file. It allows the optimizer to
|
||||
* resolve the dependency to path, but then does not include it in the output.
|
||||
* Useful to map module names that are to resources on a CDN or other
|
||||
* http: URL when running in the browser and during an optimization that
|
||||
* file should be skipped because it has no dependencies.
|
||||
*/
|
||||
paths: {
|
||||
'gettext': 'empty:',
|
||||
'xmodule': 'empty:',
|
||||
'mathjax': 'empty:',
|
||||
'tender': 'empty:',
|
||||
'youtube': 'empty:'
|
||||
},
|
||||
/**
|
||||
* If shim config is used in the app during runtime, duplicate the config
|
||||
* here. Necessary if shim config is used, so that the shim's dependencies
|
||||
* are included in the build. Using "mainConfigFile" is a better way to
|
||||
* pass this information though, so that it is only listed in one place.
|
||||
* However, if mainConfigFile is not an option, the shim config can be
|
||||
* inlined in the build config.
|
||||
*/
|
||||
shim: {
|
||||
'xmodule': {
|
||||
deps: [
|
||||
'jquery', 'underscore', 'mathjax', 'codemirror', 'tinymce',
|
||||
'jquery.tinymce', 'jquery.qtip', 'jquery.scrollTo', 'jquery.flot',
|
||||
'jquery.cookie', 'utility'
|
||||
]
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Introduced in 2.1.2: If using "dir" for an output directory, normally the
|
||||
* optimize setting is used to optimize the build bundles (the "modules"
|
||||
* section of the config) and any other JS file in the directory. However, if
|
||||
* the non-build bundle JS files will not be loaded after a build, you can
|
||||
* skip the optimization of those files, to speed up builds. Set this value
|
||||
* to true if you want to skip optimizing those other non-build bundle JS
|
||||
* files.
|
||||
*/
|
||||
skipDirOptimize: true,
|
||||
/**
|
||||
* When the optimizer copies files from the source location to the
|
||||
* destination directory, it will skip directories and files that start
|
||||
* with a ".". If you want to copy .directories or certain .files, for
|
||||
* instance if you keep some packages in a .packages directory, or copy
|
||||
* over .htaccess files, you can set this to null. If you want to change
|
||||
* the exclusion rules, change it to a different regexp. If the regexp
|
||||
* matches, it means the directory will be excluded. This used to be
|
||||
* called dirExclusionRegExp before the 1.0.2 release.
|
||||
* As of 1.0.3, this value can also be a string that is converted to a
|
||||
* RegExp via new RegExp().
|
||||
*/
|
||||
fileExclusionRegExp: /^\.|spec/,
|
||||
/**
|
||||
* Allow CSS optimizations. Allowed values:
|
||||
* - "standard": @import inlining and removal of comments, unnecessary
|
||||
* whitespace and line returns.
|
||||
* Removing line returns may have problems in IE, depending on the type
|
||||
* of CSS.
|
||||
* - "standard.keepLines": like "standard" but keeps line returns.
|
||||
* - "none": skip CSS optimizations.
|
||||
* - "standard.keepComments": keeps the file comments, but removes line
|
||||
* returns. (r.js 1.0.8+)
|
||||
* - "standard.keepComments.keepLines": keeps the file comments and line
|
||||
* returns. (r.js 1.0.8+)
|
||||
* - "standard.keepWhitespace": like "standard" but keeps unnecessary whitespace.
|
||||
*/
|
||||
optimizeCss: 'none',
|
||||
/**
|
||||
* How to optimize all the JS files in the build output directory.
|
||||
* Right now only the following values are supported:
|
||||
* - "uglify": Uses UglifyJS to minify the code.
|
||||
* - "uglify2": Uses UglifyJS2.
|
||||
* - "closure": Uses Google's Closure Compiler in simple optimization
|
||||
* mode to minify the code. Only available if REQUIRE_ENVIRONMENT is "rhino" (the default).
|
||||
* - "none": No minification will be done.
|
||||
*/
|
||||
optimize: 'uglify2',
|
||||
/**
|
||||
* Sets the logging level. It is a number:
|
||||
* TRACE: 0,
|
||||
* INFO: 1,
|
||||
* WARN: 2,
|
||||
* ERROR: 3,
|
||||
* SILENT: 4
|
||||
* Default is 0.
|
||||
*/
|
||||
logLevel: 4
|
||||
};
|
||||
} ())
|
||||
13
cms/static/js/factories/asset_index.js
Normal file
13
cms/static/js/factories/asset_index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
define([
|
||||
'jquery', 'js/collections/asset', 'js/views/assets', 'jquery.fileupload'
|
||||
], function($, AssetCollection, AssetsView) {
|
||||
'use strict';
|
||||
return function (assetCallbackUrl) {
|
||||
var assets = new AssetCollection(),
|
||||
assetsView;
|
||||
|
||||
assets.url = assetCallbackUrl;
|
||||
assetsView = new AssetsView({collection: assets, el: $('.assets-wrapper')});
|
||||
assetsView.render();
|
||||
};
|
||||
});
|
||||
2
cms/static/js/factories/base.js
Normal file
2
cms/static/js/factories/base.js
Normal file
@@ -0,0 +1,2 @@
|
||||
define(['js/base', 'coffee/src/main', 'coffee/src/logger', 'datepair', 'accessibility',
|
||||
'ieshim', 'tooltip_manager']);
|
||||
16
cms/static/js/factories/checklists.js
Normal file
16
cms/static/js/factories/checklists.js
Normal file
@@ -0,0 +1,16 @@
|
||||
define([
|
||||
'jquery', 'js/collections/checklist', 'js/views/checklist'
|
||||
], function($, ChecklistCollection, ChecklistView) {
|
||||
'use strict';
|
||||
return function (handlerUrl) {
|
||||
var checklistCollection = new ChecklistCollection(),
|
||||
editor;
|
||||
|
||||
checklistCollection.url = handlerUrl;
|
||||
editor = new ChecklistView({
|
||||
el: $('.course-checklists'),
|
||||
collection: checklistCollection
|
||||
});
|
||||
checklistCollection.fetch({reset: true});
|
||||
};
|
||||
});
|
||||
1
cms/static/js/factories/common_deps.js
Normal file
1
cms/static/js/factories/common_deps.js
Normal file
@@ -0,0 +1 @@
|
||||
define(['domReady!', 'jquery', 'backbone', 'underscore', 'gettext']);
|
||||
23
cms/static/js/factories/container.js
Normal file
23
cms/static/js/factories/container.js
Normal file
@@ -0,0 +1,23 @@
|
||||
define([
|
||||
'jquery', 'js/models/xblock_info', 'js/views/pages/container',
|
||||
'js/collections/component_template', 'xmodule', 'coffee/src/main',
|
||||
'xblock/cms.runtime.v1'
|
||||
],
|
||||
function($, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
|
||||
'use strict';
|
||||
return function (componentTemplates, XBlockInfoJson, action, isUnitPage) {
|
||||
var templates = new ComponentTemplates(componentTemplates, {parse: true}),
|
||||
mainXBlockInfo = new XBlockInfo(XBlockInfoJson, {parse: true});
|
||||
|
||||
xmoduleLoader.done(function () {
|
||||
var view = new ContainerPage({
|
||||
el: $('#content'),
|
||||
model: mainXBlockInfo,
|
||||
action: action,
|
||||
templates: templates,
|
||||
isUnitPage: isUnitPage
|
||||
});
|
||||
view.render();
|
||||
});
|
||||
};
|
||||
});
|
||||
6
cms/static/js/factories/course.js
Normal file
6
cms/static/js/factories/course.js
Normal file
@@ -0,0 +1,6 @@
|
||||
define(['js/models/course'], function(Course) {
|
||||
'use strict';
|
||||
return function (courseInfo) {
|
||||
window.course = new Course(courseInfo);
|
||||
}
|
||||
});
|
||||
4
cms/static/js/factories/course_create_rerun.js
Normal file
4
cms/static/js/factories/course_create_rerun.js
Normal file
@@ -0,0 +1,4 @@
|
||||
define(['jquery', 'jquery.form', 'js/views/course_rerun'], function ($) {
|
||||
'use strict';
|
||||
return function () {};
|
||||
});
|
||||
25
cms/static/js/factories/course_info.js
Normal file
25
cms/static/js/factories/course_info.js
Normal file
@@ -0,0 +1,25 @@
|
||||
define([
|
||||
'jquery', 'js/collections/course_update', 'js/models/module_info',
|
||||
'js/models/course_info', 'js/views/course_info_edit'
|
||||
], function($, CourseUpdateCollection, ModuleInfoModel, CourseInfoModel, CourseInfoEditView) {
|
||||
'use strict';
|
||||
return function (updatesUrl, handoutsLocator, baseAssetUrl) {
|
||||
var course_updates = new CourseUpdateCollection(),
|
||||
course_handouts, editor;
|
||||
|
||||
course_updates.url = updatesUrl;
|
||||
course_updates.fetch({reset: true});
|
||||
course_handouts = new ModuleInfoModel({
|
||||
id: handoutsLocator
|
||||
});
|
||||
editor = new CourseInfoEditView({
|
||||
el: $('.main-wrapper'),
|
||||
model : new CourseInfoModel({
|
||||
updates : course_updates,
|
||||
base_asset_url : baseAssetUrl,
|
||||
handouts : course_handouts
|
||||
})
|
||||
});
|
||||
editor.render();
|
||||
};
|
||||
});
|
||||
20
cms/static/js/factories/edit_tabs.js
Normal file
20
cms/static/js/factories/edit_tabs.js
Normal file
@@ -0,0 +1,20 @@
|
||||
define([
|
||||
'js/models/explicit_url', 'coffee/src/views/tabs', 'xmodule', 'coffee/src/main', 'xblock/cms.runtime.v1'
|
||||
], function (TabsModel, TabsEditView, xmoduleLoader) {
|
||||
'use strict';
|
||||
return function (courseLocation, explicitUrl) {
|
||||
xmoduleLoader.done(function () {
|
||||
var model = new TabsModel({
|
||||
id: courseLocation,
|
||||
explicit_url: explicitUrl
|
||||
}),
|
||||
editView;
|
||||
|
||||
editView = new TabsEditView({
|
||||
el: $('.tab-list'),
|
||||
model: model,
|
||||
mast: $('.wrapper-mast')
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
57
cms/static/js/factories/export.js
Normal file
57
cms/static/js/factories/export.js
Normal file
@@ -0,0 +1,57 @@
|
||||
define(['gettext', 'js/views/feedback_prompt'], function(gettext, PromptView) {
|
||||
'use strict';
|
||||
return function (hasUnit, editUnitUrl, courseHomeUrl, errMsg) {
|
||||
var dialog;
|
||||
if(hasUnit) {
|
||||
dialog = new PromptView({
|
||||
title: gettext('There has been an error while exporting.'),
|
||||
message: gettext('There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages.'),
|
||||
intent: 'error',
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Correct failed component'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
document.location = editUnitUrl;
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Return to Export'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var msg = '<p>' + gettext('There has been a failure to export your course to XML. Unfortunately, we do not have specific enough information to assist you in identifying the failed component. It is recommended that you inspect your courseware to identify any components in error and try again.') + '</p><p>' + gettext('The raw error message is:') + '</p>' + errMsg;
|
||||
dialog = new PromptView({
|
||||
title: gettext('There has been an error with your export.'),
|
||||
message: msg,
|
||||
intent: 'error',
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Yes, take me to the main course page'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
document.location = courseHomeUrl;
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Cancel'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The CSS animation for the dialog relies on the 'js' class
|
||||
// being on the body. This happens after this JavaScript is executed,
|
||||
// causing a 'bouncing' of the dialog after it is initially shown.
|
||||
// As a workaround, add this class first.
|
||||
$('body').addClass('js');
|
||||
dialog.show();
|
||||
};
|
||||
});
|
||||
16
cms/static/js/factories/group_configurations.js
Normal file
16
cms/static/js/factories/group_configurations.js
Normal file
@@ -0,0 +1,16 @@
|
||||
define([
|
||||
'js/collections/group_configuration', 'js/views/pages/group_configurations'
|
||||
], function(GroupConfigurationCollection, GroupConfigurationsPage) {
|
||||
'use strict';
|
||||
return function (configurations, groupConfigurationUrl, courseOutlineUrl) {
|
||||
var collection = new GroupConfigurationCollection(configurations, { parse: true }),
|
||||
configurationsPage;
|
||||
|
||||
collection.url = groupConfigurationUrl;
|
||||
collection.outlineUrl = courseOutlineUrl;
|
||||
configurationsPage = new GroupConfigurationsPage({
|
||||
el: $('#content'),
|
||||
collection: collection
|
||||
}).render();
|
||||
};
|
||||
});
|
||||
107
cms/static/js/factories/import.js
Normal file
107
cms/static/js/factories/import.js
Normal file
@@ -0,0 +1,107 @@
|
||||
define([
|
||||
'js/views/import', 'jquery', 'gettext', 'jquery.fileupload', 'jquery.cookie'
|
||||
], function(CourseImport, $, gettext) {
|
||||
'use strict';
|
||||
return function (feedbackUrl) {
|
||||
var bar = $('.progress-bar'),
|
||||
fill = $('.progress-fill'),
|
||||
submitBtn = $('.submit-button'),
|
||||
chooseBtn = $('.choose-file-button'),
|
||||
defaults = [
|
||||
gettext('There was an error during the upload process.') + '\n',
|
||||
gettext('There was an error while unpacking the file.') + '\n',
|
||||
gettext('There was an error while verifying the file you submitted.') + '\n',
|
||||
gettext('There was an error while importing the new course to our database.') + '\n'
|
||||
],
|
||||
// Display the status of last file upload on page load
|
||||
lastFileUpload = $.cookie('lastfileupload'),
|
||||
file;
|
||||
|
||||
if (lastFileUpload){
|
||||
CourseImport.getAndStartUploadFeedback(feedbackUrl.replace('fillerName', lastFileUpload), lastFileUpload);
|
||||
}
|
||||
|
||||
$('#fileupload').fileupload({
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
maxChunkSize: 20 * 1000000, // 20 MB
|
||||
autoUpload: false,
|
||||
add: function(e, data) {
|
||||
CourseImport.clearImportDisplay();
|
||||
submitBtn.unbind('click');
|
||||
file = data.files[0];
|
||||
if (file.name.match(/tar\.gz$/)) {
|
||||
submitBtn.click(function(event){
|
||||
event.preventDefault();
|
||||
$.cookie('lastfileupload', file.name);
|
||||
submitBtn.hide();
|
||||
CourseImport.startUploadFeedback();
|
||||
data.submit().complete(function(result, textStatus, xhr) {
|
||||
window.onbeforeunload = null;
|
||||
if (xhr.status != 200) {
|
||||
var serverMsg, errMsg, stage;
|
||||
try{
|
||||
serverMsg = $.parseJSON(result.responseText);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
errMsg = serverMsg.hasOwnProperty('ErrMsg') ? serverMsg.ErrMsg : '' ;
|
||||
if (serverMsg.hasOwnProperty('Stage')) {
|
||||
stage = Math.abs(serverMsg.Stage);
|
||||
CourseImport.stageError(stage, defaults[stage] + errMsg);
|
||||
}
|
||||
else {
|
||||
alert(gettext('Your import has failed.') + '\n\n' + errMsg);
|
||||
}
|
||||
chooseBtn.html(gettext('Choose new file')).show();
|
||||
bar.hide();
|
||||
}
|
||||
CourseImport.stopGetStatus = true;
|
||||
chooseBtn.html(gettext('Choose new file')).show();
|
||||
bar.hide();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
data.files = [];
|
||||
}
|
||||
},
|
||||
|
||||
progressall: function(e, data){
|
||||
var percentInt = data.loaded / data.total * 100,
|
||||
percentVal = parseInt(percentInt, 10) + '%',
|
||||
doneAt;
|
||||
// Firefox makes ProgressEvent.loaded equal ProgressEvent.total only
|
||||
// after receiving a response from the server (see Mozilla bug 637002),
|
||||
// so for Firefox we jump the gun a little.
|
||||
if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
|
||||
doneAt = 95;
|
||||
} else {
|
||||
doneAt = 99;
|
||||
}
|
||||
if (percentInt >= doneAt) {
|
||||
bar.hide();
|
||||
// Start feedback with delay so that current stage of import properly updates in session
|
||||
setTimeout(
|
||||
function () { CourseImport.startServerFeedback(feedbackUrl.replace('fillerName', file.name));},
|
||||
3000
|
||||
);
|
||||
} else {
|
||||
bar.show();
|
||||
fill.width(percentVal).html(percentVal);
|
||||
}
|
||||
},
|
||||
done: function(event, data){
|
||||
bar.hide();
|
||||
window.onbeforeunload = null;
|
||||
CourseImport.displayFinishedImport();
|
||||
},
|
||||
start: function(event) {
|
||||
window.onbeforeunload = function() {
|
||||
return gettext('Your import is in progress; navigating away will abort it.');
|
||||
};
|
||||
},
|
||||
sequentialUploads: true,
|
||||
notifyOnError: false
|
||||
});
|
||||
};
|
||||
});
|
||||
40
cms/static/js/factories/index.js
Normal file
40
cms/static/js/factories/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
define(['jquery.form', 'js/index'], function() {
|
||||
'use strict';
|
||||
return function () {
|
||||
// showing/hiding creation rights UI
|
||||
$('.show-creationrights').click(function(e) {
|
||||
e.preventDefault();
|
||||
$(this)
|
||||
.closest('.wrapper-creationrights')
|
||||
.toggleClass('is-shown')
|
||||
.find('.ui-toggle-control')
|
||||
.toggleClass('current');
|
||||
});
|
||||
|
||||
var reloadPage = function () {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
var showError = function () {
|
||||
$('#request-coursecreator-submit')
|
||||
.toggleClass('has-error')
|
||||
.find('.label')
|
||||
.text('Sorry, there was error with your request');
|
||||
$('#request-coursecreator-submit')
|
||||
.find('.icon-cog')
|
||||
.toggleClass('icon-spin');
|
||||
};
|
||||
|
||||
$('#request-coursecreator').ajaxForm({
|
||||
error: showError,
|
||||
success: reloadPage
|
||||
});
|
||||
|
||||
$('#request-coursecreator-submit').click(function(event){
|
||||
$(this)
|
||||
.toggleClass('is-disabled is-submitting')
|
||||
.find('.label')
|
||||
.text('Submitting Your Request');
|
||||
});
|
||||
};
|
||||
});
|
||||
43
cms/static/js/factories/login.js
Normal file
43
cms/static/js/factories/login.js
Normal file
@@ -0,0 +1,43 @@
|
||||
define(['jquery.cookie', 'utility'], function() {
|
||||
'use strict';
|
||||
return function (homepageURL) {
|
||||
function postJSON(url, data, callback) {
|
||||
$.ajax({
|
||||
type:'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
success: callback,
|
||||
headers : {'X-CSRFToken':$.cookie('csrftoken')}
|
||||
});
|
||||
}
|
||||
|
||||
$('form#login_form').submit(function(event) {
|
||||
event.preventDefault();
|
||||
var submit_data = $('#login_form').serialize();
|
||||
|
||||
postJSON('/login_post', submit_data, function(json) {
|
||||
if(json.success) {
|
||||
var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search));
|
||||
if (next && next.length > 1 && !isExternal(next[1])) {
|
||||
location.href = next[1];
|
||||
} else {
|
||||
location.href = homepageURL;
|
||||
}
|
||||
} else if($('#login_error').length === 0) {
|
||||
$('#login_form').prepend(
|
||||
'<div id="login_error" class="message message-status error">' +
|
||||
json.value +
|
||||
'</span></div>'
|
||||
);
|
||||
$('#login_error').addClass('is-shown');
|
||||
} else {
|
||||
$('#login_error')
|
||||
.stop()
|
||||
.addClass('is-shown')
|
||||
.html(json.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
203
cms/static/js/factories/manage_users.js
Normal file
203
cms/static/js/factories/manage_users.js
Normal file
@@ -0,0 +1,203 @@
|
||||
define(['jquery', 'underscore', 'gettext', 'js/views/feedback_prompt'], function($, _, gettext, PromptView) {
|
||||
'use strict';
|
||||
return function (staffEmails, tplUserURL) {
|
||||
var unknownErrorMessage = gettext('Unknown'),
|
||||
$createUserForm = $('#create-user-form'),
|
||||
$createUserFormWrapper = $createUserForm.closest('.wrapper-create-user'),
|
||||
$cancelButton;
|
||||
|
||||
$createUserForm.bind('submit', function(event) {
|
||||
event.preventDefault();
|
||||
var email = $('#user-email-input').val().trim(),
|
||||
url, msg;
|
||||
|
||||
if(!email) {
|
||||
msg = new PromptView.Error({
|
||||
title: gettext('A valid email address is required'),
|
||||
message: gettext('You must enter a valid email address in order to add a new team member'),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Return and add email address'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
$('#user-email-input').focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
msg.show();
|
||||
}
|
||||
|
||||
if(_.contains(staffEmails, email)) {
|
||||
msg = new PromptView.Warning({
|
||||
title: gettext('Already a course team member'),
|
||||
message: _.template(
|
||||
gettext("{email} is already on the “{course}” team. If you're trying to add a new member, please double-check the email address you provided."), {
|
||||
email: email,
|
||||
course: course.escape('name')
|
||||
}, {interpolate: /\{(.+?)\}/g}
|
||||
),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Return to team listing'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
$('#user-email-input').focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
msg.show();
|
||||
}
|
||||
|
||||
url = tplUserURL.replace('@@EMAIL@@', $('#user-email-input').val().trim());
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
notifyOnError: false,
|
||||
data: JSON.stringify({role: 'staff'}),
|
||||
success: function(data) {location.reload();},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var message, prompt;
|
||||
try {
|
||||
message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
|
||||
} catch (e) {
|
||||
message = unknownErrorMessage;
|
||||
}
|
||||
prompt = new PromptView.Error({
|
||||
title: gettext('Error adding user'),
|
||||
message: message,
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('OK'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
$('#user-email-input').focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
prompt.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$cancelButton = $createUserForm.find('.action-cancel');
|
||||
$cancelButton.bind('click', function(event) {
|
||||
event.preventDefault();
|
||||
$('.create-user-button').toggleClass('is-disabled');
|
||||
$createUserFormWrapper.toggleClass('is-shown');
|
||||
$('#user-email-input').val('');
|
||||
});
|
||||
|
||||
$('.create-user-button').bind('click', function(event) {
|
||||
event.preventDefault();
|
||||
$('.create-user-button').toggleClass('is-disabled');
|
||||
$createUserFormWrapper.toggleClass('is-shown');
|
||||
$createUserForm.find('#user-email-input').focus();
|
||||
});
|
||||
|
||||
$('body').bind('keyup', function(event) {
|
||||
if(event.which == 27) {
|
||||
$cancelButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
$('.remove-user').click(function() {
|
||||
var email = $(this).data('id'),
|
||||
msg = new PromptView.Warning({
|
||||
title: gettext('Are you sure?'),
|
||||
message: _.template(gettext('Are you sure you want to delete {email} from the course team for “{course}”?'), {email: email, course: course.get('name')}, {interpolate: /\{(.+?)\}/g}),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Delete'),
|
||||
click: function(view) {
|
||||
var url = tplUserURL.replace('@@EMAIL@@', email);
|
||||
view.hide();
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
notifyOnError: false,
|
||||
success: function(data) {location.reload();},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var message;
|
||||
try {
|
||||
message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
|
||||
} catch (e) {
|
||||
message = unknownErrorMessage;
|
||||
}
|
||||
var prompt = new PromptView.Error({
|
||||
title: gettext('Error removing user'),
|
||||
message: message,
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('OK'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
prompt.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Cancel'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
msg.show();
|
||||
});
|
||||
|
||||
$('.toggle-admin-role').click(function(event) {
|
||||
event.preventDefault();
|
||||
var type, url, role;
|
||||
if($(this).hasClass('add-admin-role')) {
|
||||
role = 'instructor';
|
||||
} else {
|
||||
role = 'staff';
|
||||
}
|
||||
|
||||
url = $(this).closest('li[data-url]').data('url');
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
notifyOnError: false,
|
||||
data: JSON.stringify({role: role}),
|
||||
success: function(data) {location.reload();},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var message, prompt;
|
||||
try {
|
||||
message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
|
||||
} catch (e) {
|
||||
message = unknownErrorMessage;
|
||||
}
|
||||
prompt = new PromptView.Error({
|
||||
title: gettext("There was an error changing the user's role"),
|
||||
message: message,
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Try Again'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
prompt.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
14
cms/static/js/factories/outline.js
Normal file
14
cms/static/js/factories/outline.js
Normal file
@@ -0,0 +1,14 @@
|
||||
define([
|
||||
'js/views/pages/course_outline', 'js/models/xblock_outline_info'
|
||||
], function(CourseOutlinePage, XBlockOutlineInfo) {
|
||||
'use strict';
|
||||
return function (XBlockOutlineInfoJson, initialStateJson) {
|
||||
var courseXBlock = new XBlockOutlineInfo(XBlockOutlineInfoJson, {parse: true}),
|
||||
view = new CourseOutlinePage({
|
||||
el: $('#content'),
|
||||
model: courseXBlock,
|
||||
initialState: initialStateJson
|
||||
});
|
||||
view.render();
|
||||
};
|
||||
});
|
||||
33
cms/static/js/factories/register.js
Normal file
33
cms/static/js/factories/register.js
Normal file
@@ -0,0 +1,33 @@
|
||||
define(['jquery', 'jquery.cookie'], function($) {
|
||||
'use strict';
|
||||
return function () {
|
||||
$('form :input')
|
||||
.focus(function() {
|
||||
$('label[for="' + this.id + '"]').addClass('is-focused');
|
||||
})
|
||||
.blur(function() {
|
||||
$('label').removeClass('is-focused');
|
||||
});
|
||||
|
||||
$('form#register_form').submit(function(event) {
|
||||
event.preventDefault();
|
||||
var submit_data = $('#register_form').serialize();
|
||||
|
||||
$.ajax({
|
||||
url: '/create_account',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRFToken': $.cookie('csrftoken')},
|
||||
notifyOnError: false,
|
||||
data: submit_data,
|
||||
success: function(json) {
|
||||
location.href = '/course/';
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var json = $.parseJSON(jqXHR.responseText);
|
||||
$('#register_error').html(json.value).stop().addClass('is-shown');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
29
cms/static/js/factories/settings.js
Normal file
29
cms/static/js/factories/settings.js
Normal file
@@ -0,0 +1,29 @@
|
||||
define([
|
||||
'jquery', 'js/models/settings/course_details', 'js/views/settings/main'
|
||||
], function($, CourseDetailsModel, MainView) {
|
||||
'use strict';
|
||||
return function (detailsUrl) {
|
||||
var model;
|
||||
// highlighting labels when fields are focused in
|
||||
$('form :input')
|
||||
.focus(function() {
|
||||
$('label[for="' + this.id + '"]').addClass('is-focused');
|
||||
})
|
||||
.blur(function() {
|
||||
$('label').removeClass('is-focused');
|
||||
});
|
||||
|
||||
model = new CourseDetailsModel();
|
||||
model.urlRoot = detailsUrl;
|
||||
model.fetch({
|
||||
success: function(model) {
|
||||
var editor = new MainView({
|
||||
el: $('.settings-details'),
|
||||
model: model
|
||||
});
|
||||
editor.render();
|
||||
},
|
||||
reset: true
|
||||
});
|
||||
};
|
||||
});
|
||||
44
cms/static/js/factories/settings_advanced.js
Normal file
44
cms/static/js/factories/settings_advanced.js
Normal file
@@ -0,0 +1,44 @@
|
||||
define([
|
||||
'jquery', 'gettext', 'js/models/settings/advanced', 'js/views/settings/advanced'
|
||||
], function($, gettext, AdvancedSettingsModel, AdvancedSettingsView) {
|
||||
'use strict';
|
||||
return function (advancedDict, advancedSettingsUrl) {
|
||||
var advancedModel, editor;
|
||||
|
||||
$('form :input')
|
||||
.focus(function() {
|
||||
$('label[for="' + this.id + '"]').addClass('is-focused');
|
||||
})
|
||||
.blur(function() {
|
||||
$('label').removeClass('is-focused');
|
||||
});
|
||||
|
||||
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
|
||||
advancedModel = new AdvancedSettingsModel(advancedDict, {parse: true});
|
||||
advancedModel.url = advancedSettingsUrl;
|
||||
|
||||
editor = new AdvancedSettingsView({
|
||||
el: $('.settings-advanced'),
|
||||
model: advancedModel
|
||||
});
|
||||
editor.render();
|
||||
|
||||
$('#deprecated-settings').click(function() {
|
||||
var wrapperDeprecatedSetting = $('.wrapper-deprecated-setting'),
|
||||
deprecatedSettingsLabel = $('.deprecated-settings-label');
|
||||
|
||||
if ($(this).is(':checked')) {
|
||||
wrapperDeprecatedSetting.addClass('is-set');
|
||||
deprecatedSettingsLabel.text(gettext('Hide Deprecated Settings'));
|
||||
editor.render_deprecated = true;
|
||||
}
|
||||
else {
|
||||
wrapperDeprecatedSetting.removeClass('is-set');
|
||||
deprecatedSettingsLabel.text(gettext('Show Deprecated Settings'));
|
||||
editor.render_deprecated = false;
|
||||
}
|
||||
|
||||
editor.render();
|
||||
});
|
||||
};
|
||||
});
|
||||
24
cms/static/js/factories/settings_graders.js
Normal file
24
cms/static/js/factories/settings_graders.js
Normal file
@@ -0,0 +1,24 @@
|
||||
define([
|
||||
'jquery', 'js/views/settings/grading', 'js/models/settings/course_grading_policy'
|
||||
], function($, GradingView, CourseGradingPolicyModel) {
|
||||
'use strict';
|
||||
return function (courseDetails, gradingUrl) {
|
||||
var model, editor;
|
||||
|
||||
$('form :input')
|
||||
.focus(function() {
|
||||
$('label[for="' + this.id + '"]').addClass('is-focused');
|
||||
})
|
||||
.blur(function() {
|
||||
$('label').removeClass('is-focused');
|
||||
});
|
||||
|
||||
model = new CourseGradingPolicyModel(courseDetails,{parse:true});
|
||||
model.urlRoot = gradingUrl;
|
||||
editor = new GradingView({
|
||||
el: $('.settings-grading'),
|
||||
model : model
|
||||
});
|
||||
editor.render();
|
||||
};
|
||||
});
|
||||
20
cms/static/js/factories/textbooks.js
Normal file
20
cms/static/js/factories/textbooks.js
Normal file
@@ -0,0 +1,20 @@
|
||||
define([
|
||||
'gettext', 'js/models/section', 'js/collections/textbook', 'js/views/list_textbooks'
|
||||
], function(gettext, Section, TextbookCollection, ListTextbooksView) {
|
||||
'use strict';
|
||||
return function (textbooksJson) {
|
||||
var textbooks = new TextbookCollection(textbooksJson, {parse: true}),
|
||||
tbView = new ListTextbooksView({collection: textbooks});
|
||||
|
||||
$('.content-primary').append(tbView.render().el);
|
||||
$('.nav-actions .new-button').click(function(event) {
|
||||
tbView.addOne(event);
|
||||
});
|
||||
$(window).on('beforeunload', function() {
|
||||
var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); });
|
||||
if(dirty) {
|
||||
return gettext('You have unsaved changes. Do you really want to leave this page?');
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -68,6 +68,7 @@ src_paths:
|
||||
- coffee/src
|
||||
- js
|
||||
- js/common_helpers
|
||||
- js/factories
|
||||
|
||||
# Paths to spec (test) JavaScript files
|
||||
spec_paths:
|
||||
|
||||
266
cms/static/require-config.js
Normal file
266
cms/static/require-config.js
Normal file
@@ -0,0 +1,266 @@
|
||||
require.config({
|
||||
// NOTE: baseUrl has been previously set in cms/static/templates/base.html
|
||||
waitSeconds: 60,
|
||||
paths: {
|
||||
"domReady": "js/vendor/domReady",
|
||||
"gettext": "/i18n",
|
||||
"mustache": "js/vendor/mustache",
|
||||
"codemirror": "js/vendor/codemirror-compressed",
|
||||
"codemirror/stex": "js/vendor/CodeMirror/stex",
|
||||
"jquery": "js/vendor/jquery.min",
|
||||
"jquery.ui": "js/vendor/jquery-ui.min",
|
||||
"jquery.form": "js/vendor/jquery.form",
|
||||
"jquery.markitup": "js/vendor/markitup/jquery.markitup",
|
||||
"jquery.leanModal": "js/vendor/jquery.leanModal.min",
|
||||
"jquery.ajaxQueue": "js/vendor/jquery.ajaxQueue",
|
||||
"jquery.smoothScroll": "js/vendor/jquery.smooth-scroll.min",
|
||||
"jquery.timepicker": "js/vendor/timepicker/jquery.timepicker",
|
||||
"jquery.cookie": "js/vendor/jquery.cookie",
|
||||
"jquery.qtip": "js/vendor/jquery.qtip.min",
|
||||
"jquery.scrollTo": "js/vendor/jquery.scrollTo-1.4.2-min",
|
||||
"jquery.flot": "js/vendor/flot/jquery.flot.min",
|
||||
"jquery.fileupload": "js/vendor/jQuery-File-Upload/js/jquery.fileupload",
|
||||
"jquery.iframe-transport": "js/vendor/jQuery-File-Upload/js/jquery.iframe-transport",
|
||||
"jquery.inputnumber": "js/vendor/html5-input-polyfills/number-polyfill",
|
||||
"jquery.immediateDescendents": "coffee/src/jquery.immediateDescendents",
|
||||
"datepair": "js/vendor/timepicker/datepair",
|
||||
"date": "js/vendor/date",
|
||||
"underscore": "js/vendor/underscore-min",
|
||||
"underscore.string": "js/vendor/underscore.string.min",
|
||||
"backbone": "js/vendor/backbone-min",
|
||||
"backbone.associations": "js/vendor/backbone-associations-min",
|
||||
"backbone.paginator": "js/vendor/backbone.paginator.min",
|
||||
"tinymce": "js/vendor/tinymce/js/tinymce/tinymce.full.min",
|
||||
"jquery.tinymce": "js/vendor/tinymce/js/tinymce/jquery.tinymce.min",
|
||||
"xmodule": "/xmodule/xmodule",
|
||||
"xblock": "coffee/src/xblock",
|
||||
"utility": "js/src/utility",
|
||||
"accessibility": "js/src/accessibility_tools",
|
||||
"draggabilly": "js/vendor/draggabilly.pkgd",
|
||||
"URI": "js/vendor/URI.min",
|
||||
"ieshim": "js/src/ie_shim",
|
||||
"tooltip_manager": "js/src/tooltip_manager",
|
||||
|
||||
// Files needed for Annotations feature
|
||||
"annotator": "js/vendor/ova/annotator-full",
|
||||
"annotator-harvardx": "js/vendor/ova/annotator-full-firebase-auth",
|
||||
"video.dev": "js/vendor/ova/video.dev",
|
||||
"vjs.youtube": 'js/vendor/ova/vjs.youtube',
|
||||
"rangeslider": 'js/vendor/ova/rangeslider',
|
||||
"share-annotator": 'js/vendor/ova/share-annotator',
|
||||
"richText-annotator": 'js/vendor/ova/richText-annotator',
|
||||
"reply-annotator": 'js/vendor/ova/reply-annotator',
|
||||
"grouping-annotator": 'js/vendor/ova/grouping-annotator',
|
||||
"tags-annotator": 'js/vendor/ova/tags-annotator',
|
||||
"diacritic-annotator": 'js/vendor/ova/diacritic-annotator',
|
||||
"flagging-annotator": 'js/vendor/ova/flagging-annotator',
|
||||
"jquery-Watch": 'js/vendor/ova/jquery-Watch',
|
||||
"openseadragon": 'js/vendor/ova/openseadragon',
|
||||
"osda": 'js/vendor/ova/OpenSeaDragonAnnotation',
|
||||
"ova": 'js/vendor/ova/ova',
|
||||
"catch": 'js/vendor/ova/catch/js/catch',
|
||||
"handlebars": 'js/vendor/ova/catch/js/handlebars-1.1.2',
|
||||
// end of Annotation tool files
|
||||
|
||||
// externally hosted files
|
||||
"tender": [
|
||||
"//edxedge.tenderapp.com/tender_widget",
|
||||
// if tender fails to load, fallback on a local file
|
||||
// so that require doesn't fall over
|
||||
"js/src/tender_fallback"
|
||||
],
|
||||
"mathjax": "//edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full&delayStartupUntil=configured",
|
||||
"youtube": [
|
||||
// youtube URL does not end in ".js". We add "?noext" to the path so
|
||||
// that require.js adds the ".js" to the query component of the URL,
|
||||
// and leaves the path component intact.
|
||||
"//www.youtube.com/player_api?noext",
|
||||
// if youtube fails to load, fallback on a local file
|
||||
// so that require doesn't fall over
|
||||
"js/src/youtube_fallback"
|
||||
]
|
||||
},
|
||||
shim: {
|
||||
"gettext": {
|
||||
exports: "gettext"
|
||||
},
|
||||
"date": {
|
||||
exports: "Date"
|
||||
},
|
||||
"jquery.ui": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.ui"
|
||||
},
|
||||
"jquery.form": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.ajaxForm"
|
||||
},
|
||||
"jquery.markitup": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.markitup"
|
||||
},
|
||||
"jquery.leanmodal": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.leanModal"
|
||||
},
|
||||
"jquery.ajaxQueue": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.ajaxQueue"
|
||||
},
|
||||
"jquery.smoothScroll": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.smoothScroll"
|
||||
},
|
||||
"jquery.cookie": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.cookie"
|
||||
},
|
||||
"jquery.qtip": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.qtip"
|
||||
},
|
||||
"jquery.scrollTo": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.scrollTo",
|
||||
},
|
||||
"jquery.flot": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.plot"
|
||||
},
|
||||
"jquery.fileupload": {
|
||||
deps: ["jquery.iframe-transport"],
|
||||
exports: "jQuery.fn.fileupload"
|
||||
},
|
||||
"jquery.inputnumber": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.inputNumber"
|
||||
},
|
||||
"jquery.tinymce": {
|
||||
deps: ["jquery", "tinymce"],
|
||||
exports: "jQuery.fn.tinymce"
|
||||
},
|
||||
"datepair": {
|
||||
deps: ["jquery.ui", "jquery.timepicker"]
|
||||
},
|
||||
"underscore": {
|
||||
exports: "_"
|
||||
},
|
||||
"backbone": {
|
||||
deps: ["underscore", "jquery"],
|
||||
exports: "Backbone"
|
||||
},
|
||||
"backbone.associations": {
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Associations"
|
||||
},
|
||||
"backbone.paginator": {
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Paginator"
|
||||
},
|
||||
"tender": {
|
||||
exports: 'Tender'
|
||||
},
|
||||
"youtube": {
|
||||
exports: "YT"
|
||||
},
|
||||
"codemirror": {
|
||||
exports: "CodeMirror"
|
||||
},
|
||||
"codemirror/stex": {
|
||||
deps: ["codemirror"]
|
||||
},
|
||||
"tinymce": {
|
||||
exports: "tinymce"
|
||||
},
|
||||
"mathjax": {
|
||||
exports: "MathJax",
|
||||
init: function() {
|
||||
MathJax.Hub.Config({
|
||||
tex2jax: {
|
||||
inlineMath: [
|
||||
["\\(","\\)"],
|
||||
['[mathjaxinline]','[/mathjaxinline]']
|
||||
],
|
||||
displayMath: [
|
||||
["\\[","\\]"],
|
||||
['[mathjax]','[/mathjax]']
|
||||
]
|
||||
}
|
||||
});
|
||||
MathJax.Hub.Configured();
|
||||
}
|
||||
},
|
||||
"URI": {
|
||||
exports: "URI"
|
||||
},
|
||||
"tooltip_manager": {
|
||||
deps: ["jquery", "underscore"]
|
||||
},
|
||||
"jquery.immediateDescendents": {
|
||||
deps: ["jquery"]
|
||||
},
|
||||
"xblock/core": {
|
||||
exports: "XBlock",
|
||||
deps: ["jquery", "jquery.immediateDescendents"]
|
||||
},
|
||||
"xblock/runtime.v1": {
|
||||
exports: "XBlock",
|
||||
deps: ["xblock/core"]
|
||||
},
|
||||
|
||||
"coffee/src/main": {
|
||||
deps: ["coffee/src/ajax_prefix"]
|
||||
},
|
||||
"coffee/src/logger": {
|
||||
exports: "Logger",
|
||||
deps: ["coffee/src/ajax_prefix"]
|
||||
},
|
||||
|
||||
// the following are all needed for annotation tools
|
||||
"video.dev": {
|
||||
exports:"videojs"
|
||||
},
|
||||
"vjs.youtube": {
|
||||
deps: ["video.dev"]
|
||||
},
|
||||
"rangeslider": {
|
||||
deps: ["video.dev"]
|
||||
},
|
||||
"annotator": {
|
||||
exports: "Annotator"
|
||||
},
|
||||
"annotator-harvardx":{
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"share-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"richText-annotator": {
|
||||
deps: ["annotator", "tinymce"]
|
||||
},
|
||||
"reply-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"tags-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"diacritic-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"flagging-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"grouping-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"ova":{
|
||||
exports: "ova",
|
||||
deps: ["annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator", "richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator", "grouping-annotator", "diacritic-annotator", "jquery-Watch", "catch", "handlebars", "URI"]
|
||||
},
|
||||
"osda":{
|
||||
exports: "osda",
|
||||
deps: ["annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator", "richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator", "grouping-annotator", "diacritic-annotator", "openseadragon", "jquery-Watch", "catch", "handlebars", "URI"]
|
||||
},
|
||||
// end of annotation tool files
|
||||
}
|
||||
});
|
||||
@@ -201,7 +201,7 @@
|
||||
|
||||
> .label {
|
||||
display: inline-block;
|
||||
max-width: 85%;
|
||||
max-width: 84%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -17,18 +17,10 @@
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["jquery", "js/collections/asset", "js/views/assets", "jquery.fileupload"],
|
||||
function($, AssetCollection, AssetsView) {
|
||||
|
||||
var assets = new AssetCollection();
|
||||
assets.url = "${asset_callback_url}";
|
||||
var assetsView = new AssetsView({collection: assets, el: $('.assets-wrapper')});
|
||||
assetsView.render();
|
||||
|
||||
}); // end of require()
|
||||
</script>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/asset_index"], function (AssetIndexFactory) {
|
||||
AssetIndexFactory("${asset_callback_url}");
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
@@ -72,9 +64,9 @@
|
||||
</div>
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Using File URLs")}</h3>
|
||||
|
||||
|
||||
<p>${_("Use the {em_start}Embed URL{em_end} value to link to the file or image from a component, a course update, or a course handout.").format(em_start='<strong>', em_end="</strong>")}</p>
|
||||
|
||||
|
||||
<p>${_("Use the {em_start}External URL{em_end} value to reference the file or image only from outside of your course.").format(em_start='<strong>', em_end="</strong>")}</p>
|
||||
<p>${_("Click in the Embed URL or External URL column to select the value, then copy it.")}</p>
|
||||
</div>
|
||||
|
||||
@@ -46,305 +46,17 @@
|
||||
<a class="nav-skip" href="#content">${_("Skip to this view's content")}</a>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.baseUrl = "${settings.STATIC_URL}";
|
||||
var require = {
|
||||
baseUrl: baseUrl,
|
||||
waitSeconds: 60,
|
||||
paths: {
|
||||
"domReady": "js/vendor/domReady",
|
||||
"gettext": "/i18n",
|
||||
"mustache": "js/vendor/mustache",
|
||||
"codemirror": "js/vendor/codemirror-compressed",
|
||||
"codemirror/stex": "js/vendor/CodeMirror/stex",
|
||||
"jquery": "js/vendor/jquery.min",
|
||||
"jquery.ui": "js/vendor/jquery-ui.min",
|
||||
"jquery.form": "js/vendor/jquery.form",
|
||||
"jquery.markitup": "js/vendor/markitup/jquery.markitup",
|
||||
"jquery.leanModal": "js/vendor/jquery.leanModal.min",
|
||||
"jquery.ajaxQueue": "js/vendor/jquery.ajaxQueue",
|
||||
"jquery.smoothScroll": "js/vendor/jquery.smooth-scroll.min",
|
||||
"jquery.timepicker": "js/vendor/timepicker/jquery.timepicker",
|
||||
"jquery.cookie": "js/vendor/jquery.cookie",
|
||||
"jquery.qtip": "js/vendor/jquery.qtip.min",
|
||||
"jquery.scrollTo": "js/vendor/jquery.scrollTo-1.4.2-min",
|
||||
"jquery.flot": "js/vendor/flot/jquery.flot.min",
|
||||
"jquery.fileupload": "js/vendor/jQuery-File-Upload/js/jquery.fileupload",
|
||||
"jquery.iframe-transport": "js/vendor/jQuery-File-Upload/js/jquery.iframe-transport",
|
||||
"jquery.inputnumber": "js/vendor/html5-input-polyfills/number-polyfill",
|
||||
"jquery.immediateDescendents": "coffee/src/jquery.immediateDescendents",
|
||||
"datepair": "js/vendor/timepicker/datepair",
|
||||
"date": "js/vendor/date",
|
||||
"underscore": "js/vendor/underscore-min",
|
||||
"underscore.string": "js/vendor/underscore.string.min",
|
||||
"backbone": "js/vendor/backbone-min",
|
||||
"backbone.associations": "js/vendor/backbone-associations-min",
|
||||
"backbone.paginator": "js/vendor/backbone.paginator.min",
|
||||
"tinymce": "js/vendor/tinymce/js/tinymce/tinymce.full.min",
|
||||
"jquery.tinymce": "js/vendor/tinymce/js/tinymce/jquery.tinymce.min",
|
||||
"xmodule": "/xmodule/xmodule",
|
||||
"xblock": "coffee/src/xblock",
|
||||
"utility": "js/src/utility",
|
||||
"accessibility": "js/src/accessibility_tools",
|
||||
"draggabilly": "js/vendor/draggabilly.pkgd",
|
||||
"URI": "js/vendor/URI.min",
|
||||
"ieshim": "js/src/ie_shim",
|
||||
"tooltip_manager": "js/src/tooltip_manager",
|
||||
|
||||
// Files needed for Annotations feature
|
||||
"annotator": "js/vendor/ova/annotator-full",
|
||||
"annotator-harvardx": "js/vendor/ova/annotator-full-firebase-auth",
|
||||
"video.dev": "js/vendor/ova/video.dev",
|
||||
"vjs.youtube": 'js/vendor/ova/vjs.youtube',
|
||||
"rangeslider": 'js/vendor/ova/rangeslider',
|
||||
"share-annotator": 'js/vendor/ova/share-annotator',
|
||||
"richText-annotator": 'js/vendor/ova/richText-annotator',
|
||||
"reply-annotator": 'js/vendor/ova/reply-annotator',
|
||||
"grouping-annotator": 'js/vendor/ova/grouping-annotator',
|
||||
"tags-annotator": 'js/vendor/ova/tags-annotator',
|
||||
"diacritic-annotator": 'js/vendor/ova/diacritic-annotator',
|
||||
"flagging-annotator": 'js/vendor/ova/flagging-annotator',
|
||||
"jquery-Watch": 'js/vendor/ova/jquery-Watch',
|
||||
"openseadragon": 'js/vendor/ova/openseadragon',
|
||||
"osda": 'js/vendor/ova/OpenSeaDragonAnnotation',
|
||||
"ova": 'js/vendor/ova/ova',
|
||||
"catch": 'js/vendor/ova/catch/js/catch',
|
||||
"handlebars": 'js/vendor/ova/catch/js/handlebars-1.1.2',
|
||||
// end of Annotation tool files
|
||||
|
||||
// externally hosted files
|
||||
"tender": [
|
||||
"//edxedge.tenderapp.com/tender_widget",
|
||||
// if tender fails to load, fallback on a local file
|
||||
// so that require doesn't fall over
|
||||
"js/src/tender_fallback"
|
||||
],
|
||||
"mathjax": "//edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full&delayStartupUntil=configured",
|
||||
"youtube": [
|
||||
// youtube URL does not end in ".js". We add "?noext" to the path so
|
||||
// that require.js adds the ".js" to the query component of the URL,
|
||||
// and leaves the path component intact.
|
||||
"//www.youtube.com/player_api?noext",
|
||||
// if youtube fails to load, fallback on a local file
|
||||
// so that require doesn't fall over
|
||||
"js/src/youtube_fallback"
|
||||
]
|
||||
},
|
||||
shim: {
|
||||
"gettext": {
|
||||
exports: "gettext"
|
||||
},
|
||||
"date": {
|
||||
exports: "Date"
|
||||
},
|
||||
"jquery.ui": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.ui"
|
||||
},
|
||||
"jquery.form": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.ajaxForm"
|
||||
},
|
||||
"jquery.markitup": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.markitup"
|
||||
},
|
||||
"jquery.leanmodal": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.leanModal"
|
||||
},
|
||||
"jquery.ajaxQueue": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.ajaxQueue"
|
||||
},
|
||||
"jquery.smoothScroll": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.smoothScroll"
|
||||
},
|
||||
"jquery.cookie": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.cookie"
|
||||
},
|
||||
"jquery.qtip": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.qtip"
|
||||
},
|
||||
"jquery.scrollTo": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.scrollTo",
|
||||
},
|
||||
"jquery.flot": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.plot"
|
||||
},
|
||||
"jquery.fileupload": {
|
||||
deps: ["jquery.iframe-transport"],
|
||||
exports: "jQuery.fn.fileupload"
|
||||
},
|
||||
"jquery.inputnumber": {
|
||||
deps: ["jquery"],
|
||||
exports: "jQuery.fn.inputNumber"
|
||||
},
|
||||
"jquery.tinymce": {
|
||||
deps: ["jquery", "tinymce"],
|
||||
exports: "jQuery.fn.tinymce"
|
||||
},
|
||||
"datepair": {
|
||||
deps: ["jquery.ui", "jquery.timepicker"]
|
||||
},
|
||||
"underscore": {
|
||||
exports: "_"
|
||||
},
|
||||
"backbone": {
|
||||
deps: ["underscore", "jquery"],
|
||||
exports: "Backbone"
|
||||
},
|
||||
"backbone.associations": {
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Associations"
|
||||
},
|
||||
"backbone.paginator": {
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Paginator"
|
||||
},
|
||||
"tender": {
|
||||
exports: 'Tender'
|
||||
},
|
||||
"youtube": {
|
||||
exports: "YT"
|
||||
},
|
||||
"codemirror": {
|
||||
exports: "CodeMirror"
|
||||
},
|
||||
"codemirror/stex": {
|
||||
deps: ["codemirror"]
|
||||
},
|
||||
"tinymce": {
|
||||
exports: "tinymce"
|
||||
},
|
||||
"mathjax": {
|
||||
exports: "MathJax",
|
||||
init: function() {
|
||||
MathJax.Hub.Config({
|
||||
tex2jax: {
|
||||
inlineMath: [
|
||||
["\\(","\\)"],
|
||||
['[mathjaxinline]','[/mathjaxinline]']
|
||||
],
|
||||
displayMath: [
|
||||
["\\[","\\]"],
|
||||
['[mathjax]','[/mathjax]']
|
||||
]
|
||||
}
|
||||
});
|
||||
MathJax.Hub.Configured();
|
||||
}
|
||||
},
|
||||
"URI": {
|
||||
exports: "URI"
|
||||
},
|
||||
"tooltip_manager": {
|
||||
deps: ["jquery", "underscore"]
|
||||
},
|
||||
"jquery.immediateDescendents": {
|
||||
deps: ["jquery"]
|
||||
},
|
||||
"xblock/core": {
|
||||
exports: "XBlock",
|
||||
deps: ["jquery", "jquery.immediateDescendents"]
|
||||
},
|
||||
"xblock/runtime.v1": {
|
||||
exports: "XBlock",
|
||||
deps: ["xblock/core"]
|
||||
},
|
||||
|
||||
"coffee/src/main": {
|
||||
deps: ["coffee/src/ajax_prefix"]
|
||||
},
|
||||
"coffee/src/logger": {
|
||||
exports: "Logger",
|
||||
deps: ["coffee/src/ajax_prefix"]
|
||||
},
|
||||
|
||||
// the following are all needed for annotation tools
|
||||
"video.dev": {
|
||||
exports:"videojs"
|
||||
},
|
||||
"vjs.youtube": {
|
||||
deps: ["video.dev"]
|
||||
},
|
||||
"rangeslider": {
|
||||
deps: ["video.dev"]
|
||||
},
|
||||
"annotator": {
|
||||
exports: "Annotator"
|
||||
},
|
||||
"annotator-harvardx":{
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"share-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"richText-annotator": {
|
||||
deps: ["annotator", "tinymce"]
|
||||
},
|
||||
"reply-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"tags-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"diacritic-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"flagging-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"grouping-annotator": {
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"ova":{
|
||||
exports: "ova",
|
||||
deps: ["annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator", "richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator", "grouping-annotator", "diacritic-annotator", "jquery-Watch", "catch", "handlebars", "URI"]
|
||||
},
|
||||
"osda":{
|
||||
exports: "osda",
|
||||
deps: ["annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator", "richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator", "grouping-annotator", "diacritic-annotator", "openseadragon", "jquery-Watch", "catch", "handlebars", "URI"]
|
||||
},
|
||||
// end of annotation tool files
|
||||
},
|
||||
// load jquery and gettext automatically
|
||||
deps: ["jquery", "gettext"],
|
||||
callback: function() {
|
||||
// load other scripts on every page, after jquery loads
|
||||
require(["js/base", "coffee/src/main", "coffee/src/logger", "datepair", "accessibility", "ieshim", "tooltip_manager"]);
|
||||
// we need "datepair" because it dynamically modifies the page
|
||||
// when it is loaded -- yuck!
|
||||
}
|
||||
};
|
||||
window.baseUrl = "${settings.STATIC_URL}";
|
||||
var require = {baseUrl: window.baseUrl};
|
||||
</script>
|
||||
<script type="text/javascript" src="${static.url("js/vendor/require.js")}"></script>
|
||||
<script type="text/javascript" src="${static.url("require-config.js")}"></script>
|
||||
|
||||
## js templates
|
||||
<script id="system-feedback-tpl" type="text/template">
|
||||
<%static:include path="js/system-feedback.underscore" />
|
||||
</script>
|
||||
|
||||
|
||||
% if context_course:
|
||||
<script type="text/javascript">
|
||||
require(['js/models/course'], function(Course) {
|
||||
window.course = new Course({
|
||||
id: "${context_course.id | escapejs}",
|
||||
name: "${context_course.display_name_with_default | h}",
|
||||
url_name: "${context_course.location.name | h}",
|
||||
org: "${context_course.location.org | h}",
|
||||
num: "${context_course.location.course | h}",
|
||||
revision: "${context_course.location.revision | h}"
|
||||
});
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
|
||||
<!-- view -->
|
||||
<div class="wrapper wrapper-view" dir="${dir_rtl}">
|
||||
<% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %>
|
||||
@@ -359,12 +71,8 @@
|
||||
</div>
|
||||
|
||||
% if user.is_authenticated():
|
||||
<script type="text/javascript">
|
||||
require(['js/sock']);
|
||||
</script>
|
||||
<%include file="widgets/sock.html" args="online_help_token=online_help_token" />
|
||||
<%include file="widgets/sock.html" args="online_help_token=online_help_token" />
|
||||
% endif
|
||||
|
||||
<%include file="widgets/footer.html" />
|
||||
<%include file="widgets/tender.html" />
|
||||
|
||||
@@ -373,6 +81,28 @@
|
||||
|
||||
<div id="page-prompt"></div>
|
||||
<%block name="jsextra"></%block>
|
||||
<script type="text/javascript">
|
||||
require(['js/factories/common_deps'], function () {
|
||||
require(['js/factories/base'], function () {
|
||||
% if context_course:
|
||||
require(['js/factories/course'], function(CourseFactory) {
|
||||
CourseFactory({
|
||||
id: "${context_course.id | escapejs}",
|
||||
name: "${context_course.display_name_with_default | h}",
|
||||
url_name: "${context_course.location.name | h}",
|
||||
org: "${context_course.location.org | h}",
|
||||
num: "${context_course.location.course | h}",
|
||||
revision: "${context_course.location.revision | h}"
|
||||
});
|
||||
});
|
||||
% endif
|
||||
% if user.is_authenticated():
|
||||
require(['js/sock']);
|
||||
% endif
|
||||
<%block name='requirejs'></%block>
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<div class="modal-cover"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,20 +17,10 @@
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["domReady!", "jquery", "js/collections/checklist", "js/views/checklist"],
|
||||
function(doc, $, ChecklistCollection, ChecklistView) {
|
||||
var checklistCollection = new ChecklistCollection();
|
||||
checklistCollection.url = "${handler_url}";
|
||||
|
||||
var editor = new ChecklistView({
|
||||
el: $('.course-checklists'),
|
||||
collection: checklistCollection
|
||||
});
|
||||
checklistCollection.fetch({reset: true});
|
||||
});
|
||||
</script>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/checklists"], function (ChecklistsFactory) {
|
||||
ChecklistsFactory("${handler_url}");
|
||||
});
|
||||
</%block>
|
||||
|
||||
|
||||
|
||||
@@ -34,28 +34,13 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal",
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type='text/javascript'>
|
||||
require(["domReady!", "jquery", "js/models/xblock_info", "js/views/pages/container",
|
||||
"js/collections/component_template", "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function(doc, $, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
|
||||
var templates = new ComponentTemplates(${component_templates | n}, {parse: true});
|
||||
var mainXBlockInfo = new XBlockInfo(${json.dumps(xblock_info) | n}, {parse: true});
|
||||
var isUnitPage = ${json.dumps(is_unit_page)}
|
||||
|
||||
xmoduleLoader.done(function () {
|
||||
var view = new ContainerPage({
|
||||
el: $('#content'),
|
||||
model: mainXBlockInfo,
|
||||
action: "${action}",
|
||||
templates: templates,
|
||||
isUnitPage: isUnitPage
|
||||
});
|
||||
view.render();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/container"], function(ContainerFactory) {
|
||||
ContainerFactory(
|
||||
${component_templates | n}, ${json.dumps(xblock_info) | n},
|
||||
"${action}", ${json.dumps(is_unit_page)}
|
||||
);
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -8,15 +8,14 @@
|
||||
<%block name="bodyclass">is-signedin view-course-create view-course-create-rerun</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["domReady!", "jquery", "jquery.form", "js/views/course_rerun"], function(doc, $) {
|
||||
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
var source_course_key = "${source_course_key | escapejs}"
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var source_course_key = "${source_course_key | escapejs}";
|
||||
</script>
|
||||
</%block>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/course_create_rerun"], function (CourseCreateRerunFactory) {
|
||||
CourseCreateRerunFactory();
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -19,30 +19,10 @@
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
|
||||
require(["domReady!", "jquery", "js/collections/course_update", "js/models/module_info", "js/models/course_info", "js/views/course_info_edit"],
|
||||
function(doc, $, CourseUpdateCollection, ModuleInfoModel, CourseInfoModel, CourseInfoEditView) {
|
||||
var course_updates = new CourseUpdateCollection();
|
||||
course_updates.url = '${updates_url}';
|
||||
course_updates.fetch({reset: true});
|
||||
|
||||
var course_handouts = new ModuleInfoModel({
|
||||
id: '${handouts_locator | escapejs}'
|
||||
});
|
||||
|
||||
var editor = new CourseInfoEditView({
|
||||
el: $('.main-wrapper'),
|
||||
model : new CourseInfoModel({
|
||||
updates : course_updates,
|
||||
base_asset_url : '${base_asset_url}',
|
||||
handouts : course_handouts
|
||||
})
|
||||
});
|
||||
editor.render();
|
||||
});
|
||||
</script>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/course_info"], function(CourseInfoFactory) {
|
||||
CourseInfoFactory("${updates_url}", "${handouts_locator | escapejs}", "${base_asset_url}");
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -12,19 +12,10 @@ from contentstore.utils import reverse_usage_url
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["domReady!", "jquery", "js/views/pages/course_outline", "js/models/xblock_outline_info"],
|
||||
function(doc, $, CourseOutlinePage, XBlockOutlineInfo) {
|
||||
var courseXBlock = new XBlockOutlineInfo(${json.dumps(course_structure) | n}, { parse: true });
|
||||
var view = new CourseOutlinePage({
|
||||
el: $('#content'),
|
||||
model: courseXBlock,
|
||||
initialState: ${json.dumps(initial_state) | n}
|
||||
});
|
||||
view.render();
|
||||
});
|
||||
</script>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/outline"], function (OutlineFactory) {
|
||||
OutlineFactory(${json.dumps(course_structure) | n}, ${json.dumps(initial_state) | n});
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
|
||||
@@ -18,25 +18,10 @@
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type='text/javascript'>
|
||||
require(["js/models/explicit_url", "coffee/src/views/tabs",
|
||||
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function (TabsModel, TabsEditView, xmoduleLoader) {
|
||||
xmoduleLoader.done(function () {
|
||||
var model = new TabsModel({
|
||||
id: "${context_course.location | escapejs}",
|
||||
explicit_url: "${reverse('contentstore.views.tabs_handler', kwargs={'course_key_string': context_course.id})}"
|
||||
});
|
||||
|
||||
new TabsEditView({
|
||||
el: $('.tab-list'),
|
||||
model: model,
|
||||
mast: $('.wrapper-mast')
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/edit_tabs"], function (EditTabsFactory) {
|
||||
EditTabsFactory("${context_course.location | escapejs}", "${reverse('contentstore.views.tabs_handler', kwargs={'course_key_string': context_course.id})}");
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -9,71 +9,17 @@
|
||||
<%block name="title">${_("Course Export")}</%block>
|
||||
<%block name="bodyclass">is-signedin course tools view-export</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
% if in_err:
|
||||
<script type='text/javascript'>
|
||||
var hasUnit = ${json.dumps(bool(unit))},
|
||||
editUnitUrl = "${edit_unit_url or ""}",
|
||||
courseHomeUrl = "${course_home_url or ""}",
|
||||
errMsg = ${json.dumps(raw_err_msg or "")};
|
||||
<%block name="requirejs">
|
||||
% if in_err:
|
||||
var hasUnit = ${json.dumps(bool(unit))},
|
||||
editUnitUrl = "${edit_unit_url or ""}",
|
||||
courseHomeUrl = "${course_home_url or ""}",
|
||||
errMsg = ${json.dumps(raw_err_msg or "")};
|
||||
|
||||
require(["domReady!", "gettext", "js/views/feedback_prompt"], function(doc, gettext, PromptView) {
|
||||
var dialog;
|
||||
if(hasUnit) {
|
||||
dialog = new PromptView({
|
||||
title: gettext('There has been an error while exporting.'),
|
||||
message: gettext("There has been a failure to export to XML at least one component. It is recommended that you go to the edit page and repair the error before attempting another export. Please check that all components on the page are valid and do not display any error messages."),
|
||||
intent: "error",
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Correct failed component'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
document.location = editUnitUrl;
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Return to Export'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var msg = "<p>" + gettext("There has been a failure to export your course to XML. Unfortunately, we do not have specific enough information to assist you in identifying the failed component. It is recommended that you inspect your courseware to identify any components in error and try again.") + "</p><p>" + gettext("The raw error message is:") + "</p>" + errMsg;
|
||||
dialog = new PromptView({
|
||||
title: gettext('There has been an error with your export.'),
|
||||
message: msg,
|
||||
intent: "error",
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Yes, take me to the main course page'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
document.location = courseHomeUrl;
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Cancel'),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The CSS animation for the dialog relies on the 'js' class
|
||||
// being on the body. This happens after this JavaScript is executed,
|
||||
// causing a "bouncing" of the dialog after it is initially shown.
|
||||
// As a workaround, add this class first.
|
||||
$('body').addClass('js');
|
||||
dialog.show();
|
||||
|
||||
});
|
||||
</script>
|
||||
%endif
|
||||
require(["js/factories/export"], function(ExportFactory) {
|
||||
ExportFactory(hasUnit, editUnitUrl, courseHomeUrl, errMsg);
|
||||
});
|
||||
%endif
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -18,22 +18,12 @@
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["domReady!", "js/collections/group_configuration", "js/views/pages/group_configurations"],
|
||||
function(doc, GroupConfigurationCollection, GroupConfigurationsPage) {
|
||||
% if configurations is not None:
|
||||
var collection = new GroupConfigurationCollection(${json.dumps(configurations)}, { parse: true });
|
||||
|
||||
collection.url = "${group_configuration_url}";
|
||||
collection.outlineUrl = "${course_outline_url}";
|
||||
new GroupConfigurationsPage({
|
||||
el: $('#content'),
|
||||
collection: collection
|
||||
}).render();
|
||||
% endif
|
||||
});
|
||||
</script>
|
||||
<%block name="requirejs">
|
||||
% if configurations is not None:
|
||||
require(["js/factories/group_configurations"], function(GroupConfigurationsFactory) {
|
||||
GroupConfigurationsFactory(${json.dumps(configurations)}, "${group_configuration_url}", "${course_outline_url}");
|
||||
});
|
||||
% endif
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -147,127 +147,8 @@
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script>
|
||||
require(
|
||||
["js/views/import", "jquery", "gettext", "jquery.fileupload", "jquery.cookie"],
|
||||
function(CourseImport, $, gettext) {
|
||||
|
||||
var file;
|
||||
var bar = $('.progress-bar');
|
||||
var fill = $('.progress-fill');
|
||||
var percent = $('.percent');
|
||||
var status = $('#status');
|
||||
var statusBlock = $('.status-block');
|
||||
var submitBtn = $('.submit-button');
|
||||
var chooseBtn = $('.choose-file-button');
|
||||
|
||||
var allStats = $('#status-infos');
|
||||
|
||||
var feedbackUrl = "${import_status_url}";
|
||||
|
||||
var defaults = [
|
||||
"${_("There was an error during the upload process.")}\n",
|
||||
"${_("There was an error while unpacking the file.")}\n",
|
||||
"${_("There was an error while verifying the file you submitted.")}\n",
|
||||
"${_("There was an error while importing the new course to our database.")}\n"
|
||||
];
|
||||
|
||||
// Display the status of last file upload on page load
|
||||
var lastfileupload = $.cookie('lastfileupload');
|
||||
if (lastfileupload){
|
||||
CourseImport.getAndStartUploadFeedback(feedbackUrl.replace('fillerName', lastfileupload), lastfileupload);
|
||||
}
|
||||
|
||||
$('#fileupload').fileupload({
|
||||
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
|
||||
maxChunkSize: 20 * 1000000, // 20 MB
|
||||
|
||||
autoUpload: false,
|
||||
|
||||
add: function(e, data) {
|
||||
CourseImport.clearImportDisplay();
|
||||
submitBtn.unbind('click');
|
||||
file = data.files[0];
|
||||
if (file.name.match(/tar\.gz$/)) {
|
||||
submitBtn.click(function(e){
|
||||
$.cookie('lastfileupload', file.name);
|
||||
e.preventDefault();
|
||||
submitBtn.hide();
|
||||
CourseImport.startUploadFeedback();
|
||||
data.submit().complete(function(result, textStatus, xhr) {
|
||||
window.onbeforeunload = null;
|
||||
if (xhr.status != 200) {
|
||||
try{
|
||||
var serverMsg = $.parseJSON(result.responseText);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
var serverMsg = $.parseJSON(result.responseText);
|
||||
var errMsg = serverMsg.hasOwnProperty("ErrMsg") ? serverMsg.ErrMsg : "" ;
|
||||
if (serverMsg.hasOwnProperty("Stage")) {
|
||||
var stage = Math.abs(serverMsg.Stage);
|
||||
CourseImport.stageError(stage, defaults[stage] + errMsg);
|
||||
}
|
||||
else {
|
||||
alert("${_("Your import has failed.")}\n\n" + errMsg);
|
||||
}
|
||||
chooseBtn.html("${_("Choose new file")}").show();
|
||||
bar.hide();
|
||||
}
|
||||
CourseImport.stopGetStatus = true;
|
||||
chooseBtn.html("${_("Choose new file")}").show();
|
||||
bar.hide();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
data.files = [];
|
||||
}
|
||||
},
|
||||
|
||||
progressall: function(e, data){
|
||||
var doneAt;
|
||||
var percentInt = data.loaded / data.total * 100
|
||||
var percentVal = parseInt(percentInt, 10) + "%";
|
||||
// Firefox makes ProgressEvent.loaded equal ProgressEvent.total only
|
||||
// after receiving a response from the server (see Mozilla bug 637002),
|
||||
// so for Firefox we jump the gun a little.
|
||||
if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
|
||||
doneAt = 95;
|
||||
} else {
|
||||
doneAt = 99;
|
||||
}
|
||||
if (percentInt >= doneAt) {
|
||||
bar.hide();
|
||||
// Start feedback with delay so that current stage of import properly updates in session
|
||||
setTimeout(
|
||||
function() { CourseImport.startServerFeedback(feedbackUrl.replace('fillerName', file.name)) },
|
||||
3000
|
||||
);
|
||||
} else {
|
||||
bar.show();
|
||||
fill.width(percentVal);
|
||||
fill.html(percentVal);
|
||||
}
|
||||
},
|
||||
done: function(e, data){
|
||||
bar.hide();
|
||||
window.onbeforeunload = null;
|
||||
CourseImport.displayFinishedImport();
|
||||
},
|
||||
start: function(e) {
|
||||
window.onbeforeunload = function() {
|
||||
return "${_("Your import is in progress; navigating away will abort it.")}";
|
||||
}
|
||||
},
|
||||
sequentialUploads: true,
|
||||
notifyOnError: false
|
||||
|
||||
});
|
||||
|
||||
}); // end define()
|
||||
</script>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/import"], function(ImportFactory) {
|
||||
ImportFactory("${import_status_url}");
|
||||
});
|
||||
</%block>
|
||||
|
||||
@@ -5,31 +5,10 @@
|
||||
<%block name="title">${_("My Courses")}</%block>
|
||||
<%block name="bodyclass">is-signedin index view-dashboard</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
|
||||
// showing/hiding creation rights UI
|
||||
$('.show-creationrights').click(function(e){
|
||||
(e).preventDefault();
|
||||
$(this).closest('.wrapper-creationrights').toggleClass('is-shown').find('.ui-toggle-control').toggleClass('current');
|
||||
});
|
||||
|
||||
var reloadPage = function () {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
var showError = function () {
|
||||
$('#request-coursecreator-submit').toggleClass('has-error').find('.label').text('Sorry, there was error with your request');
|
||||
$('#request-coursecreator-submit').find('.icon-cog').toggleClass('icon-spin');
|
||||
};
|
||||
|
||||
$('#request-coursecreator').ajaxForm({error: showError, success: reloadPage});
|
||||
|
||||
$('#request-coursecreator-submit').click(function(e){
|
||||
$(this).toggleClass('is-disabled is-submitting').find('.label').text('Submitting Your Request');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/index"], function (IndexFactory) {
|
||||
IndexFactory();
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -57,42 +57,8 @@ from django.utils.translation import ugettext as _
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["jquery", "jquery.cookie", "utility"], function($) {
|
||||
function postJSON(url, data, callback) {
|
||||
$.ajax({type:'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
success: callback,
|
||||
headers : {'X-CSRFToken':$.cookie('csrftoken')}
|
||||
});
|
||||
}
|
||||
|
||||
$('form#login_form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
var submit_data = $('#login_form').serialize();
|
||||
|
||||
postJSON('/login_post',
|
||||
submit_data,
|
||||
function(json) {
|
||||
if(json.success) {
|
||||
var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search));
|
||||
if (next && next.length > 1 && !isExternal(next[1])) {
|
||||
location.href = next[1];
|
||||
}
|
||||
else location.href = "${reverse('homepage')}";
|
||||
} else if($('#login_error').length == 0) {
|
||||
$('#login_form').prepend('<div id="login_error" class="message message-status error">' + json.value + '</span></div>');
|
||||
$('#login_error').addClass('is-shown');
|
||||
} else {
|
||||
$('#login_error').stop().addClass('is-shown');
|
||||
$('#login_error').html(json.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/login"], function(LoginFactory) {
|
||||
LoginFactory("${reverse('homepage')}");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -161,216 +161,8 @@
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["jquery", "underscore", "gettext", "js/views/feedback_prompt"],
|
||||
function($, _, gettext, PromptView) {
|
||||
|
||||
var staffEmails = ${json.dumps([user.email for user in staff])};
|
||||
var tplUserURL = "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'})}"
|
||||
|
||||
var unknownErrorMessage = gettext("Unknown");
|
||||
|
||||
$(document).ready(function() {
|
||||
var $createUserForm = $('#create-user-form');
|
||||
var $createUserFormWrapper = $createUserForm.closest('.wrapper-create-user');
|
||||
$createUserForm.bind('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var email = $('#user-email-input').val().trim();
|
||||
if(!email) {
|
||||
var msg = new PromptView.Error({
|
||||
title: gettext("A valid email address is required"),
|
||||
message: gettext("You must enter a valid email address in order to add a new team member"),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext("Return and add email address"),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
$("#user-email-input").focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
msg.show();
|
||||
return;
|
||||
}
|
||||
if(_.contains(staffEmails, email)) {
|
||||
var msg = new PromptView.Warning({
|
||||
title: gettext("Already a course team member"),
|
||||
message: _.template(gettext("{email} is already on the “{course}” team. If you're trying to add a new member, please double-check the email address you provided."), {email: email, course: course.escape('name')}, {interpolate: /\{(.+?)\}/g}),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext("Return to team listing"),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
$("#user-email-input").focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
msg.show();
|
||||
return;
|
||||
}
|
||||
var url = tplUserURL.replace("@@EMAIL@@", $('#user-email-input').val().trim())
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
role: 'staff',
|
||||
}),
|
||||
success: function(data) {
|
||||
location.reload();
|
||||
},
|
||||
notifyOnError: false,
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var message;
|
||||
try {
|
||||
message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
|
||||
} catch (e) {
|
||||
message = unknownErrorMessage
|
||||
}
|
||||
var prompt = new PromptView.Error({
|
||||
title: gettext("Error adding user"),
|
||||
message: message,
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext("OK"),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
$("#user-email-input").focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
prompt.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var $cancelButton = $createUserForm.find('.action-cancel');
|
||||
$cancelButton.bind('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('.create-user-button').toggleClass('is-disabled');
|
||||
$createUserFormWrapper.toggleClass('is-shown');
|
||||
$('#user-email-input').val('');
|
||||
});
|
||||
|
||||
$('.create-user-button').bind('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('.create-user-button').toggleClass('is-disabled');
|
||||
$createUserFormWrapper.toggleClass('is-shown');
|
||||
$createUserForm.find('#user-email-input').focus();
|
||||
});
|
||||
|
||||
$('body').bind('keyup', function(e) {
|
||||
if(e.which == 27) {
|
||||
$cancelButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
$('.remove-user').click(function() {
|
||||
var email = $(this).data('id');
|
||||
var msg = new PromptView.Warning({
|
||||
title: gettext("Are you sure?"),
|
||||
message: _.template(gettext("Are you sure you want to delete {email} from the course team for “{course}”?"), {email: email, course: course.get('name')}, {interpolate: /\{(.+?)\}/g}),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext("Delete"),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
var url = tplUserURL.replace("@@EMAIL@@", email)
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
success: function(data) {
|
||||
location.reload();
|
||||
},
|
||||
notifyOnError: false,
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var message;
|
||||
try {
|
||||
message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
|
||||
} catch (e) {
|
||||
message = unknownErrorMessage;
|
||||
}
|
||||
var prompt = new PromptView.Error({
|
||||
title: gettext("Error removing user"),
|
||||
message: message,
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext("OK"),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
prompt.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext("Cancel"),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
msg.show();
|
||||
});
|
||||
|
||||
$(".toggle-admin-role").click(function(e) {
|
||||
e.preventDefault()
|
||||
var type;
|
||||
if($(this).hasClass("add-admin-role")) {
|
||||
role = 'instructor';
|
||||
} else {
|
||||
role = 'staff';
|
||||
}
|
||||
var url = $(this).closest("li[data-url]").data("url");
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
role: role
|
||||
}),
|
||||
success: function(data) {
|
||||
location.reload();
|
||||
},
|
||||
notifyOnError: false,
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var message;
|
||||
try {
|
||||
message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
|
||||
} catch (e) {
|
||||
message = unknownErrorMessage;
|
||||
}
|
||||
var prompt = new PromptView.Error({
|
||||
title: gettext("There was an error changing the user's role"),
|
||||
message: message,
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext("Try Again"),
|
||||
click: function(view) {
|
||||
view.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
prompt.show();
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/manage_users"], function(ManageUsersFactory) {
|
||||
ManageUsersFactory(${json.dumps([user.email for user in staff])}, "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'})}");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -99,35 +99,8 @@
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["jquery", "jquery.cookie"], function($) {
|
||||
$("form :input").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
|
||||
$('form#register_form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
var submit_data = $('#register_form').serialize();
|
||||
|
||||
$.ajax({
|
||||
url: '/create_account',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: submit_data,
|
||||
headers: {'X-CSRFToken': $.cookie('csrftoken')},
|
||||
success: function(json) {
|
||||
location.href = "/course/";
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
json = $.parseJSON(jqXHR.responseText);
|
||||
$('#register_error').html(json.value).stop().addClass('is-shown');
|
||||
},
|
||||
notifyOnError: false
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/register"], function (RegisterFactory) {
|
||||
RegisterFactory();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -25,30 +25,13 @@
|
||||
window.CMS = window.CMS || {};
|
||||
CMS.URL = CMS.URL || {};
|
||||
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
|
||||
|
||||
require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/settings/main"],
|
||||
function(doc, $, CourseDetailsModel, MainView) {
|
||||
// highlighting labels when fields are focused in
|
||||
$("form :input").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
var model = new CourseDetailsModel();
|
||||
model.urlRoot = '${details_url}';
|
||||
model.fetch({
|
||||
success: function(model) {
|
||||
var editor = new MainView({
|
||||
el: $('.settings-details'),
|
||||
model: model
|
||||
});
|
||||
editor.render();
|
||||
},
|
||||
reset: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/settings"], function(SettingsFactory) {
|
||||
SettingsFactory("${details_url}");
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
|
||||
@@ -9,53 +9,18 @@
|
||||
<%block name="title">${_("Advanced Settings")}</%block>
|
||||
<%block name="bodyclass">is-signedin course advanced view-settings</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["advanced_entry", "basic-modal", "modal-button", "validation-error-modal"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<script type="text/javascript">
|
||||
require(["domReady!", "jquery", "gettext", "js/models/settings/advanced", "js/views/settings/advanced"],
|
||||
function(doc, $, gettext, AdvancedSettingsModel, AdvancedSettingsView) {
|
||||
$("form :input").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
|
||||
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
|
||||
var advancedModel = new AdvancedSettingsModel(${advanced_dict | n}, {parse: true});
|
||||
advancedModel.url = "${advanced_settings_url}";
|
||||
|
||||
var editor = new AdvancedSettingsView({
|
||||
el: $('.settings-advanced'),
|
||||
model: advancedModel
|
||||
});
|
||||
editor.render();
|
||||
|
||||
$("#deprecated-settings").click(function() {
|
||||
var $this = $(this);
|
||||
var wrapperDeprecatedSetting = $('.wrapper-deprecated-setting');
|
||||
var deprecatedSettingsLabel = $('.deprecated-settings-label');
|
||||
if ($this.is(':checked')) {
|
||||
wrapperDeprecatedSetting.addClass('is-set');
|
||||
deprecatedSettingsLabel.text('${escapejs(_('Hide Deprecated Settings'))}');
|
||||
editor.render_deprecated = true;
|
||||
}
|
||||
else {
|
||||
wrapperDeprecatedSetting.removeClass('is-set');
|
||||
deprecatedSettingsLabel.text('${escapejs(_('Show Deprecated Settings'))}');
|
||||
editor.render_deprecated = false;
|
||||
}
|
||||
|
||||
editor.render();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/settings_advanced"], function(SettingsAdvancedFactory) {
|
||||
SettingsAdvancedFactory(${advanced_dict | n}, "${advanced_settings_url}");
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -19,24 +19,11 @@
|
||||
|
||||
<%block name="jsextra">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script type="text/javascript">
|
||||
require(["domReady!", "jquery", "js/views/settings/grading", "js/models/settings/course_grading_policy"], function(doc, $, GradingView, CourseGradingPolicyModel) {
|
||||
$("form :input").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
</%block>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/settings_graders"], function(SettingsGradersFactory) {
|
||||
SettingsGradersFactory(${course_details|n}, "${grading_url}");
|
||||
});
|
||||
|
||||
var model = new CourseGradingPolicyModel(${course_details|n},{parse:true});
|
||||
model.urlRoot = '${grading_url}';
|
||||
var editor = new GradingView({
|
||||
el: $('.settings-grading'),
|
||||
model : model
|
||||
});
|
||||
|
||||
editor.render();
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
@@ -22,26 +22,13 @@ CMS.URL = CMS.URL || {};
|
||||
CMS.URL.UPLOAD_ASSET = "${upload_asset_url}"
|
||||
CMS.URL.TEXTBOOKS = "${textbook_url}"
|
||||
CMS.URL.LMS_BASE = "${settings.LMS_BASE}"
|
||||
require(["js/models/section", "js/collections/textbook", "js/views/list_textbooks"],
|
||||
function(Section, TextbookCollection, ListTextbooksView) {
|
||||
var textbooks = new TextbookCollection(${json.dumps(textbooks)}, {parse: true});
|
||||
var tbView = new ListTextbooksView({collection: textbooks});
|
||||
|
||||
$(function() {
|
||||
$(".content-primary").append(tbView.render().el);
|
||||
$(".nav-actions .new-button").click(function(e) {
|
||||
tbView.addOne(e);
|
||||
})
|
||||
$(window).on("beforeunload", function() {
|
||||
var dirty = textbooks.find(function(textbook) { return textbook.isDirty(); });
|
||||
if(dirty) {
|
||||
return "${_('You have unsaved changes. Do you really want to leave this page?')}";
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/textbooks"], function(TextbooksFactory) {
|
||||
TextbooksFactory(${json.dumps(textbooks)});
|
||||
});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
|
||||
0
common/djangoapps/course_modes/helpers.py
Normal file
0
common/djangoapps/course_modes/helpers.py
Normal file
@@ -163,6 +163,90 @@ class CourseMode(models.Model):
|
||||
return mode.min_price
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def has_payment_options(cls, course_id):
|
||||
"""Determines if there is any mode that has payment options
|
||||
|
||||
Check the dict of course modes and see if any of them have a minimum price or
|
||||
suggested prices. Returns True if any course mode has a payment option.
|
||||
|
||||
Args:
|
||||
course_mode_dict (dict): Dictionary mapping course mode slugs to Modes
|
||||
|
||||
Returns:
|
||||
True if any course mode has a payment option.
|
||||
|
||||
"""
|
||||
for mode in cls.modes_for_course(course_id):
|
||||
if mode.min_price > 0 or mode.suggested_prices != '':
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def can_auto_enroll(cls, course_id, modes_dict=None):
|
||||
"""Check whether students should be auto-enrolled in the course.
|
||||
|
||||
If a course is behind a paywall (e.g. professional ed or white-label),
|
||||
then users should NOT be auto-enrolled. Instead, the user will
|
||||
be enrolled when he/she completes the payment flow.
|
||||
|
||||
Otherwise, users can be enrolled in the default mode "honor"
|
||||
with the option to upgrade later.
|
||||
|
||||
Args:
|
||||
course_id (CourseKey): The course to check.
|
||||
|
||||
Keyword Args:
|
||||
modes_dict (dict): If provided, use these course modes.
|
||||
Useful for avoiding unnecessary database queries.
|
||||
|
||||
Returns:
|
||||
bool
|
||||
|
||||
"""
|
||||
if modes_dict is None:
|
||||
modes_dict = cls.modes_for_course_dict(course_id)
|
||||
|
||||
# Professional mode courses are always behind a paywall
|
||||
if "professional" in modes_dict:
|
||||
return False
|
||||
|
||||
# White-label uses course mode honor with a price
|
||||
# to indicate that the course is behind a paywall.
|
||||
if cls.is_white_label(course_id, modes_dict=modes_dict):
|
||||
return False
|
||||
|
||||
# Check that the default mode is available.
|
||||
return ("honor" in modes_dict)
|
||||
|
||||
@classmethod
|
||||
def is_white_label(cls, course_id, modes_dict=None):
|
||||
"""Check whether a course is a "white label" (paid) course.
|
||||
|
||||
By convention, white label courses have a course mode slug "honor"
|
||||
and a price.
|
||||
|
||||
Args:
|
||||
course_id (CourseKey): The course to check.
|
||||
|
||||
Keyword Args:
|
||||
modes_dict (dict): If provided, use these course modes.
|
||||
Useful for avoiding unnecessary database queries.
|
||||
|
||||
Returns:
|
||||
bool
|
||||
|
||||
"""
|
||||
if modes_dict is None:
|
||||
modes_dict = cls.modes_for_course_dict(course_id)
|
||||
|
||||
# White-label uses course mode honor with a price
|
||||
# to indicate that the course is behind a paywall.
|
||||
if "honor" in modes_dict and len(modes_dict) == 1:
|
||||
if modes_dict["honor"].min_price > 0 or modes_dict["honor"].suggested_prices != '':
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def min_course_price_for_currency(cls, course_id, currency):
|
||||
"""
|
||||
|
||||
@@ -7,12 +7,14 @@ Replace this with more appropriate tests for your application.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
import ddt
|
||||
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from django.test import TestCase
|
||||
from course_modes.models import CourseMode, Mode
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseModeModelTest(TestCase):
|
||||
"""
|
||||
Tests for the CourseMode model
|
||||
@@ -127,3 +129,37 @@ class CourseModeModelTest(TestCase):
|
||||
mode = CourseMode.verified_mode_for_course(self.course_key)
|
||||
|
||||
self.assertEqual(mode.slug, 'professional')
|
||||
|
||||
def test_course_has_payment_options(self):
|
||||
# Has no payment options.
|
||||
honor, _ = self.create_mode('honor', 'Honor')
|
||||
self.assertFalse(CourseMode.has_payment_options(self.course_key))
|
||||
|
||||
# Now we do have a payment option.
|
||||
verified, _ = self.create_mode('verified', 'Verified', min_price=5)
|
||||
self.assertTrue(CourseMode.has_payment_options(self.course_key))
|
||||
|
||||
# Unset verified's minimum price.
|
||||
verified.min_price = 0
|
||||
verified.save()
|
||||
self.assertFalse(CourseMode.has_payment_options(self.course_key))
|
||||
|
||||
# Finally, give the honor mode payment options
|
||||
honor.suggested_prices = '5, 10, 15'
|
||||
honor.save()
|
||||
self.assertTrue(CourseMode.has_payment_options(self.course_key))
|
||||
|
||||
@ddt.data(
|
||||
([], True),
|
||||
([("honor", 0), ("audit", 0), ("verified", 100)], True),
|
||||
([("honor", 100)], False),
|
||||
([("professional", 100)], False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_can_auto_enroll(self, modes_and_prices, can_auto_enroll):
|
||||
# Create the modes and min prices
|
||||
for mode_slug, min_price in modes_and_prices:
|
||||
self.create_mode(mode_slug, mode_slug.capitalize(), min_price=min_price)
|
||||
|
||||
# Verify that we can or cannot auto enroll
|
||||
self.assertEqual(CourseMode.can_auto_enroll(self.course_key), can_auto_enroll)
|
||||
|
||||
@@ -32,41 +32,33 @@ class CourseModeViewTest(ModuleStoreTestCase):
|
||||
self.client.login(username=self.user.username, password="edx")
|
||||
|
||||
@ddt.data(
|
||||
# is_active?, enrollment_mode, upgrade?, redirect?
|
||||
(True, 'verified', True, False), # User has an active verified enrollment and is trying to upgrade
|
||||
(True, 'verified', False, True), # User has an active verified enrollment and is not trying to upgrade
|
||||
(True, 'honor', True, False), # User has an active honor enrollment and is trying to upgrade
|
||||
(True, 'honor', False, False), # User has an active honor enrollment and is not trying to upgrade
|
||||
(True, 'audit', True, False), # User has an active audit enrollment and is trying to upgrade
|
||||
(True, 'audit', False, False), # User has an active audit enrollment and is not trying to upgrade
|
||||
(False, 'verified', True, True), # User has an inactive verified enrollment and is trying to upgrade
|
||||
(False, 'verified', False, True), # User has an inactive verified enrollment and is not trying to upgrade
|
||||
(False, 'honor', True, True), # User has an inactive honor enrollment and is trying to upgrade
|
||||
(False, 'honor', False, True), # User has an inactive honor enrollment and is not trying to upgrade
|
||||
(False, 'audit', True, True), # User has an inactive audit enrollment and is trying to upgrade
|
||||
(False, 'audit', False, True), # User has an inactive audit enrollment and is not trying to upgrade
|
||||
# is_active?, enrollment_mode, redirect?
|
||||
(True, 'verified', True),
|
||||
(True, 'honor', False),
|
||||
(True, 'audit', False),
|
||||
(False, 'verified', False),
|
||||
(False, 'honor', False),
|
||||
(False, 'audit', False),
|
||||
(False, None, False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_redirect_to_dashboard(self, is_active, enrollment_mode, upgrade, redirect):
|
||||
def test_redirect_to_dashboard(self, is_active, enrollment_mode, redirect):
|
||||
# Create the course modes
|
||||
for mode in ('audit', 'honor', 'verified'):
|
||||
CourseModeFactory(mode_slug=mode, course_id=self.course.id)
|
||||
|
||||
# Enroll the user in the test course
|
||||
CourseEnrollmentFactory(
|
||||
is_active=is_active,
|
||||
mode=enrollment_mode,
|
||||
course_id=self.course.id,
|
||||
user=self.user
|
||||
)
|
||||
if enrollment_mode is not None:
|
||||
CourseEnrollmentFactory(
|
||||
is_active=is_active,
|
||||
mode=enrollment_mode,
|
||||
course_id=self.course.id,
|
||||
user=self.user
|
||||
)
|
||||
|
||||
# Configure whether we're upgrading or not
|
||||
get_params = {}
|
||||
if upgrade:
|
||||
get_params = {'upgrade': True}
|
||||
|
||||
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
response = self.client.get(url, get_params)
|
||||
response = self.client.get(url)
|
||||
|
||||
# Check whether we were correctly redirected
|
||||
if redirect:
|
||||
@@ -74,7 +66,19 @@ class CourseModeViewTest(ModuleStoreTestCase):
|
||||
else:
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
def test_redirect_to_dashboard_no_enrollment(self):
|
||||
def test_upgrade_copy(self):
|
||||
# Create the course modes
|
||||
for mode in ('audit', 'honor', 'verified'):
|
||||
CourseModeFactory(mode_slug=mode, course_id=self.course.id)
|
||||
|
||||
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
response = self.client.get(url, {"upgrade": True})
|
||||
|
||||
# Verify that the upgrade copy is displayed instead
|
||||
# of the usual text.
|
||||
self.assertContains(response, "Upgrade Your Enrollment")
|
||||
|
||||
def test_no_enrollment(self):
|
||||
# Create the course modes
|
||||
for mode in ('audit', 'honor', 'verified'):
|
||||
CourseModeFactory(mode_slug=mode, course_id=self.course.id)
|
||||
@@ -83,7 +87,7 @@ class CourseModeViewTest(ModuleStoreTestCase):
|
||||
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertRedirects(response, reverse('dashboard'))
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
@ddt.data(
|
||||
'',
|
||||
@@ -121,7 +125,7 @@ class CourseModeViewTest(ModuleStoreTestCase):
|
||||
# TODO: Fix it so that response.templates works w/ mako templates, and then assert
|
||||
# that the right template rendered
|
||||
|
||||
def test_professional_registration(self):
|
||||
def test_professional_enrollment(self):
|
||||
# The only course mode is professional ed
|
||||
CourseModeFactory(mode_slug='professional', course_id=self.course.id)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from course_modes.models import CourseMode
|
||||
from courseware.access import has_access
|
||||
from student.models import CourseEnrollment
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from util.db import commit_on_success_with_read_committed
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -26,10 +27,10 @@ class ChooseModeView(View):
|
||||
|
||||
When a get request is used, shows the selection page.
|
||||
|
||||
When a post request is used, assumes that it is a form submission
|
||||
When a post request is used, assumes that it is a form submission
|
||||
from the selection page, parses the response, and then sends user
|
||||
to the next step in the flow.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
@method_decorator(login_required)
|
||||
@@ -48,28 +49,19 @@ class ChooseModeView(View):
|
||||
Response
|
||||
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key)
|
||||
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
|
||||
upgrade = request.GET.get('upgrade', False)
|
||||
request.session['attempting_upgrade'] = upgrade
|
||||
|
||||
# Students will already have an active course enrollment at this stage,
|
||||
# but we should still show them the "choose your track" page so they have
|
||||
# the option to enter the verification/payment flow.
|
||||
go_to_dashboard = (
|
||||
not upgrade and enrollment_mode in ['verified', 'professional']
|
||||
)
|
||||
|
||||
if go_to_dashboard:
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key)
|
||||
modes = CourseMode.modes_for_course_dict(course_key)
|
||||
|
||||
# We assume that, if 'professional' is one of the modes, it is the *only* mode.
|
||||
# If we offer more modes alongside 'professional' in the future, this will need to route
|
||||
# to the usual "choose your track" page.
|
||||
if "professional" in modes:
|
||||
has_enrolled_professional = (enrollment_mode == "professional" and is_active)
|
||||
if "professional" in modes and not has_enrolled_professional:
|
||||
return redirect(
|
||||
reverse(
|
||||
'verify_student_show_requirements',
|
||||
@@ -77,14 +69,15 @@ class ChooseModeView(View):
|
||||
)
|
||||
)
|
||||
|
||||
# If a user's course enrollment is inactive at this stage, the track
|
||||
# selection page may have been visited directly, so we should redirect
|
||||
# the user to their dashboard. By the time the user gets here during the
|
||||
# normal registration process, they will already have an activated enrollment;
|
||||
# the button appearing on the track selection page only redirects the user to
|
||||
# the dashboard, and we don't want the user to be confused when they click the
|
||||
# honor button and are taken to their dashboard without being enrolled.
|
||||
if not is_active:
|
||||
# If there isn't a verified mode available, then there's nothing
|
||||
# to do on this page. The user has almost certainly been auto-registered
|
||||
# in the "honor" track by this point, so we send the user
|
||||
# to the dashboard.
|
||||
if not CourseMode.has_verified_mode(modes):
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
# If a user has already paid, redirect them to the dashboard.
|
||||
if is_active and enrollment_mode in CourseMode.VERIFIED_MODES:
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
donation_for_course = request.session.get("donation_for_course", {})
|
||||
|
||||
113
common/djangoapps/student/helpers.py
Normal file
113
common/djangoapps/student/helpers.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Helpers for the student app. """
|
||||
import time
|
||||
from django.utils.http import cookie_date
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from course_modes.models import CourseMode
|
||||
from third_party_auth import ( # pylint: disable=W0611
|
||||
pipeline, provider,
|
||||
is_enabled as third_party_auth_enabled
|
||||
)
|
||||
|
||||
|
||||
def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None):
|
||||
"""Retrieve URLs for each enabled third-party auth provider.
|
||||
|
||||
These URLs are used on the "sign up" and "sign in" buttons
|
||||
on the login/registration forms to allow users to begin
|
||||
authentication with a third-party provider.
|
||||
|
||||
Optionally, we can redirect the user to an arbitrary
|
||||
url after auth completes successfully. We use this
|
||||
to redirect the user to a page that required login,
|
||||
or to send users to the payment flow when enrolling
|
||||
in a course.
|
||||
|
||||
Args:
|
||||
auth_entry (string): Either `pipeline.AUTH_ENTRY_LOGIN` or `pipeline.AUTH_ENTRY_REGISTER`
|
||||
|
||||
Keyword Args:
|
||||
redirect_url (unicode): If provided, send users to this URL
|
||||
after they successfully authenticate.
|
||||
|
||||
course_id (unicode): The ID of the course the user is enrolling in.
|
||||
We use this to send users to the track selection page
|
||||
if the course has a payment option.
|
||||
Note that `redirect_url` takes precedence over the redirect
|
||||
to the track selection page.
|
||||
|
||||
Returns:
|
||||
dict mapping provider names to URLs
|
||||
|
||||
"""
|
||||
if not third_party_auth_enabled():
|
||||
return {}
|
||||
|
||||
if redirect_url is not None:
|
||||
pipeline_redirect = redirect_url
|
||||
elif course_id is not None:
|
||||
# If the course is white-label (paid), then we send users
|
||||
# to the shopping cart. (There is a third party auth pipeline
|
||||
# step that will add the course to the cart.)
|
||||
if CourseMode.is_white_label(CourseKey.from_string(course_id)):
|
||||
pipeline_redirect = reverse("shoppingcart.views.show_cart")
|
||||
|
||||
# Otherwise, send the user to the track selection page.
|
||||
# The track selection page may redirect the user to the dashboard
|
||||
# (if the only available mode is honor), or directly to verification
|
||||
# (for professional ed).
|
||||
else:
|
||||
pipeline_redirect = reverse(
|
||||
"course_modes_choose",
|
||||
kwargs={'course_id': unicode(course_id)}
|
||||
)
|
||||
else:
|
||||
pipeline_redirect = None
|
||||
|
||||
return {
|
||||
provider.NAME: pipeline.get_login_url(
|
||||
provider.NAME, auth_entry,
|
||||
enroll_course_id=course_id,
|
||||
redirect_url=pipeline_redirect
|
||||
)
|
||||
for provider in provider.Registry.enabled()
|
||||
}
|
||||
|
||||
|
||||
def set_logged_in_cookie(request, response):
|
||||
"""Set a cookie indicating that the user is logged in.
|
||||
|
||||
Some installations have an external marketing site configured
|
||||
that displays a different UI when the user is logged in
|
||||
(e.g. a link to the student dashboard instead of to the login page)
|
||||
|
||||
Arguments:
|
||||
request (HttpRequest): The request to the view, used to calculate
|
||||
the cookie's expiration date based on the session expiration date.
|
||||
response (HttpResponse): The response on which the cookie will be set.
|
||||
|
||||
Returns:
|
||||
HttpResponse
|
||||
|
||||
"""
|
||||
if request.session.get_expire_at_browser_close():
|
||||
max_age = None
|
||||
expires = None
|
||||
else:
|
||||
max_age = request.session.get_expiry_age()
|
||||
expires_time = time.time() + max_age
|
||||
expires = cookie_date(expires_time)
|
||||
|
||||
response.set_cookie(
|
||||
settings.EDXMKTG_COOKIE_NAME, 'true', max_age=max_age,
|
||||
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
path='/', secure=None, httponly=None,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def is_logged_in_cookie_set(request):
|
||||
"""Check whether the request has the logged in cookie set. """
|
||||
return settings.EDXMKTG_COOKIE_NAME in request.COOKIES
|
||||
@@ -1,81 +1,135 @@
|
||||
"""
|
||||
Transfer Student Management Command
|
||||
"""
|
||||
from django.db import transaction
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from optparse import make_option
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import CourseEnrollment
|
||||
from shoppingcart.models import CertificateItem
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from track.management.tracked_command import TrackedCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
class TransferStudentError(Exception):
|
||||
"""Generic Error when handling student transfers."""
|
||||
pass
|
||||
|
||||
|
||||
class Command(TrackedCommand):
|
||||
"""Management Command for transferring students from one course to new courses."""
|
||||
help = """
|
||||
This command takes two course ids as input and transfers
|
||||
all students enrolled in one course into the other. This will
|
||||
remove them from the first class and enroll them in the second
|
||||
class in the same mode as the first one. eg. honor, verified,
|
||||
remove them from the first class and enroll them in the specified
|
||||
class(es) in the same mode as the first one. eg. honor, verified,
|
||||
audit.
|
||||
|
||||
example:
|
||||
# Transfer students from the old demoX class to a new one.
|
||||
manage.py ... transfer_students -f edX/Open_DemoX/edx_demo_course -t edX/Open_DemoX/new_demoX
|
||||
|
||||
# Transfer students from old course to new, with original certificate items.
|
||||
manage.py ... transfer_students -f edX/Open_DemoX/edx_demo_course -t edX/Open_DemoX/new_demoX -c true
|
||||
|
||||
# Transfer students from the old demoX class into two new classes.
|
||||
manage.py ... transfer_students -f edX/Open_DemoX/edx_demo_course
|
||||
-t edX/Open_DemoX/new_demoX,edX/Open_DemoX/edX_Insider
|
||||
|
||||
"""
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
option_list = TrackedCommand.option_list + (
|
||||
make_option('-f', '--from',
|
||||
metavar='SOURCE_COURSE',
|
||||
dest='source_course',
|
||||
help='The course to transfer students from.'),
|
||||
make_option('-t', '--to',
|
||||
metavar='DEST_COURSE',
|
||||
dest='dest_course',
|
||||
help='The new course to enroll the student into.'),
|
||||
metavar='DEST_COURSE_LIST',
|
||||
dest='dest_course_list',
|
||||
help='The new course(es) to enroll the student into.'),
|
||||
make_option('-c', '--transfer-certificates',
|
||||
metavar='TRANSFER_CERTIFICATES',
|
||||
dest='transfer_certificates',
|
||||
help="If True, try to transfer certificate items to the new course.")
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
source_key = SlashSeparatedCourseKey.from_deprecated_string(options['source_course'])
|
||||
dest_key = SlashSeparatedCourseKey.from_deprecated_string(options['dest_course'])
|
||||
@transaction.commit_manually
|
||||
def handle(self, *args, **options): # pylint: disable=unused-argument
|
||||
source_key = CourseKey.from_string(options.get('source_course', ''))
|
||||
dest_keys = []
|
||||
for course_key in options.get('dest_course_list', '').split(','):
|
||||
dest_keys.append(CourseKey.from_string(course_key))
|
||||
|
||||
if not source_key or not dest_keys:
|
||||
raise TransferStudentError(u"Must have a source course and destination course specified.")
|
||||
|
||||
tc_option = options.get('transfer_certificates', '')
|
||||
transfer_certificates = ('true' == tc_option.lower()) if tc_option else False
|
||||
if transfer_certificates and len(dest_keys) != 1:
|
||||
raise TransferStudentError(u"Cannot transfer certificate items from one course to many.")
|
||||
|
||||
source_students = User.objects.filter(
|
||||
courseenrollment__course_id=source_key
|
||||
)
|
||||
|
||||
for user in source_students:
|
||||
if CourseEnrollment.is_enrolled(user, dest_key):
|
||||
# Un Enroll from source course but don't mess
|
||||
# with the enrollment in the destination course.
|
||||
CourseEnrollment.unenroll(user, source_key)
|
||||
print("Unenrolled {} from {}".format(user.username, source_key.to_deprecated_string()))
|
||||
msg = "Skipping {}, already enrolled in destination course {}"
|
||||
print(msg.format(user.username, dest_key.to_deprecated_string()))
|
||||
continue
|
||||
with transaction.commit_on_success():
|
||||
print("Moving {}.".format(user.username))
|
||||
# Find the old enrollment.
|
||||
enrollment = CourseEnrollment.objects.get(
|
||||
user=user,
|
||||
course_id=source_key
|
||||
)
|
||||
|
||||
print("Moving {}.".format(user.username))
|
||||
# Find the old enrollment.
|
||||
enrollment = CourseEnrollment.objects.get(
|
||||
user=user,
|
||||
course_id=source_key
|
||||
# Move the Student between the classes.
|
||||
mode = enrollment.mode
|
||||
old_is_active = enrollment.is_active
|
||||
CourseEnrollment.unenroll(user, source_key, emit_unenrollment_event=False)
|
||||
print(u"Unenrolled {} from {}".format(user.username, unicode(source_key)))
|
||||
|
||||
for dest_key in dest_keys:
|
||||
if CourseEnrollment.is_enrolled(user, dest_key):
|
||||
# Un Enroll from source course but don't mess
|
||||
# with the enrollment in the destination course.
|
||||
msg = u"Skipping {}, already enrolled in destination course {}"
|
||||
print(msg.format(user.username, unicode(dest_key)))
|
||||
else:
|
||||
new_enrollment = CourseEnrollment.enroll(user, dest_key, mode=mode)
|
||||
|
||||
# Un-enroll from the new course if the user had un-enrolled
|
||||
# form the old course.
|
||||
if not old_is_active:
|
||||
new_enrollment.update_enrollment(is_active=False, emit_unenrollment_event=False)
|
||||
|
||||
if transfer_certificates:
|
||||
self._transfer_certificate_item(source_key, enrollment, user, dest_keys, new_enrollment)
|
||||
|
||||
@staticmethod
|
||||
def _transfer_certificate_item(source_key, enrollment, user, dest_keys, new_enrollment):
|
||||
""" Transfer the certificate item from one course to another.
|
||||
|
||||
Do not use this generally, since certificate items are directly associated with a particular purchase.
|
||||
This should only be used when a single course to a new location. This cannot be used when transferring
|
||||
from one course to many.
|
||||
|
||||
Args:
|
||||
source_key (str): The course key string representation for the original course.
|
||||
enrollment (CourseEnrollment): The original enrollment to move the certificate item from.
|
||||
user (User): The user to transfer the item for.
|
||||
dest_keys (list): A list of course key strings to transfer the item to.
|
||||
new_enrollment (CourseEnrollment): The new enrollment to associate the certificate item with.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
try:
|
||||
certificate_item = CertificateItem.objects.get(
|
||||
course_id=source_key,
|
||||
course_enrollment=enrollment
|
||||
)
|
||||
except CertificateItem.DoesNotExist:
|
||||
print(u"No certificate for {}".format(user))
|
||||
return
|
||||
|
||||
# Move the Student between the classes.
|
||||
mode = enrollment.mode
|
||||
old_is_active = enrollment.is_active
|
||||
CourseEnrollment.unenroll(user, source_key)
|
||||
new_enrollment = CourseEnrollment.enroll(user, dest_key, mode=mode)
|
||||
|
||||
# Unenroll from the new coures if the user had unenrolled
|
||||
# form the old course.
|
||||
if not old_is_active:
|
||||
new_enrollment.update_enrollment(is_active=False)
|
||||
|
||||
if mode == 'verified':
|
||||
try:
|
||||
certificate_item = CertificateItem.objects.get(
|
||||
course_id=source_key,
|
||||
course_enrollment=enrollment
|
||||
)
|
||||
except CertificateItem.DoesNotExist:
|
||||
print("No certificate for {}".format(user))
|
||||
continue
|
||||
|
||||
certificate_item.course_id = dest_key
|
||||
certificate_item.course_enrollment = new_enrollment
|
||||
certificate_item.save()
|
||||
certificate_item.course_id = dest_keys[0]
|
||||
certificate_item.course_enrollment = new_enrollment
|
||||
|
||||
1
common/djangoapps/student/management/tests/__init__.py
Normal file
1
common/djangoapps/student/management/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for Student Management Commands."""
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Tests the transfer student management command
|
||||
"""
|
||||
from django.conf import settings
|
||||
from opaque_keys.edx import locator
|
||||
import unittest
|
||||
import ddt
|
||||
from student.management.commands import transfer_students
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@ddt.ddt
|
||||
class TestTransferStudents(ModuleStoreTestCase):
|
||||
"""Tests for transferring students between courses."""
|
||||
|
||||
PASSWORD = 'test'
|
||||
|
||||
def test_transfer_students(self):
|
||||
student = UserFactory()
|
||||
student.set_password(self.PASSWORD) # pylint: disable=E1101
|
||||
student.save() # pylint: disable=E1101
|
||||
|
||||
# Original Course
|
||||
original_course_location = locator.CourseLocator('Org0', 'Course0', 'Run0')
|
||||
course = self._create_course(original_course_location)
|
||||
# Enroll the student in 'verified'
|
||||
CourseEnrollment.enroll(student, course.id, mode="verified")
|
||||
|
||||
# New Course 1
|
||||
course_location_one = locator.CourseLocator('Org1', 'Course1', 'Run1')
|
||||
new_course_one = self._create_course(course_location_one)
|
||||
|
||||
# New Course 2
|
||||
course_location_two = locator.CourseLocator('Org2', 'Course2', 'Run2')
|
||||
new_course_two = self._create_course(course_location_two)
|
||||
original_key = unicode(course.id)
|
||||
new_key_one = unicode(new_course_one.id)
|
||||
new_key_two = unicode(new_course_two.id)
|
||||
|
||||
# Run the actual management command
|
||||
transfer_students.Command().handle(
|
||||
source_course=original_key, dest_course_list=new_key_one + "," + new_key_two
|
||||
)
|
||||
|
||||
# Confirm the enrollment mode is verified on the new courses, and enrollment is enabled as appropriate.
|
||||
self.assertEquals(('verified', False), CourseEnrollment.enrollment_mode_for_user(student, course.id))
|
||||
self.assertEquals(('verified', True), CourseEnrollment.enrollment_mode_for_user(student, new_course_one.id))
|
||||
self.assertEquals(('verified', True), CourseEnrollment.enrollment_mode_for_user(student, new_course_two.id))
|
||||
|
||||
def _create_course(self, course_location):
|
||||
""" Creates a course """
|
||||
return CourseFactory.create(
|
||||
org=course_location.org,
|
||||
number=course_location.course,
|
||||
run=course_location.run
|
||||
)
|
||||
@@ -776,7 +776,7 @@ class CourseEnrollment(models.Model):
|
||||
is_course_full = cls.num_enrolled_in(course.id) >= course.max_student_enrollments_allowed
|
||||
return is_course_full
|
||||
|
||||
def update_enrollment(self, mode=None, is_active=None):
|
||||
def update_enrollment(self, mode=None, is_active=None, emit_unenrollment_event=True):
|
||||
"""
|
||||
Updates an enrollment for a user in a class. This includes options
|
||||
like changing the mode, toggling is_active True/False, etc.
|
||||
@@ -784,6 +784,7 @@ class CourseEnrollment(models.Model):
|
||||
Also emits relevant events for analytics purposes.
|
||||
|
||||
This saves immediately.
|
||||
|
||||
"""
|
||||
activation_changed = False
|
||||
# if is_active is None, then the call to update_enrollment didn't specify
|
||||
@@ -813,7 +814,7 @@ class CourseEnrollment(models.Model):
|
||||
u"mode:{}".format(self.mode)]
|
||||
)
|
||||
|
||||
else:
|
||||
elif emit_unenrollment_event:
|
||||
UNENROLL_DONE.send(sender=None, course_enrollment=self)
|
||||
|
||||
self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)
|
||||
@@ -987,7 +988,7 @@ class CourseEnrollment(models.Model):
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def unenroll(cls, user, course_id):
|
||||
def unenroll(cls, user, course_id, emit_unenrollment_event=True):
|
||||
"""
|
||||
Remove the user from a given course. If the relevant `CourseEnrollment`
|
||||
object doesn't exist, we log an error but don't throw an exception.
|
||||
@@ -997,10 +998,12 @@ class CourseEnrollment(models.Model):
|
||||
adding an enrollment for it.
|
||||
|
||||
`course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall)
|
||||
|
||||
`emit_unenrollment_events` can be set to False to suppress events firing.
|
||||
"""
|
||||
try:
|
||||
record = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
record.update_enrollment(is_active=False)
|
||||
record.update_enrollment(is_active=False, emit_unenrollment_event=emit_unenrollment_event)
|
||||
|
||||
except cls.DoesNotExist:
|
||||
err_msg = u"Tried to unenroll student {} from {} but they were not enrolled"
|
||||
|
||||
@@ -11,12 +11,8 @@ from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, mixed_store_config
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from social.strategies.django_strategy import DjangoStrategy
|
||||
from django.test.client import RequestFactory
|
||||
from student.tests.factories import UserFactory, CourseModeFactory
|
||||
from student.models import CourseEnrollment
|
||||
from student.views import register_user
|
||||
from third_party_auth.pipeline import change_enrollment as change_enrollment_third_party
|
||||
|
||||
# Since we don't need any XML course fixtures, use a modulestore configuration
|
||||
# that disables the XML modulestore.
|
||||
@@ -59,7 +55,7 @@ class EnrollmentTest(ModuleStoreTestCase):
|
||||
# Professional ed
|
||||
# Expect that we're sent to the "choose your track" page
|
||||
# (which will, in turn, redirect us to a page where we can verify/pay)
|
||||
# We should NOT be auto-enrolled, because that would be giving
|
||||
# We should NOT be auto-enrolled, because that would be giving
|
||||
# away an expensive course for free :)
|
||||
(['professional'], 'course_modes_choose', None),
|
||||
)
|
||||
@@ -97,25 +93,6 @@ class EnrollmentTest(ModuleStoreTestCase):
|
||||
self.assertTrue(is_active)
|
||||
self.assertEqual(course_mode, enrollment_mode)
|
||||
|
||||
def test_enroll_from_third_party_redirect(self):
|
||||
"""
|
||||
Test that, when a user visits the registration page *after* visiting a course,
|
||||
if they go on to register and/or log in via third-party auth, they'll be enrolled
|
||||
in that course.
|
||||
|
||||
The testing here is a bit hackish, since we just ping the registration page, then
|
||||
directly call the step in the third party pipeline that registers the user if
|
||||
`registration_course_id` is set in the session, but it should catch any major breaks.
|
||||
"""
|
||||
self.client.logout()
|
||||
self.client.get(reverse('register_user'), {'course_id': self.course.id})
|
||||
self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
||||
self.dummy_request = RequestFactory().request()
|
||||
self.dummy_request.session = self.client.session
|
||||
strategy = DjangoStrategy(RequestFactory, request=self.dummy_request)
|
||||
change_enrollment_third_party(is_register=True, strategy=strategy, user=self.user)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
def test_unenroll(self):
|
||||
# Enroll the student in the course
|
||||
CourseEnrollment.enroll(self.user, self.course.id, mode="honor")
|
||||
|
||||
185
common/djangoapps/student/tests/test_login_registration_forms.py
Normal file
185
common/djangoapps/student/tests/test_login_registration_forms.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""Tests for the login and registration form rendering. """
|
||||
import urllib
|
||||
import unittest
|
||||
from mock import patch
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
import ddt
|
||||
from django.test.utils import override_settings
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from student.tests.factories import CourseModeFactory
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, mixed_store_config
|
||||
)
|
||||
|
||||
|
||||
# This relies on third party auth being enabled and configured
|
||||
# in the test settings. See the setting `THIRD_PARTY_AUTH`
|
||||
# and the feature flag `ENABLE_THIRD_PARTY_AUTH`
|
||||
THIRD_PARTY_AUTH_BACKENDS = ["google-oauth2", "facebook"]
|
||||
THIRD_PARTY_AUTH_PROVIDERS = ["Google", "Facebook"]
|
||||
|
||||
# Since we don't need any XML course fixtures, use a modulestore configuration
|
||||
# that disables the XML modulestore.
|
||||
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
|
||||
|
||||
|
||||
def _third_party_login_url(backend_name, auth_entry, course_id=None, redirect_url=None):
|
||||
"""Construct the login URL to start third party authentication. """
|
||||
params = [("auth_entry", auth_entry)]
|
||||
if redirect_url:
|
||||
params.append(("next", redirect_url))
|
||||
if course_id:
|
||||
params.append(("enroll_course_id", course_id))
|
||||
|
||||
return u"{url}?{params}".format(
|
||||
url=reverse("social:begin", kwargs={"backend": backend_name}),
|
||||
params=urllib.urlencode(params)
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class LoginFormTest(ModuleStoreTestCase):
|
||||
"""Test rendering of the login form. """
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse("signin_user")
|
||||
self.course = CourseFactory.create()
|
||||
self.course_id = unicode(self.course.id)
|
||||
self.course_modes_url = reverse("course_modes_choose", kwargs={"course_id": self.course_id})
|
||||
self.courseware_url = reverse("courseware", args=[self.course_id])
|
||||
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False})
|
||||
@ddt.data(THIRD_PARTY_AUTH_PROVIDERS)
|
||||
def test_third_party_auth_disabled(self, provider_name):
|
||||
response = self.client.get(self.url)
|
||||
self.assertNotContains(response, provider_name)
|
||||
|
||||
@ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
|
||||
def test_third_party_auth_no_course_id(self, backend_name):
|
||||
response = self.client.get(self.url)
|
||||
expected_url = _third_party_login_url(backend_name, "login")
|
||||
self.assertContains(response, expected_url)
|
||||
|
||||
@ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
|
||||
def test_third_party_auth_with_course_id(self, backend_name):
|
||||
# Provide a course ID to the login page, simulating what happens
|
||||
# when a user tries to enroll in a course without being logged in
|
||||
response = self.client.get(self.url, {"course_id": self.course_id})
|
||||
|
||||
# Expect that the course ID is added to the third party auth entry
|
||||
# point, so that the pipeline will enroll the student and
|
||||
# redirect the student to the track selection page.
|
||||
expected_url = _third_party_login_url(
|
||||
backend_name,
|
||||
"login",
|
||||
course_id=self.course_id,
|
||||
redirect_url=self.course_modes_url
|
||||
)
|
||||
self.assertContains(response, expected_url)
|
||||
|
||||
@ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
|
||||
def test_third_party_auth_with_white_label_course(self, backend_name):
|
||||
# Set the course mode to honor with a min price,
|
||||
# indicating that the course is behind a paywall.
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug="honor",
|
||||
mode_display_name="Honor",
|
||||
min_price=100
|
||||
)
|
||||
|
||||
# Expect that we're redirected to the shopping cart
|
||||
# instead of to the track selection page.
|
||||
response = self.client.get(self.url, {"course_id": self.course_id})
|
||||
expected_url = _third_party_login_url(
|
||||
backend_name,
|
||||
"login",
|
||||
course_id=self.course_id,
|
||||
redirect_url=reverse("shoppingcart.views.show_cart")
|
||||
)
|
||||
self.assertContains(response, expected_url)
|
||||
|
||||
@ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
|
||||
def test_third_party_auth_with_redirect_url(self, backend_name):
|
||||
# Try to access courseware while logged out, expecting to be
|
||||
# redirected to the login page.
|
||||
response = self.client.get(self.courseware_url, follow=True)
|
||||
self.assertRedirects(
|
||||
response,
|
||||
u"{url}?next={redirect_url}".format(
|
||||
url=reverse("accounts_login"),
|
||||
redirect_url=self.courseware_url
|
||||
)
|
||||
)
|
||||
|
||||
# Verify that the third party auth URLs include the redirect URL
|
||||
# The third party auth pipeline will redirect to this page
|
||||
# once the user successfully authenticates.
|
||||
expected_url = _third_party_login_url(
|
||||
backend_name,
|
||||
"login",
|
||||
redirect_url=self.courseware_url
|
||||
)
|
||||
self.assertContains(response, expected_url)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class RegisterFormTest(TestCase):
|
||||
"""Test rendering of the registration form. """
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse("register_user")
|
||||
self.course = CourseFactory.create()
|
||||
self.course_id = unicode(self.course.id)
|
||||
self.course_modes_url = reverse("course_modes_choose", kwargs={"course_id": self.course_id})
|
||||
|
||||
@patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False})
|
||||
@ddt.data(*THIRD_PARTY_AUTH_PROVIDERS)
|
||||
def test_third_party_auth_disabled(self, provider_name):
|
||||
response = self.client.get(self.url)
|
||||
self.assertNotContains(response, provider_name)
|
||||
|
||||
@ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
|
||||
def test_register_third_party_auth_no_course_id(self, backend_name):
|
||||
response = self.client.get(self.url)
|
||||
expected_url = _third_party_login_url(backend_name, "register")
|
||||
self.assertContains(response, expected_url)
|
||||
|
||||
@ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
|
||||
def test_register_third_party_auth_with_course_id(self, backend_name):
|
||||
response = self.client.get(self.url, {"course_id": self.course_id})
|
||||
expected_url = _third_party_login_url(
|
||||
backend_name,
|
||||
"register",
|
||||
course_id=self.course_id,
|
||||
redirect_url=self.course_modes_url
|
||||
)
|
||||
self.assertContains(response, expected_url)
|
||||
|
||||
@ddt.data(*THIRD_PARTY_AUTH_BACKENDS)
|
||||
def test_third_party_auth_with_white_label_course(self, backend_name):
|
||||
# Set the course mode to honor with a min price,
|
||||
# indicating that the course is behind a paywall.
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug="honor",
|
||||
mode_display_name="Honor",
|
||||
min_price=100
|
||||
)
|
||||
|
||||
# Expect that we're redirected to the shopping cart
|
||||
# instead of to the track selection page.
|
||||
response = self.client.get(self.url, {"course_id": self.course_id})
|
||||
expected_url = _third_party_login_url(
|
||||
backend_name,
|
||||
"register",
|
||||
course_id=self.course_id,
|
||||
redirect_url=reverse("shoppingcart.views.show_cart")
|
||||
)
|
||||
self.assertContains(response, expected_url)
|
||||
@@ -94,6 +94,7 @@ from util.password_policy_validators import (
|
||||
|
||||
import third_party_auth
|
||||
from third_party_auth import pipeline, provider
|
||||
from student.helpers import auth_pipeline_urls, set_logged_in_cookie
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from shoppingcart.models import CourseRegistrationCode
|
||||
|
||||
@@ -353,13 +354,15 @@ def signin_user(request):
|
||||
if request.user.is_authenticated():
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
course_id = request.GET.get('course_id')
|
||||
context = {
|
||||
'course_id': request.GET.get('course_id'),
|
||||
'course_id': course_id,
|
||||
'enrollment_action': request.GET.get('enrollment_action'),
|
||||
# Bool injected into JS to submit form if we're inside a running third-
|
||||
# party auth pipeline; distinct from the actual instance of the running
|
||||
# pipeline, if any.
|
||||
'pipeline_running': 'true' if pipeline.running(request) else 'false',
|
||||
'pipeline_url': auth_pipeline_urls(pipeline.AUTH_ENTRY_LOGIN, course_id=course_id),
|
||||
'platform_name': microsite.get_value(
|
||||
'platform_name',
|
||||
settings.PLATFORM_NAME
|
||||
@@ -381,12 +384,15 @@ def register_user(request, extra_context=None):
|
||||
# and registration is disabled.
|
||||
return external_auth.views.redirect_with_get('root', request.GET)
|
||||
|
||||
course_id = request.GET.get('course_id')
|
||||
|
||||
context = {
|
||||
'course_id': request.GET.get('course_id'),
|
||||
'course_id': course_id,
|
||||
'email': '',
|
||||
'enrollment_action': request.GET.get('enrollment_action'),
|
||||
'name': '',
|
||||
'running_pipeline': None,
|
||||
'pipeline_urls': auth_pipeline_urls(pipeline.AUTH_ENTRY_REGISTER, course_id=course_id),
|
||||
'platform_name': microsite.get_value(
|
||||
'platform_name',
|
||||
settings.PLATFORM_NAME
|
||||
@@ -395,10 +401,6 @@ def register_user(request, extra_context=None):
|
||||
'username': '',
|
||||
}
|
||||
|
||||
# We save this so, later on, we can determine what course motivated a user's signup
|
||||
# if they actually complete the registration process
|
||||
request.session['registration_course_id'] = context['course_id']
|
||||
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
|
||||
@@ -698,7 +700,10 @@ def _allow_donation(course_modes, course_id):
|
||||
True if the course is allowing donations.
|
||||
|
||||
"""
|
||||
return DonationConfiguration.current().enabled and not CourseMode.has_verified_mode(course_modes[course_id])
|
||||
donations_enabled = DonationConfiguration.current().enabled
|
||||
is_verified_mode = CourseMode.has_verified_mode(course_modes[course_id])
|
||||
has_payment_option = CourseMode.has_payment_options(course_id)
|
||||
return donations_enabled and not is_verified_mode and not has_payment_option
|
||||
|
||||
|
||||
def try_change_enrollment(request):
|
||||
@@ -796,14 +801,9 @@ def change_enrollment(request, check_access=True):
|
||||
|
||||
available_modes = CourseMode.modes_for_course_dict(course_id)
|
||||
|
||||
# Handle professional ed as a special case.
|
||||
# If professional ed is included in the list of available modes,
|
||||
# then do NOT automatically enroll the student (we want them to pay first!)
|
||||
# By convention, professional ed should be the *only* available course mode,
|
||||
# if it's included at all -- anything else is a misconfiguration. But if someone
|
||||
# messes up and adds an additional course mode, we err on the side of NOT
|
||||
# accidentally giving away free courses.
|
||||
if "professional" not in available_modes:
|
||||
# Check that auto enrollment is allowed for this course
|
||||
# (= the course is NOT behind a paywall)
|
||||
if CourseMode.can_auto_enroll(course_id):
|
||||
# Enroll the user using the default mode (honor)
|
||||
# We're assuming that users of the course enrollment table
|
||||
# will NOT try to look up the course enrollment model
|
||||
@@ -819,7 +819,7 @@ def change_enrollment(request, check_access=True):
|
||||
# then send the user to the choose your track page.
|
||||
# (In the case of professional ed, this will redirect to a page that
|
||||
# funnels users directly into the verification / payment flow)
|
||||
if len(available_modes) > 1 or "professional" in available_modes:
|
||||
if CourseMode.has_verified_mode(available_modes):
|
||||
return HttpResponse(
|
||||
reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)})
|
||||
)
|
||||
@@ -900,6 +900,7 @@ def accounts_login(request):
|
||||
|
||||
context = {
|
||||
'pipeline_running': 'false',
|
||||
'pipeline_url': auth_pipeline_urls(pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to),
|
||||
'platform_name': settings.PLATFORM_NAME,
|
||||
}
|
||||
return render_to_response('login.html', context)
|
||||
@@ -1051,14 +1052,12 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
|
||||
'username': username,
|
||||
})
|
||||
|
||||
# If the user entered the flow via a specific course page, we track that
|
||||
registration_course_id = request.session.get('registration_course_id')
|
||||
analytics.track(
|
||||
user.id,
|
||||
"edx.bi.user.account.authenticated",
|
||||
{
|
||||
'category': "conversion",
|
||||
'label': registration_course_id,
|
||||
'label': request.POST.get('course_id'),
|
||||
'provider': None
|
||||
},
|
||||
context={
|
||||
@@ -1067,7 +1066,6 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
|
||||
}
|
||||
}
|
||||
)
|
||||
request.session['registration_course_id'] = None
|
||||
|
||||
if user is not None and user.is_active:
|
||||
try:
|
||||
@@ -1095,25 +1093,9 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
|
||||
"redirect_url": redirect_url,
|
||||
})
|
||||
|
||||
# set the login cookie for the edx marketing site
|
||||
# we want this cookie to be accessed via javascript
|
||||
# so httponly is set to None
|
||||
|
||||
if request.session.get_expire_at_browser_close():
|
||||
max_age = None
|
||||
expires = None
|
||||
else:
|
||||
max_age = request.session.get_expiry_age()
|
||||
expires_time = time.time() + max_age
|
||||
expires = cookie_date(expires_time)
|
||||
|
||||
response.set_cookie(
|
||||
settings.EDXMKTG_COOKIE_NAME, 'true', max_age=max_age,
|
||||
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
path='/', secure=None, httponly=None,
|
||||
)
|
||||
|
||||
return response
|
||||
# Ensure that the external marketing site can
|
||||
# detect that the user is logged in.
|
||||
return set_logged_in_cookie(request, response)
|
||||
|
||||
if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
|
||||
AUDIT_LOG.warning(u"Login failed - Account not active for user.id: {0}, resending activation".format(user.id))
|
||||
@@ -1128,6 +1110,7 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
|
||||
}) # TODO: this should be status code 400 # pylint: disable=fixme
|
||||
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def logout_user(request):
|
||||
"""
|
||||
@@ -1534,13 +1517,12 @@ def create_account(request, post_override=None): # pylint: disable-msg=too-many
|
||||
current_provider = provider.Registry.get_by_backend_name(running_pipeline.get('backend'))
|
||||
provider_name = current_provider.NAME
|
||||
|
||||
registration_course_id = request.session.get('registration_course_id')
|
||||
analytics.track(
|
||||
user.id,
|
||||
"edx.bi.user.account.registered",
|
||||
{
|
||||
'category': 'conversion',
|
||||
'label': registration_course_id,
|
||||
'label': request.POST.get('course_id'),
|
||||
'provider': provider_name
|
||||
},
|
||||
context={
|
||||
@@ -1549,7 +1531,6 @@ def create_account(request, post_override=None): # pylint: disable-msg=too-many
|
||||
}
|
||||
}
|
||||
)
|
||||
request.session['registration_course_id'] = None
|
||||
|
||||
create_comments_service_user(user)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from microsite_configuration import microsite
|
||||
def is_enabled():
|
||||
"""Check whether third party authentication has been enabled. """
|
||||
|
||||
# We do this imports internally to avoid initializing settings prematurely
|
||||
# We do this import internally to avoid initializing settings prematurely
|
||||
from django.conf import settings
|
||||
|
||||
return microsite.get_value(
|
||||
|
||||
@@ -59,6 +59,8 @@ See http://psa.matiasaguirre.net/docs/pipeline.html for more docs.
|
||||
|
||||
import random
|
||||
import string # pylint: disable-msg=deprecated-module
|
||||
from collections import OrderedDict
|
||||
import urllib
|
||||
import analytics
|
||||
from eventtracking import tracker
|
||||
|
||||
@@ -69,15 +71,40 @@ from social.apps.django_app.default import models
|
||||
from social.exceptions import AuthException
|
||||
from social.pipeline import partial
|
||||
|
||||
import student
|
||||
from shoppingcart.models import Order, PaidCourseRegistration # pylint: disable=F0401
|
||||
from shoppingcart.exceptions import ( # pylint: disable=F0401
|
||||
CourseDoesNotExistException,
|
||||
ItemAlreadyInCartException,
|
||||
AlreadyEnrolledInCourseException
|
||||
)
|
||||
from student.models import CourseEnrollment, CourseEnrollmentException
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from course_modes.models import CourseMode
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from . import provider
|
||||
|
||||
|
||||
# These are the query string params you can pass
|
||||
# to the URL that starts the authentication process.
|
||||
#
|
||||
# `AUTH_ENTRY_KEY` is required and indicates how the user
|
||||
# enters the authentication process.
|
||||
#
|
||||
# `AUTH_REDIRECT_KEY` provides an optional URL to redirect
|
||||
# to upon successful authentication
|
||||
# (if not provided, defaults to `_SOCIAL_AUTH_LOGIN_REDIRECT_URL`)
|
||||
#
|
||||
# `AUTH_ENROLL_COURSE_ID_KEY` provides the course ID that a student
|
||||
# is trying to enroll in, used to generate analytics events
|
||||
# and auto-enroll students.
|
||||
|
||||
AUTH_ENTRY_KEY = 'auth_entry'
|
||||
AUTH_REDIRECT_KEY = 'next'
|
||||
AUTH_ENROLL_COURSE_ID_KEY = 'enroll_course_id'
|
||||
|
||||
AUTH_ENTRY_DASHBOARD = 'dashboard'
|
||||
AUTH_ENTRY_LOGIN = 'login'
|
||||
AUTH_ENTRY_PROFILE = 'profile'
|
||||
@@ -192,15 +219,25 @@ def _get_enabled_provider_by_name(provider_name):
|
||||
return enabled_provider
|
||||
|
||||
|
||||
def _get_url(view_name, backend_name, auth_entry=None):
|
||||
def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, enroll_course_id=None):
|
||||
"""Creates a URL to hook into social auth endpoints."""
|
||||
kwargs = {'backend': backend_name}
|
||||
url = reverse(view_name, kwargs=kwargs)
|
||||
|
||||
query_params = OrderedDict()
|
||||
if auth_entry:
|
||||
url += '?%s=%s' % (AUTH_ENTRY_KEY, auth_entry)
|
||||
query_params[AUTH_ENTRY_KEY] = auth_entry
|
||||
|
||||
return url
|
||||
if redirect_url:
|
||||
query_params[AUTH_REDIRECT_KEY] = redirect_url
|
||||
|
||||
if enroll_course_id:
|
||||
query_params[AUTH_ENROLL_COURSE_ID_KEY] = enroll_course_id
|
||||
|
||||
return u"{url}?{params}".format(
|
||||
url=url,
|
||||
params=urllib.urlencode(query_params)
|
||||
)
|
||||
|
||||
|
||||
def get_complete_url(backend_name):
|
||||
@@ -241,7 +278,7 @@ def get_disconnect_url(provider_name):
|
||||
return _get_url('social:disconnect', enabled_provider.BACKEND_CLASS.name)
|
||||
|
||||
|
||||
def get_login_url(provider_name, auth_entry):
|
||||
def get_login_url(provider_name, auth_entry, redirect_url=None, enroll_course_id=None):
|
||||
"""Gets the login URL for the endpoint that kicks off auth with a provider.
|
||||
|
||||
Args:
|
||||
@@ -251,6 +288,13 @@ def get_login_url(provider_name, auth_entry):
|
||||
for the auth pipeline. Used by the pipeline for later branching.
|
||||
Must be one of _AUTH_ENTRY_CHOICES.
|
||||
|
||||
Keyword Args:
|
||||
redirect_url (string): If provided, redirect to this URL at the end
|
||||
of the authentication process.
|
||||
|
||||
enroll_course_id (string): If provided, auto-enroll the user in this
|
||||
course upon successful authentication.
|
||||
|
||||
Returns:
|
||||
String. URL that starts the auth pipeline for a provider.
|
||||
|
||||
@@ -259,7 +303,13 @@ def get_login_url(provider_name, auth_entry):
|
||||
"""
|
||||
assert auth_entry in _AUTH_ENTRY_CHOICES
|
||||
enabled_provider = _get_enabled_provider_by_name(provider_name)
|
||||
return _get_url('social:begin', enabled_provider.BACKEND_CLASS.name, auth_entry=auth_entry)
|
||||
return _get_url(
|
||||
'social:begin',
|
||||
enabled_provider.BACKEND_CLASS.name,
|
||||
auth_entry=auth_entry,
|
||||
redirect_url=redirect_url,
|
||||
enroll_course_id=enroll_course_id
|
||||
)
|
||||
|
||||
|
||||
def get_duplicate_provider(messages):
|
||||
@@ -423,8 +473,54 @@ def redirect_to_supplementary_form(
|
||||
if is_register_2 and user_unset:
|
||||
return redirect(reverse(AUTH_ENTRY_REGISTER_2))
|
||||
|
||||
|
||||
@partial.partial
|
||||
def login_analytics(*args, **kwargs):
|
||||
def set_logged_in_cookie(backend=None, user=None, request=None, *args, **kwargs):
|
||||
"""This pipeline step sets the "logged in" cookie for authenticated users.
|
||||
|
||||
Some installations have a marketing site front-end separate from
|
||||
edx-platform. Those installations sometimes display different
|
||||
information for logged in versus anonymous users (e.g. a link
|
||||
to the student dashboard instead of the login page.)
|
||||
|
||||
Since social auth uses Django's native `login()` method, it bypasses
|
||||
our usual login view that sets this cookie. For this reason, we need
|
||||
to set the cookie ourselves within the pipeline.
|
||||
|
||||
The procedure for doing this is a little strange. On the one hand,
|
||||
we need to send a response to the user in order to set the cookie.
|
||||
On the other hand, we don't want to drop the user out of the pipeline.
|
||||
|
||||
For this reason, we send a redirect back to the "complete" URL,
|
||||
so users immediately re-enter the pipeline. The redirect response
|
||||
contains a header that sets the logged in cookie.
|
||||
|
||||
If the user is not logged in, or the logged in cookie is already set,
|
||||
the function returns `None`, indicating that control should pass
|
||||
to the next pipeline step.
|
||||
|
||||
"""
|
||||
if user is not None and user.is_authenticated():
|
||||
if request is not None:
|
||||
# Check that the cookie isn't already set.
|
||||
# This ensures that we allow the user to continue to the next
|
||||
# pipeline step once he/she has the cookie set by this step.
|
||||
has_cookie = student.helpers.is_logged_in_cookie_set(request)
|
||||
if not has_cookie:
|
||||
try:
|
||||
redirect_url = get_complete_url(backend.name)
|
||||
except ValueError:
|
||||
# If for some reason we can't get the URL, just skip this step
|
||||
# This may be overly paranoid, but it's far more important that
|
||||
# the user log in successfully than that the cookie is set.
|
||||
pass
|
||||
else:
|
||||
response = redirect(redirect_url)
|
||||
return student.helpers.set_logged_in_cookie(request, response)
|
||||
|
||||
|
||||
@partial.partial
|
||||
def login_analytics(strategy, *args, **kwargs):
|
||||
""" Sends login info to Segment.io """
|
||||
event_name = None
|
||||
|
||||
@@ -447,14 +543,13 @@ def login_analytics(*args, **kwargs):
|
||||
event_name = action_to_event_name[action]
|
||||
|
||||
if event_name is not None:
|
||||
registration_course_id = kwargs['request'].session.get('registration_course_id')
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
analytics.track(
|
||||
kwargs['user'].id,
|
||||
event_name,
|
||||
{
|
||||
'category': "conversion",
|
||||
'label': registration_course_id,
|
||||
'label': strategy.session_get('enroll_course_id'),
|
||||
'provider': getattr(kwargs['backend'], 'name')
|
||||
},
|
||||
context={
|
||||
@@ -464,21 +559,54 @@ def login_analytics(*args, **kwargs):
|
||||
}
|
||||
)
|
||||
|
||||
#@partial.partial
|
||||
def change_enrollment(*args, **kwargs):
|
||||
|
||||
@partial.partial
|
||||
def change_enrollment(strategy, user=None, *args, **kwargs):
|
||||
"""Enroll a user in a course.
|
||||
|
||||
If a user entered the authentication flow when trying to enroll
|
||||
in a course, then attempt to enroll the user.
|
||||
We will try to do this if the pipeline was started with the
|
||||
querystring param `enroll_course_id`.
|
||||
|
||||
In the following cases, we can't enroll the user:
|
||||
* The course does not have an honor mode.
|
||||
* The course has an honor mode with a minimum price.
|
||||
* The course is not yet open for enrollment.
|
||||
* The course does not exist.
|
||||
|
||||
If we can't enroll the user now, then skip this step.
|
||||
For paid courses, users will be redirected to the payment flow
|
||||
upon completion of the authentication pipeline
|
||||
(configured using the ?next parameter to the third party auth login url).
|
||||
|
||||
"""
|
||||
If the user accessed the third party auth flow after trying to register for
|
||||
a course, we automatically log them into that course.
|
||||
"""
|
||||
if kwargs['strategy'].session_get('registration_course_id'):
|
||||
try:
|
||||
CourseEnrollment.enroll(
|
||||
kwargs['user'],
|
||||
SlashSeparatedCourseKey.from_deprecated_string(
|
||||
kwargs['strategy'].session_get('registration_course_id')
|
||||
)
|
||||
)
|
||||
except CourseEnrollmentException:
|
||||
pass
|
||||
except Exception, e:
|
||||
logger.exception(e)
|
||||
enroll_course_id = strategy.session_get('enroll_course_id')
|
||||
if enroll_course_id:
|
||||
course_id = CourseKey.from_string(enroll_course_id)
|
||||
modes = CourseMode.modes_for_course_dict(course_id)
|
||||
if CourseMode.can_auto_enroll(course_id, modes_dict=modes):
|
||||
try:
|
||||
CourseEnrollment.enroll(user, course_id, check_access=True)
|
||||
except CourseEnrollmentException:
|
||||
pass
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
|
||||
# Handle white-label courses as a special case
|
||||
# If a course is white-label, we should add it to the shopping cart.
|
||||
elif CourseMode.is_white_label(course_id, modes_dict=modes):
|
||||
try:
|
||||
cart = Order.get_cart_for_user(user)
|
||||
PaidCourseRegistration.add_to_order(cart, course_id)
|
||||
except (
|
||||
CourseDoesNotExistException,
|
||||
ItemAlreadyInCartException,
|
||||
AlreadyEnrolledInCourseException
|
||||
):
|
||||
pass
|
||||
# It's more important to complete login than to
|
||||
# ensure that the course was added to the shopping cart.
|
||||
# Log errors, but don't stop the authentication pipeline.
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
|
||||
@@ -46,7 +46,7 @@ If true, it:
|
||||
from . import provider
|
||||
|
||||
|
||||
_FIELDS_STORED_IN_SESSION = ['auth_entry']
|
||||
_FIELDS_STORED_IN_SESSION = ['auth_entry', 'next', 'enroll_course_id']
|
||||
_MIDDLEWARE_CLASSES = (
|
||||
'third_party_auth.middleware.ExceptionMiddleware',
|
||||
)
|
||||
@@ -116,6 +116,7 @@ def _set_global_settings(django_settings):
|
||||
'social.pipeline.social_auth.associate_user',
|
||||
'social.pipeline.social_auth.load_extra_data',
|
||||
'social.pipeline.user.user_details',
|
||||
'third_party_auth.pipeline.set_logged_in_cookie',
|
||||
'third_party_auth.pipeline.login_analytics',
|
||||
'third_party_auth.pipeline.change_enrollment',
|
||||
)
|
||||
|
||||
@@ -394,6 +394,19 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
"""Gets a user by email, using the given strategy."""
|
||||
return strategy.storage.user.user_model().objects.get(email=email)
|
||||
|
||||
def assert_logged_in_cookie_redirect(self, response):
|
||||
"""Verify that the user was redirected in order to set the logged in cookie. """
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(
|
||||
response["Location"],
|
||||
pipeline.get_complete_url(self.PROVIDER_CLASS.BACKEND_CLASS.name)
|
||||
)
|
||||
self.assertEqual(response.cookies[django_settings.EDXMKTG_COOKIE_NAME].value, 'true')
|
||||
|
||||
def set_logged_in_cookie(self, request):
|
||||
"""Simulate setting the marketing site cookie on the request. """
|
||||
request.COOKIES[django_settings.EDXMKTG_COOKIE_NAME] = 'true'
|
||||
|
||||
# Actual tests, executed once per child.
|
||||
|
||||
def test_canceling_authentication_redirects_to_login_when_auth_entry_login(self):
|
||||
@@ -430,6 +443,16 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
self.assert_dashboard_response_looks_correct(student_views.dashboard(request), request.user, linked=False)
|
||||
self.assert_social_auth_does_not_exist_for_user(request.user, strategy)
|
||||
|
||||
# We should be redirected back to the complete page, setting
|
||||
# the "logged in" cookie for the marketing site.
|
||||
self.assert_logged_in_cookie_redirect(actions.do_complete(
|
||||
request.social_strategy, social_views._do_login, request.user, None, # pylint: disable-msg=protected-access
|
||||
redirect_field_name=auth.REDIRECT_FIELD_NAME
|
||||
))
|
||||
|
||||
# Set the cookie and try again
|
||||
self.set_logged_in_cookie(request)
|
||||
|
||||
# Fire off the auth pipeline to link.
|
||||
self.assert_redirect_to_dashboard_looks_correct(actions.do_complete(
|
||||
request.social_strategy, social_views._do_login, request.user, None, # pylint: disable-msg=protected-access
|
||||
@@ -449,6 +472,9 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
strategy, 'user@example.com', 'password', self.get_username())
|
||||
self.assert_social_auth_exists_for_user(user, strategy)
|
||||
|
||||
# We're already logged in, so simulate that the cookie is set correctly
|
||||
self.set_logged_in_cookie(request)
|
||||
|
||||
# Instrument the pipeline to get to the dashboard with the full
|
||||
# expected state.
|
||||
self.client.get(
|
||||
@@ -561,6 +587,17 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
# redirects to /auth/complete. In the browser ajax handlers will
|
||||
# redirect the user to the dashboard; we invoke it manually here.
|
||||
self.assert_json_success_response_looks_correct(student_views.login_user(strategy.request))
|
||||
|
||||
# We should be redirected back to the complete page, setting
|
||||
# the "logged in" cookie for the marketing site.
|
||||
self.assert_logged_in_cookie_redirect(actions.do_complete(
|
||||
request.social_strategy, social_views._do_login, request.user, None, # pylint: disable-msg=protected-access
|
||||
redirect_field_name=auth.REDIRECT_FIELD_NAME
|
||||
))
|
||||
|
||||
# Set the cookie and try again
|
||||
self.set_logged_in_cookie(request)
|
||||
|
||||
self.assert_redirect_to_dashboard_looks_correct(
|
||||
actions.do_complete(strategy, social_views._do_login, user=user))
|
||||
self.assert_dashboard_response_looks_correct(student_views.dashboard(request), user)
|
||||
@@ -652,6 +689,16 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
|
||||
# social auth.
|
||||
self.assert_social_auth_does_not_exist_for_user(created_user, strategy)
|
||||
|
||||
# We should be redirected back to the complete page, setting
|
||||
# the "logged in" cookie for the marketing site.
|
||||
self.assert_logged_in_cookie_redirect(actions.do_complete(
|
||||
request.social_strategy, social_views._do_login, request.user, None, # pylint: disable-msg=protected-access
|
||||
redirect_field_name=auth.REDIRECT_FIELD_NAME
|
||||
))
|
||||
|
||||
# Set the cookie and try again
|
||||
self.set_logged_in_cookie(request)
|
||||
|
||||
# Pick the pipeline back up. This will create the account association
|
||||
# and send the user to the dashboard, where the association will be
|
||||
# displayed.
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Tests for the change enrollment step of the pipeline. """
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
import ddt
|
||||
import pytz
|
||||
from third_party_auth import pipeline
|
||||
from shoppingcart.models import Order, PaidCourseRegistration # pylint: disable=F0401
|
||||
from social.apps.django_app import utils as social_utils
|
||||
from django.conf import settings
|
||||
from django.contrib.sessions.backends import cache
|
||||
from django.test import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from student.tests.factories import UserFactory, CourseModeFactory
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, mixed_store_config
|
||||
)
|
||||
|
||||
|
||||
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
|
||||
|
||||
THIRD_PARTY_AUTH_CONFIGURED = (
|
||||
settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and
|
||||
getattr(settings, 'THIRD_PARTY_AUTH', {})
|
||||
)
|
||||
|
||||
|
||||
@unittest.skipUnless(THIRD_PARTY_AUTH_CONFIGURED, "Third party auth must be configured")
|
||||
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
|
||||
@ddt.ddt
|
||||
class PipelineEnrollmentTest(ModuleStoreTestCase):
|
||||
"""Test that the pipeline auto-enrolls students upon successful authentication. """
|
||||
|
||||
BACKEND_NAME = "google-oauth2"
|
||||
|
||||
def setUp(self):
|
||||
"""Create a test course and user. """
|
||||
super(PipelineEnrollmentTest, self).setUp()
|
||||
self.course = CourseFactory.create()
|
||||
self.user = UserFactory.create()
|
||||
|
||||
@ddt.data(
|
||||
([], "honor"),
|
||||
(["honor", "verified", "audit"], "honor"),
|
||||
(["professional"], None)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_auto_enroll_step(self, course_modes, enrollment_mode):
|
||||
# Create the course modes for the test case
|
||||
for mode_slug in course_modes:
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=mode_slug,
|
||||
mode_display_name=mode_slug.capitalize()
|
||||
)
|
||||
|
||||
# Simulate the pipeline step, passing in a course ID
|
||||
# to indicate that the user was trying to enroll
|
||||
# when they started the auth process.
|
||||
strategy = self._fake_strategy()
|
||||
strategy.session_set('enroll_course_id', unicode(self.course.id))
|
||||
|
||||
result = pipeline.change_enrollment(strategy, 1, user=self.user) # pylint: disable=E1111,E1124
|
||||
self.assertEqual(result, {})
|
||||
|
||||
# Check that the user was or was not enrolled
|
||||
# (this will vary based on the course mode)
|
||||
if enrollment_mode is not None:
|
||||
actual_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
|
||||
self.assertTrue(is_active)
|
||||
self.assertEqual(actual_mode, enrollment_mode)
|
||||
else:
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
def test_add_white_label_to_cart(self):
|
||||
# Create a white label course (honor with a minimum price)
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug="honor",
|
||||
mode_display_name="Honor",
|
||||
min_price=100
|
||||
)
|
||||
|
||||
# Simulate the pipeline step for enrolling in this course
|
||||
strategy = self._fake_strategy()
|
||||
strategy.session_set('enroll_course_id', unicode(self.course.id))
|
||||
result = pipeline.change_enrollment(strategy, 1, user=self.user) # pylint: disable=E1111,E1124
|
||||
self.assertEqual(result, {})
|
||||
|
||||
# Expect that the uesr is NOT enrolled in the course
|
||||
# because the user has not yet paid
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
# Expect that the course was added to the shopping cart
|
||||
cart = Order.get_cart_for_user(self.user)
|
||||
self.assertTrue(cart.has_items(PaidCourseRegistration))
|
||||
order_item = PaidCourseRegistration.objects.get(order=cart)
|
||||
self.assertEqual(order_item.course_id, self.course.id)
|
||||
|
||||
def test_auto_enroll_not_accessible(self):
|
||||
# Set the course open date in the future
|
||||
tomorrow = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1)
|
||||
self.course.enrollment_start = tomorrow
|
||||
self.update_course(self.course, self.user.id)
|
||||
|
||||
# Finish authentication and try to auto-enroll
|
||||
# This should fail silently, with no exception
|
||||
strategy = self._fake_strategy()
|
||||
strategy.session_set('enroll_course_id', unicode(self.course.id))
|
||||
result = pipeline.change_enrollment(strategy, 1, user=self.user) # pylint: disable=E1111,E1124
|
||||
self.assertEqual(result, {})
|
||||
|
||||
# Verify that we were NOT enrolled
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
def test_no_course_id_skips_enroll(self):
|
||||
strategy = self._fake_strategy()
|
||||
result = pipeline.change_enrollment(strategy, 1, user=self.user) # pylint: disable=E1111,E1124
|
||||
self.assertEqual(result, {})
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
def _fake_strategy(self):
|
||||
"""Simulate the strategy passed to the pipeline step. """
|
||||
request = RequestFactory().get(pipeline.get_complete_url(self.BACKEND_NAME))
|
||||
request.user = self.user
|
||||
request.session = cache.SessionStore()
|
||||
|
||||
return social_utils.load_strategy(
|
||||
backend=self.BACKEND_NAME, request=request
|
||||
)
|
||||
174
common/lib/xmodule/xmodule/assetstore/__init__.py
Normal file
174
common/lib/xmodule/xmodule/assetstore/__init__.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
Classes representing asset & asset thumbnail metadata.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
from contracts import contract, new_contract
|
||||
from opaque_keys.edx.keys import CourseKey, AssetKey
|
||||
|
||||
new_contract('AssetKey', AssetKey)
|
||||
new_contract('datetime', datetime)
|
||||
new_contract('basestring', basestring)
|
||||
|
||||
|
||||
class IncorrectAssetIdType(Exception):
|
||||
"""
|
||||
Raised when the asset ID passed-in to create an AssetMetadata or
|
||||
AssetThumbnailMetadata is of the wrong type.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class AssetMetadata(object):
|
||||
"""
|
||||
Stores the metadata associated with a particular course asset. The asset metadata gets stored
|
||||
in the modulestore.
|
||||
"""
|
||||
|
||||
TOP_LEVEL_ATTRS = ['basename', 'internal_name', 'locked', 'contenttype', 'md5']
|
||||
EDIT_INFO_ATTRS = ['curr_version', 'prev_version', 'edited_by', 'edited_on']
|
||||
ALLOWED_ATTRS = TOP_LEVEL_ATTRS + EDIT_INFO_ATTRS
|
||||
|
||||
# All AssetMetadata objects should have AssetLocators with this type.
|
||||
ASSET_TYPE = 'asset'
|
||||
|
||||
@contract(asset_id='AssetKey', basename='basestring | None', internal_name='str | None', locked='bool | None', contenttype='basestring | None',
|
||||
md5='str | None', curr_version='str | None', prev_version='str | None', edited_by='int | None', edited_on='datetime | None')
|
||||
def __init__(self, asset_id,
|
||||
basename=None, internal_name=None,
|
||||
locked=None, contenttype=None, md5=None,
|
||||
curr_version=None, prev_version=None,
|
||||
edited_by=None, edited_on=None, field_decorator=None):
|
||||
"""
|
||||
Construct a AssetMetadata object.
|
||||
|
||||
Arguments:
|
||||
asset_id (AssetKey): Key identifying this particular asset.
|
||||
basename (str): Original path to file at asset upload time.
|
||||
internal_name (str): Name under which the file is stored internally.
|
||||
locked (bool): If True, only course participants can access the asset.
|
||||
contenttype (str): MIME type of the asset.
|
||||
curr_version (str): Current version of the asset.
|
||||
prev_version (str): Previous version of the asset.
|
||||
edited_by (str): Username of last user to upload this asset.
|
||||
edited_on (datetime): Datetime of last upload of this asset.
|
||||
field_decorator (function): used by strip_key to convert OpaqueKeys to the app's understanding
|
||||
"""
|
||||
if asset_id.asset_type != self.ASSET_TYPE:
|
||||
raise IncorrectAssetIdType()
|
||||
self.asset_id = asset_id if field_decorator is None else field_decorator(asset_id)
|
||||
self.basename = basename # Path w/o filename.
|
||||
self.internal_name = internal_name
|
||||
self.locked = locked
|
||||
self.contenttype = contenttype
|
||||
self.md5 = md5
|
||||
self.curr_version = curr_version
|
||||
self.prev_version = prev_version
|
||||
self.edited_by = edited_by
|
||||
self.edited_on = edited_on or datetime.now(pytz.utc)
|
||||
|
||||
def __repr__(self):
|
||||
return """AssetMetadata{!r}""".format((
|
||||
self.asset_id,
|
||||
self.basename, self.internal_name,
|
||||
self.locked, self.contenttype, self.md5,
|
||||
self.curr_version, self.prev_version,
|
||||
self.edited_by, self.edited_on
|
||||
))
|
||||
|
||||
def update(self, attr_dict):
|
||||
"""
|
||||
Set the attributes on the metadata. Ignore all those outside the known fields.
|
||||
|
||||
Arguments:
|
||||
attr_dict: Prop, val dictionary of all attributes to set.
|
||||
"""
|
||||
for attr, val in attr_dict.iteritems():
|
||||
if attr in self.ALLOWED_ATTRS:
|
||||
setattr(self, attr, val)
|
||||
|
||||
def to_mongo(self):
|
||||
"""
|
||||
Converts metadata properties into a MongoDB-storable dict.
|
||||
"""
|
||||
return {
|
||||
'filename': self.asset_id.path,
|
||||
'basename': self.basename,
|
||||
'internal_name': self.internal_name,
|
||||
'locked': self.locked,
|
||||
'contenttype': self.contenttype,
|
||||
'md5': self.md5,
|
||||
'edit_info': {
|
||||
'curr_version': self.curr_version,
|
||||
'prev_version': self.prev_version,
|
||||
'edited_by': self.edited_by,
|
||||
'edited_on': self.edited_on
|
||||
}
|
||||
}
|
||||
|
||||
@contract(asset_doc='dict | None')
|
||||
def from_mongo(self, asset_doc):
|
||||
"""
|
||||
Fill in all metadata fields from a MongoDB document.
|
||||
|
||||
The asset_id prop is initialized upon construction only.
|
||||
"""
|
||||
if asset_doc is None:
|
||||
return
|
||||
self.basename = asset_doc['basename']
|
||||
self.internal_name = asset_doc['internal_name']
|
||||
self.locked = asset_doc['locked']
|
||||
self.contenttype = asset_doc['contenttype']
|
||||
self.md5 = asset_doc['md5']
|
||||
edit_info = asset_doc['edit_info']
|
||||
self.curr_version = edit_info['curr_version']
|
||||
self.prev_version = edit_info['prev_version']
|
||||
self.edited_by = edit_info['edited_by']
|
||||
self.edited_on = edit_info['edited_on']
|
||||
|
||||
|
||||
class AssetThumbnailMetadata(object):
|
||||
"""
|
||||
Stores the metadata associated with the thumbnail of a course asset.
|
||||
"""
|
||||
|
||||
# All AssetThumbnailMetadata objects should have AssetLocators with this type.
|
||||
ASSET_TYPE = 'thumbnail'
|
||||
|
||||
@contract(asset_id='AssetKey', internal_name='str | unicode | None')
|
||||
def __init__(self, asset_id, internal_name=None, field_decorator=None):
|
||||
"""
|
||||
Construct a AssetThumbnailMetadata object.
|
||||
|
||||
Arguments:
|
||||
asset_id (AssetKey): Key identifying this particular asset.
|
||||
internal_name (str): Name under which the file is stored internally.
|
||||
"""
|
||||
if asset_id.asset_type != self.ASSET_TYPE:
|
||||
raise IncorrectAssetIdType()
|
||||
self.asset_id = asset_id if field_decorator is None else field_decorator(asset_id)
|
||||
self.internal_name = internal_name
|
||||
|
||||
def __repr__(self):
|
||||
return """AssetMetadata{!r}""".format((self.asset_id, self.internal_name))
|
||||
|
||||
def to_mongo(self):
|
||||
"""
|
||||
Converts metadata properties into a MongoDB-storable dict.
|
||||
"""
|
||||
return {
|
||||
'filename': self.asset_id.path,
|
||||
'internal_name': self.internal_name
|
||||
}
|
||||
|
||||
@contract(thumbnail_doc='dict | None')
|
||||
def from_mongo(self, thumbnail_doc):
|
||||
"""
|
||||
Fill in all metadata fields from a MongoDB document.
|
||||
|
||||
The asset_id prop is initialized upon construction only.
|
||||
"""
|
||||
if thumbnail_doc is None:
|
||||
return
|
||||
self.internal_name = thumbnail_doc['internal_name']
|
||||
@@ -29,6 +29,8 @@ from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
|
||||
from .fields import Timedelta, Date
|
||||
from django.utils.timezone import UTC
|
||||
from .util.duedate import get_extended_due_date
|
||||
from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER
|
||||
from django.conf import settings
|
||||
|
||||
log = logging.getLogger("edx.courseware")
|
||||
|
||||
@@ -63,9 +65,9 @@ class Randomization(String):
|
||||
"""
|
||||
def from_json(self, value):
|
||||
if value in ("", "true"):
|
||||
return "always"
|
||||
return RANDOMIZATION.ALWAYS
|
||||
elif value == "false":
|
||||
return "per_student"
|
||||
return RANDOMIZATION.PER_STUDENT
|
||||
return value
|
||||
|
||||
to_json = from_json
|
||||
@@ -103,15 +105,15 @@ class CapaFields(object):
|
||||
max_attempts = Integer(
|
||||
display_name=_("Maximum Attempts"),
|
||||
help=_("Defines the number of times a student can try to answer this problem. "
|
||||
"If the value is not set, infinite attempts are allowed."),
|
||||
"If the value is not set, infinite attempts are allowed."),
|
||||
values={"min": 0}, scope=Scope.settings
|
||||
)
|
||||
due = Date(help=_("Date that this problem is due by"), scope=Scope.settings)
|
||||
extended_due = Date(
|
||||
help=_("Date that this problem is due by for a particular student. This "
|
||||
"can be set by an instructor, and will override the global due "
|
||||
"date if it is set to a date that is later than the global due "
|
||||
"date."),
|
||||
"can be set by an instructor, and will override the global due "
|
||||
"date if it is set to a date that is later than the global due "
|
||||
"date."),
|
||||
default=None,
|
||||
scope=Scope.user_state,
|
||||
)
|
||||
@@ -122,36 +124,45 @@ class CapaFields(object):
|
||||
showanswer = String(
|
||||
display_name=_("Show Answer"),
|
||||
help=_("Defines when to show the answer to the problem. "
|
||||
"A default value can be set in Advanced Settings."),
|
||||
"A default value can be set in Advanced Settings."),
|
||||
scope=Scope.settings,
|
||||
default="finished",
|
||||
default=SHOWANSWER.FINISHED,
|
||||
values=[
|
||||
{"display_name": _("Always"), "value": "always"},
|
||||
{"display_name": _("Answered"), "value": "answered"},
|
||||
{"display_name": _("Attempted"), "value": "attempted"},
|
||||
{"display_name": _("Closed"), "value": "closed"},
|
||||
{"display_name": _("Finished"), "value": "finished"},
|
||||
{"display_name": _("Correct or Past Due"), "value": "correct_or_past_due"},
|
||||
{"display_name": _("Past Due"), "value": "past_due"},
|
||||
{"display_name": _("Never"), "value": "never"}]
|
||||
{"display_name": _("Always"), "value": SHOWANSWER.ALWAYS},
|
||||
{"display_name": _("Answered"), "value": SHOWANSWER.ANSWERED},
|
||||
{"display_name": _("Attempted"), "value": SHOWANSWER.ATTEMPTED},
|
||||
{"display_name": _("Closed"), "value": SHOWANSWER.CLOSED},
|
||||
{"display_name": _("Finished"), "value": SHOWANSWER.FINISHED},
|
||||
{"display_name": _("Correct or Past Due"), "value": SHOWANSWER.CORRECT_OR_PAST_DUE},
|
||||
{"display_name": _("Past Due"), "value": SHOWANSWER.PAST_DUE},
|
||||
{"display_name": _("Never"), "value": SHOWANSWER.NEVER}]
|
||||
)
|
||||
force_save_button = Boolean(
|
||||
help=_("Whether to force the save button to appear on the page"),
|
||||
scope=Scope.settings,
|
||||
default=False
|
||||
)
|
||||
reset_key = "DEFAULT_SHOW_RESET_BUTTON"
|
||||
default_reset_button = getattr(settings, reset_key) if hasattr(settings, reset_key) else False
|
||||
show_reset_button = Boolean(
|
||||
display_name=_("Show Reset Button"),
|
||||
help=_("Determines whether a 'Reset' button is shown so the user may reset their answer. "
|
||||
"A default value can be set in Advanced Settings."),
|
||||
scope=Scope.settings,
|
||||
default=default_reset_button
|
||||
)
|
||||
rerandomize = Randomization(
|
||||
display_name=_("Randomization"),
|
||||
help=_("Defines how often inputs are randomized when a student loads the problem. "
|
||||
"This setting only applies to problems that can have randomly generated numeric values. "
|
||||
"A default value can be set in Advanced Settings."),
|
||||
default="never",
|
||||
"This setting only applies to problems that can have randomly generated numeric values. "
|
||||
"A default value can be set in Advanced Settings."),
|
||||
default=RANDOMIZATION.NEVER,
|
||||
scope=Scope.settings,
|
||||
values=[
|
||||
{"display_name": _("Always"), "value": "always"},
|
||||
{"display_name": _("On Reset"), "value": "onreset"},
|
||||
{"display_name": _("Never"), "value": "never"},
|
||||
{"display_name": _("Per Student"), "value": "per_student"}
|
||||
{"display_name": _("Always"), "value": RANDOMIZATION.ALWAYS},
|
||||
{"display_name": _("On Reset"), "value": RANDOMIZATION.ONRESET},
|
||||
{"display_name": _("Never"), "value": RANDOMIZATION.NEVER},
|
||||
{"display_name": _("Per Student"), "value": RANDOMIZATION.PER_STUDENT}
|
||||
]
|
||||
)
|
||||
data = String(help=_("XML data for the problem"), scope=Scope.content, default="<problem></problem>")
|
||||
@@ -170,7 +181,7 @@ class CapaFields(object):
|
||||
weight = Float(
|
||||
display_name=_("Problem Weight"),
|
||||
help=_("Defines the number of points each problem is worth. "
|
||||
"If the value is not set, each response field in the problem is worth one point."),
|
||||
"If the value is not set, each response field in the problem is worth one point."),
|
||||
values={"min": 0, "step": .1},
|
||||
scope=Scope.settings
|
||||
)
|
||||
@@ -254,7 +265,7 @@ class CapaMixin(CapaFields):
|
||||
tb=cgi.escape(
|
||||
u''.join(['Traceback (most recent call last):\n'] +
|
||||
traceback.format_tb(sys.exc_info()[2])))
|
||||
)
|
||||
)
|
||||
# create a dummy problem with error message instead of failing
|
||||
problem_text = (u'<problem><text><span class="inline-error">'
|
||||
u'Problem {url} has an error:</span>{msg}</text></problem>'.format(
|
||||
@@ -274,9 +285,9 @@ class CapaMixin(CapaFields):
|
||||
"""
|
||||
Choose a new seed.
|
||||
"""
|
||||
if self.rerandomize == 'never':
|
||||
if self.rerandomize == RANDOMIZATION.NEVER:
|
||||
self.seed = 1
|
||||
elif self.rerandomize == "per_student" and hasattr(self.runtime, 'seed'):
|
||||
elif self.rerandomize == RANDOMIZATION.PER_STUDENT and hasattr(self.runtime, 'seed'):
|
||||
# see comment on randomization_bin
|
||||
self.seed = randomization_bin(self.runtime.seed, unicode(self.location).encode('utf-8'))
|
||||
else:
|
||||
@@ -446,7 +457,7 @@ class CapaMixin(CapaFields):
|
||||
"""
|
||||
Return True/False to indicate whether to show the "Check" button.
|
||||
"""
|
||||
submitted_without_reset = (self.is_submitted() and self.rerandomize == "always")
|
||||
submitted_without_reset = (self.is_submitted() and self.rerandomize == RANDOMIZATION.ALWAYS)
|
||||
|
||||
# If the problem is closed (past due / too many attempts)
|
||||
# then we do NOT show the "check" button
|
||||
@@ -463,19 +474,20 @@ class CapaMixin(CapaFields):
|
||||
"""
|
||||
is_survey_question = (self.max_attempts == 0)
|
||||
|
||||
if self.rerandomize in ["always", "onreset"]:
|
||||
# If the problem is closed (and not a survey question with max_attempts==0),
|
||||
# then do NOT show the reset button.
|
||||
if (self.closed() and not is_survey_question):
|
||||
return False
|
||||
|
||||
# If the problem is closed (and not a survey question with max_attempts==0),
|
||||
# then do NOT show the reset button.
|
||||
# If the problem hasn't been submitted yet, then do NOT show
|
||||
# the reset button.
|
||||
if (self.closed() and not is_survey_question) or not self.is_submitted():
|
||||
# Button only shows up for randomized problems if the question has been submitted
|
||||
if self.rerandomize in [RANDOMIZATION.ALWAYS, RANDOMIZATION.ONRESET] and self.is_submitted():
|
||||
return True
|
||||
else:
|
||||
# Do NOT show the button if the problem is correct
|
||||
if self.is_correct():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
# Only randomized problems need a "reset" button
|
||||
else:
|
||||
return False
|
||||
return self.show_reset_button
|
||||
|
||||
def should_show_save_button(self):
|
||||
"""
|
||||
@@ -489,7 +501,7 @@ class CapaMixin(CapaFields):
|
||||
return not self.closed()
|
||||
else:
|
||||
is_survey_question = (self.max_attempts == 0)
|
||||
needs_reset = self.is_submitted() and self.rerandomize == "always"
|
||||
needs_reset = self.is_submitted() and self.rerandomize == RANDOMIZATION.ALWAYS
|
||||
|
||||
# If the student has unlimited attempts, and their answers
|
||||
# are not randomized, then we do not need a save button
|
||||
@@ -503,7 +515,7 @@ class CapaMixin(CapaFields):
|
||||
# In those cases. the if statement below is false,
|
||||
# and the save button can still be displayed.
|
||||
#
|
||||
if self.max_attempts is None and self.rerandomize != "always":
|
||||
if self.max_attempts is None and self.rerandomize != RANDOMIZATION.ALWAYS:
|
||||
return False
|
||||
|
||||
# If the problem is closed (and not a survey question with max_attempts==0),
|
||||
@@ -697,28 +709,28 @@ class CapaMixin(CapaFields):
|
||||
"""
|
||||
if self.showanswer == '':
|
||||
return False
|
||||
elif self.showanswer == "never":
|
||||
elif self.showanswer == SHOWANSWER.NEVER:
|
||||
return False
|
||||
elif self.runtime.user_is_staff:
|
||||
# This is after the 'never' check because admins can see the answer
|
||||
# unless the problem explicitly prevents it
|
||||
return True
|
||||
elif self.showanswer == 'attempted':
|
||||
elif self.showanswer == SHOWANSWER.ATTEMPTED:
|
||||
return self.attempts > 0
|
||||
elif self.showanswer == 'answered':
|
||||
elif self.showanswer == SHOWANSWER.ANSWERED:
|
||||
# NOTE: this is slightly different from 'attempted' -- resetting the problems
|
||||
# makes lcp.done False, but leaves attempts unchanged.
|
||||
return self.lcp.done
|
||||
elif self.showanswer == 'closed':
|
||||
elif self.showanswer == SHOWANSWER.CLOSED:
|
||||
return self.closed()
|
||||
elif self.showanswer == 'finished':
|
||||
elif self.showanswer == SHOWANSWER.FINISHED:
|
||||
return self.closed() or self.is_correct()
|
||||
|
||||
elif self.showanswer == 'correct_or_past_due':
|
||||
elif self.showanswer == SHOWANSWER.CORRECT_OR_PAST_DUE:
|
||||
return self.is_correct() or self.is_past_due()
|
||||
elif self.showanswer == 'past_due':
|
||||
elif self.showanswer == SHOWANSWER.PAST_DUE:
|
||||
return self.is_past_due()
|
||||
elif self.showanswer == 'always':
|
||||
elif self.showanswer == SHOWANSWER.ALWAYS:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -952,7 +964,7 @@ class CapaMixin(CapaFields):
|
||||
raise NotFoundError(_("Problem is closed."))
|
||||
|
||||
# Problem submitted. Student should reset before checking again
|
||||
if self.done and self.rerandomize == "always":
|
||||
if self.done and self.rerandomize == RANDOMIZATION.ALWAYS:
|
||||
event_info['failure'] = 'unreset'
|
||||
self.track_function_unmask('problem_check_fail', event_info)
|
||||
if dog_stats_api:
|
||||
@@ -1206,7 +1218,7 @@ class CapaMixin(CapaFields):
|
||||
# was presented to the user, with values interpolated etc, but that can be done
|
||||
# later if necessary.
|
||||
variant = ''
|
||||
if self.rerandomize != 'never':
|
||||
if self.rerandomize != RANDOMIZATION.NEVER:
|
||||
variant = self.seed
|
||||
|
||||
is_correct = correct_map.is_correct(input_id)
|
||||
@@ -1333,7 +1345,7 @@ class CapaMixin(CapaFields):
|
||||
|
||||
# Problem submitted. Student should reset before saving
|
||||
# again.
|
||||
if self.done and self.rerandomize == "always":
|
||||
if self.done and self.rerandomize == RANDOMIZATION.ALWAYS:
|
||||
event_info['failure'] = 'done'
|
||||
self.track_function_unmask('save_problem_fail', event_info)
|
||||
return {
|
||||
@@ -1357,7 +1369,7 @@ class CapaMixin(CapaFields):
|
||||
def reset_problem(self, _data):
|
||||
"""
|
||||
Changes problem state to unfinished -- removes student answers,
|
||||
and causes problem to rerender itself.
|
||||
Causes problem to rerender itself if randomization is enabled.
|
||||
|
||||
Returns a dictionary of the form:
|
||||
{'success': True/False,
|
||||
@@ -1380,7 +1392,7 @@ class CapaMixin(CapaFields):
|
||||
'error': _("Problem is closed."),
|
||||
}
|
||||
|
||||
if not self.done:
|
||||
if not self.is_submitted():
|
||||
event_info['failure'] = 'not_done'
|
||||
self.track_function_unmask('reset_problem_fail', event_info)
|
||||
return {
|
||||
@@ -1389,7 +1401,7 @@ class CapaMixin(CapaFields):
|
||||
'error': _("Refresh the page and make an attempt before resetting."),
|
||||
}
|
||||
|
||||
if self.rerandomize in ["always", "onreset"]:
|
||||
if self.is_submitted() and self.rerandomize in [RANDOMIZATION.ALWAYS, RANDOMIZATION.ONRESET]:
|
||||
# Reset random number generator seed.
|
||||
self.choose_new_seed()
|
||||
|
||||
|
||||
28
common/lib/xmodule/xmodule/capa_base_constants.py
Normal file
28
common/lib/xmodule/xmodule/capa_base_constants.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Constants for capa_base problems
|
||||
"""
|
||||
|
||||
|
||||
class SHOWANSWER:
|
||||
"""
|
||||
Constants for when to show answer
|
||||
"""
|
||||
ALWAYS = "always"
|
||||
ANSWERED = "answered"
|
||||
ATTEMPTED = "attempted"
|
||||
CLOSED = "closed"
|
||||
FINISHED = "finished"
|
||||
CORRECT_OR_PAST_DUE = "correct_or_past_due"
|
||||
PAST_DUE = "past_due"
|
||||
NEVER = "never"
|
||||
|
||||
|
||||
class RANDOMIZATION:
|
||||
"""
|
||||
Constants for problem randomization
|
||||
"""
|
||||
ALWAYS = "always"
|
||||
ONRESET = "onreset"
|
||||
NEVER = "never"
|
||||
PER_STUDENT = "per_student"
|
||||
@@ -954,10 +954,9 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
"""Return the course_id for this course"""
|
||||
return self.location.course_key
|
||||
|
||||
@property
|
||||
def start_date_text(self):
|
||||
def start_datetime_text(self, format_string="SHORT_DATE"):
|
||||
"""
|
||||
Returns the desired text corresponding the course's start date. Prefers .advertised_start,
|
||||
Returns the desired text corresponding the course's start date and time in UTC. Prefers .advertised_start,
|
||||
then falls back to .start
|
||||
"""
|
||||
i18n = self.runtime.service(self, "i18n")
|
||||
@@ -970,7 +969,9 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
if result is None:
|
||||
result = text.title()
|
||||
else:
|
||||
result = strftime(result, "SHORT_DATE")
|
||||
result = strftime(result, format_string)
|
||||
if format_string == "DATE_TIME":
|
||||
result = self._add_timezone_string(result)
|
||||
except ValueError:
|
||||
result = text.title()
|
||||
|
||||
@@ -984,7 +985,11 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
return _('TBD')
|
||||
else:
|
||||
when = self.advertised_start or self.start
|
||||
return strftime(when, "SHORT_DATE")
|
||||
|
||||
if format_string == "DATE_TIME":
|
||||
return self._add_timezone_string(strftime(when, format_string))
|
||||
|
||||
return strftime(when, format_string)
|
||||
|
||||
@property
|
||||
def start_date_is_still_default(self):
|
||||
@@ -994,10 +999,9 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
"""
|
||||
return self.advertised_start is None and self.start == CourseFields.start.default
|
||||
|
||||
@property
|
||||
def end_date_text(self):
|
||||
def end_datetime_text(self, format_string="SHORT_DATE"):
|
||||
"""
|
||||
Returns the end date for the course formatted as a string.
|
||||
Returns the end date or date_time for the course formatted as a string.
|
||||
|
||||
If the course does not have an end date set (course.end is None), an empty string will be returned.
|
||||
"""
|
||||
@@ -1005,7 +1009,14 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
return ''
|
||||
else:
|
||||
strftime = self.runtime.service(self, "i18n").strftime
|
||||
return strftime(self.end, "SHORT_DATE")
|
||||
date_time = strftime(self.end, format_string)
|
||||
return date_time if format_string == "SHORT_DATE" else self._add_timezone_string(date_time)
|
||||
|
||||
def _add_timezone_string(self, date_time):
|
||||
"""
|
||||
Adds 'UTC' string to the end of start/end date and time texts.
|
||||
"""
|
||||
return date_time + u" UTC"
|
||||
|
||||
@property
|
||||
def forum_posts_allowed(self):
|
||||
|
||||
@@ -397,6 +397,14 @@ function (HTML5Video, Resizer) {
|
||||
}
|
||||
);
|
||||
}
|
||||
// In Html5 mode if video speed is changed before playing in firefox and
|
||||
// changed speed is not '1.0' then manually trigger setPlaybackRate method.
|
||||
// In browsers other than firefox like safari user can set speed to '1.0'
|
||||
// if its not already set to '1.0' so in that case we don't have to
|
||||
// call 'setPlaybackRate'
|
||||
if (this.isHtml5Mode() && newSpeed != '1.0') {
|
||||
this.videoPlayer.player.setPlaybackRate(newSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,23 +12,30 @@ from uuid import uuid4
|
||||
from collections import namedtuple, defaultdict
|
||||
import collections
|
||||
from contextlib import contextmanager
|
||||
import functools
|
||||
import threading
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from contracts import contract, new_contract
|
||||
from xblock.plugin import default_select
|
||||
|
||||
from .exceptions import InvalidLocationError, InsufficientSpecificationError
|
||||
from xmodule.errortracker import make_error_tracker
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from xmodule.assetstore import AssetMetadata, AssetThumbnailMetadata
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey, AssetKey
|
||||
from opaque_keys.edx.locations import Location # For import backwards compatibility
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xblock.runtime import Mixologist
|
||||
from xblock.core import XBlock
|
||||
import functools
|
||||
import threading
|
||||
|
||||
log = logging.getLogger('edx.modulestore')
|
||||
|
||||
new_contract('CourseKey', CourseKey)
|
||||
new_contract('AssetKey', AssetKey)
|
||||
new_contract('AssetMetadata', AssetMetadata)
|
||||
new_contract('AssetThumbnailMetadata', AssetThumbnailMetadata)
|
||||
|
||||
|
||||
class ModuleStoreEnum(object):
|
||||
"""
|
||||
@@ -740,6 +747,9 @@ class ModuleStoreReadBase(BulkOperationsMixin, ModuleStoreRead):
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
"""
|
||||
Wraps a method to memoize results.
|
||||
"""
|
||||
if self.request_cache:
|
||||
cache_key = '&'.join([hashvalue(arg) for arg in args])
|
||||
if cache_key in self.request_cache.data.setdefault(func.__name__, {}):
|
||||
@@ -863,6 +873,276 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
|
||||
parent.children.append(item.location)
|
||||
self.update_item(parent, user_id)
|
||||
|
||||
def _find_course_assets(self, course_key):
|
||||
"""
|
||||
Base method to override.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _find_course_asset(self, course_key, filename, get_thumbnail=False):
|
||||
"""
|
||||
Internal; finds or creates course asset info -and- finds existing asset (or thumbnail) metadata.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): course identifier
|
||||
filename (str): filename of the asset or thumbnail
|
||||
get_thumbnail (bool): True gets thumbnail data, False gets asset data
|
||||
|
||||
Returns:
|
||||
Asset info for the course, index of asset/thumbnail in list (None if asset/thumbnail does not exist)
|
||||
"""
|
||||
course_assets = self._find_course_assets(course_key)
|
||||
|
||||
if get_thumbnail:
|
||||
all_assets = course_assets['thumbnails']
|
||||
else:
|
||||
all_assets = course_assets['assets']
|
||||
|
||||
# See if this asset already exists by checking the external_filename.
|
||||
# Studio doesn't currently support using multiple course assets with the same filename.
|
||||
# So use the filename as the unique identifier.
|
||||
for idx, asset in enumerate(all_assets):
|
||||
if asset['filename'] == filename:
|
||||
return course_assets, idx
|
||||
|
||||
return course_assets, None
|
||||
|
||||
def _save_asset_info(self, course_key, asset_metadata, user_id, thumbnail=False):
|
||||
"""
|
||||
Base method to over-ride in modulestore.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@contract(course_key='CourseKey', asset_metadata='AssetMetadata')
|
||||
def save_asset_metadata(self, course_key, asset_metadata, user_id):
|
||||
"""
|
||||
Saves the asset metadata for a particular course's asset.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): course identifier
|
||||
asset_metadata (AssetMetadata): data about the course asset data
|
||||
|
||||
Returns:
|
||||
True if metadata save was successful, else False
|
||||
"""
|
||||
return self._save_asset_info(course_key, asset_metadata, user_id, thumbnail=False)
|
||||
|
||||
@contract(course_key='CourseKey', asset_thumbnail_metadata='AssetThumbnailMetadata')
|
||||
def save_asset_thumbnail_metadata(self, course_key, asset_thumbnail_metadata, user_id):
|
||||
"""
|
||||
Saves the asset thumbnail metadata for a particular course asset's thumbnail.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): course identifier
|
||||
asset_thumbnail_metadata (AssetThumbnailMetadata): data about the course asset thumbnail
|
||||
|
||||
Returns:
|
||||
True if thumbnail metadata save was successful, else False
|
||||
"""
|
||||
return self._save_asset_info(course_key, asset_thumbnail_metadata, user_id, thumbnail=True)
|
||||
|
||||
@contract(asset_key='AssetKey')
|
||||
def _find_asset_info(self, asset_key, thumbnail=False, **kwargs):
|
||||
"""
|
||||
Find the info for a particular course asset/thumbnail.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): key containing original asset filename
|
||||
thumbnail (bool): True if finding thumbnail, False if finding asset metadata
|
||||
|
||||
Returns:
|
||||
asset/thumbnail metadata (AssetMetadata/AssetThumbnailMetadata) -or- None if not found
|
||||
"""
|
||||
course_assets, asset_idx = self._find_course_asset(asset_key.course_key, asset_key.path, thumbnail)
|
||||
if asset_idx is None:
|
||||
return None
|
||||
|
||||
if thumbnail:
|
||||
info = 'thumbnails'
|
||||
mdata = AssetThumbnailMetadata(asset_key, asset_key.path, **kwargs)
|
||||
else:
|
||||
info = 'assets'
|
||||
mdata = AssetMetadata(asset_key, asset_key.path, **kwargs)
|
||||
all_assets = course_assets[info]
|
||||
mdata.from_mongo(all_assets[asset_idx])
|
||||
return mdata
|
||||
|
||||
@contract(asset_key='AssetKey')
|
||||
def find_asset_metadata(self, asset_key, **kwargs):
|
||||
"""
|
||||
Find the metadata for a particular course asset.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): key containing original asset filename
|
||||
|
||||
Returns:
|
||||
asset metadata (AssetMetadata) -or- None if not found
|
||||
"""
|
||||
return self._find_asset_info(asset_key, thumbnail=False, **kwargs)
|
||||
|
||||
@contract(asset_key='AssetKey')
|
||||
def find_asset_thumbnail_metadata(self, asset_key, **kwargs):
|
||||
"""
|
||||
Find the metadata for a particular course asset.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): key containing original asset filename
|
||||
|
||||
Returns:
|
||||
asset metadata (AssetMetadata) -or- None if not found
|
||||
"""
|
||||
return self._find_asset_info(asset_key, thumbnail=True, **kwargs)
|
||||
|
||||
@contract(course_key='CourseKey', start='int | None', maxresults='int | None', sort='list | None', get_thumbnails='bool')
|
||||
def _get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=None, get_thumbnails=False, **kwargs):
|
||||
"""
|
||||
Returns a list of static asset (or thumbnail) metadata for a course.
|
||||
|
||||
Args:
|
||||
course_key (CourseKey): course identifier
|
||||
start (int): optional - start at this asset number
|
||||
maxresults (int): optional - return at most this many, -1 means no limit
|
||||
sort (array): optional - None means no sort
|
||||
(sort_by (str), sort_order (str))
|
||||
sort_by - one of 'uploadDate' or 'displayname'
|
||||
sort_order - one of 'ascending' or 'descending'
|
||||
get_thumbnails (bool): True if getting thumbnail metadata, else getting asset metadata
|
||||
|
||||
Returns:
|
||||
List of AssetMetadata or AssetThumbnailMetadata objects.
|
||||
"""
|
||||
course_assets = self._find_course_assets(course_key)
|
||||
if course_assets is None:
|
||||
# If no course assets are found, return None instead of empty list
|
||||
# to distinguish zero assets from "not able to retrieve assets".
|
||||
return None
|
||||
|
||||
if get_thumbnails:
|
||||
all_assets = course_assets.get('thumbnails', [])
|
||||
else:
|
||||
all_assets = course_assets.get('assets', [])
|
||||
|
||||
# DO_NEXT: Add start/maxresults/sort functionality as part of https://openedx.atlassian.net/browse/PLAT-74
|
||||
if start and maxresults and sort:
|
||||
pass
|
||||
|
||||
ret_assets = []
|
||||
for asset in all_assets:
|
||||
if get_thumbnails:
|
||||
thumb = AssetThumbnailMetadata(
|
||||
course_key.make_asset_key('thumbnail', asset['filename']),
|
||||
internal_name=asset['filename'], **kwargs
|
||||
)
|
||||
ret_assets.append(thumb)
|
||||
else:
|
||||
asset = AssetMetadata(
|
||||
course_key.make_asset_key('asset', asset['filename']),
|
||||
basename=asset['filename'],
|
||||
edited_on=asset['edit_info']['edited_on'],
|
||||
contenttype=asset['contenttype'],
|
||||
md5=str(asset['md5']), **kwargs
|
||||
)
|
||||
ret_assets.append(asset)
|
||||
return ret_assets
|
||||
|
||||
@contract(course_key='CourseKey', start='int | None', maxresults='int | None', sort='list | None')
|
||||
def get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=None, **kwargs):
|
||||
"""
|
||||
Returns a list of static assets for a course.
|
||||
By default all assets are returned, but start and maxresults can be provided to limit the query.
|
||||
|
||||
Args:
|
||||
course_key (CourseKey): course identifier
|
||||
start (int): optional - start at this asset number
|
||||
maxresults (int): optional - return at most this many, -1 means no limit
|
||||
sort (array): optional - None means no sort
|
||||
(sort_by (str), sort_order (str))
|
||||
sort_by - one of 'uploadDate' or 'displayname'
|
||||
sort_order - one of 'ascending' or 'descending'
|
||||
|
||||
Returns:
|
||||
List of AssetMetadata objects.
|
||||
"""
|
||||
return self._get_all_asset_metadata(course_key, start, maxresults, sort, get_thumbnails=False, **kwargs)
|
||||
|
||||
@contract(course_key='CourseKey')
|
||||
def get_all_asset_thumbnail_metadata(self, course_key, **kwargs):
|
||||
"""
|
||||
Returns a list of thumbnails for all course assets.
|
||||
|
||||
Args:
|
||||
course_key (CourseKey): course identifier
|
||||
|
||||
Returns:
|
||||
List of AssetThumbnailMetadata objects.
|
||||
"""
|
||||
return self._get_all_asset_metadata(course_key, get_thumbnails=True, **kwargs)
|
||||
|
||||
def set_asset_metadata_attrs(self, asset_key, attrs, user_id):
|
||||
"""
|
||||
Base method to over-ride in modulestore.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _delete_asset_data(self, asset_key, user_id, thumbnail=False):
|
||||
"""
|
||||
Base method to over-ride in modulestore.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@contract(asset_key='AssetKey', attr=str)
|
||||
def set_asset_metadata_attr(self, asset_key, attr, value, user_id):
|
||||
"""
|
||||
Add/set the given attr on the asset at the given location. Value can be any type which pymongo accepts.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): asset identifier
|
||||
attr (str): which attribute to set
|
||||
value: the value to set it to (any type pymongo accepts such as datetime, number, string)
|
||||
|
||||
Raises:
|
||||
ItemNotFoundError if no such item exists
|
||||
AttributeError is attr is one of the build in attrs.
|
||||
"""
|
||||
return self.set_asset_metadata_attrs(asset_key, {attr: value}, user_id)
|
||||
|
||||
@contract(asset_key='AssetKey')
|
||||
def delete_asset_metadata(self, asset_key, user_id):
|
||||
"""
|
||||
Deletes a single asset's metadata.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): locator containing original asset filename
|
||||
|
||||
Returns:
|
||||
Number of asset metadata entries deleted (0 or 1)
|
||||
"""
|
||||
return self._delete_asset_data(asset_key, user_id, thumbnail=False)
|
||||
|
||||
@contract(asset_key='AssetKey')
|
||||
def delete_asset_thumbnail_metadata(self, asset_key, user_id):
|
||||
"""
|
||||
Deletes a single asset's metadata.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): locator containing original asset filename
|
||||
|
||||
Returns:
|
||||
Number of asset metadata entries deleted (0 or 1)
|
||||
"""
|
||||
return self._delete_asset_data(asset_key, user_id, thumbnail=True)
|
||||
|
||||
@contract(source_course_key='CourseKey', dest_course_key='CourseKey')
|
||||
def copy_all_asset_metadata(self, source_course_key, dest_course_key, user_id):
|
||||
"""
|
||||
Copy all the course assets from source_course_key to dest_course_key.
|
||||
|
||||
Arguments:
|
||||
source_course_key (CourseKey): identifier of course to copy from
|
||||
dest_course_key (CourseKey): identifier of course to copy to
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def only_xmodules(identifier, entry_points):
|
||||
"""Only use entry_points that are supplied by the xmodule package"""
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
Support for inheritance of fields down an XBlock hierarchy.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from datetime import datetime
|
||||
from pytz import UTC
|
||||
|
||||
from xmodule.partitions.partitions import UserPartition
|
||||
from xblock.fields import Scope, Boolean, String, Float, XBlockMixin, Dict, Integer, List
|
||||
from xblock.runtime import KeyValueStore, KvsFieldData
|
||||
|
||||
from xmodule.fields import Date, Timedelta
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
# Make '_' a no-op so we can scrape strings
|
||||
_ = lambda text: text
|
||||
@@ -153,6 +154,16 @@ class InheritanceMixin(XBlockMixin):
|
||||
scope=Scope.settings
|
||||
)
|
||||
|
||||
reset_key = "DEFAULT_SHOW_RESET_BUTTON"
|
||||
default_reset_button = getattr(settings, reset_key) if hasattr(settings, reset_key) else False
|
||||
show_reset_button = Boolean(
|
||||
display_name=_("Show Reset Button for Problems"),
|
||||
help=_("Enter true or false. If true, problems default to displaying a 'Reset' button. This value may be "
|
||||
"overriden in each problem's settings. Existing problems whose reset setting have not been changed are affected."),
|
||||
scope=Scope.settings,
|
||||
default=default_reset_button
|
||||
)
|
||||
|
||||
|
||||
def compute_inherited_metadata(descriptor):
|
||||
"""Given a descriptor, traverse all of its descendants and do metadata
|
||||
|
||||
@@ -9,10 +9,12 @@ import logging
|
||||
from contextlib import contextmanager
|
||||
import itertools
|
||||
import functools
|
||||
from contracts import contract, new_contract
|
||||
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.keys import CourseKey, AssetKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.assetstore import AssetMetadata, AssetThumbnailMetadata
|
||||
|
||||
from . import ModuleStoreWriteBase
|
||||
from . import ModuleStoreEnum
|
||||
@@ -20,6 +22,10 @@ from .exceptions import ItemNotFoundError, DuplicateCourseError
|
||||
from .draft_and_published import ModuleStoreDraftAndPublished
|
||||
from .split_migrator import SplitMigrator
|
||||
|
||||
new_contract('CourseKey', CourseKey)
|
||||
new_contract('AssetKey', AssetKey)
|
||||
new_contract('AssetMetadata', AssetMetadata)
|
||||
new_contract('AssetThumbnailMetadata', AssetThumbnailMetadata)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -309,6 +315,189 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
|
||||
store = self._get_modulestore_for_courseid(course_key)
|
||||
return store.delete_course(course_key, user_id)
|
||||
|
||||
@contract(course_key='CourseKey', asset_metadata='AssetMetadata')
|
||||
def save_asset_metadata(self, course_key, asset_metadata, user_id):
|
||||
"""
|
||||
Saves the asset metadata for a particular course's asset.
|
||||
|
||||
Args:
|
||||
course_key (CourseKey): course identifier
|
||||
asset_metadata (AssetMetadata): data about the course asset data
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(course_key)
|
||||
return store.save_asset_metadata(course_key, asset_metadata, user_id)
|
||||
|
||||
@contract(course_key='CourseKey', asset_thumbnail_metadata='AssetThumbnailMetadata')
|
||||
def save_asset_thumbnail_metadata(self, course_key, asset_thumbnail_metadata, user_id):
|
||||
"""
|
||||
Saves the asset thumbnail metadata for a particular course asset's thumbnail.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): course identifier
|
||||
asset_thumbnail_metadata (AssetThumbnailMetadata): data about the course asset thumbnail
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(course_key)
|
||||
return store.save_asset_thumbnail_metadata(course_key, asset_thumbnail_metadata, user_id)
|
||||
|
||||
@strip_key
|
||||
@contract(asset_key='AssetKey')
|
||||
def find_asset_metadata(self, asset_key, **kwargs):
|
||||
"""
|
||||
Find the metadata for a particular course asset.
|
||||
|
||||
Args:
|
||||
asset_key (AssetKey): locator containing original asset filename
|
||||
|
||||
Returns:
|
||||
asset metadata (AssetMetadata) -or- None if not found
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(asset_key.course_key)
|
||||
return store.find_asset_metadata(asset_key, **kwargs)
|
||||
|
||||
@strip_key
|
||||
@contract(asset_key='AssetKey')
|
||||
def find_asset_thumbnail_metadata(self, asset_key, **kwargs):
|
||||
"""
|
||||
Find the metadata for a particular course asset.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): key containing original asset filename
|
||||
|
||||
Returns:
|
||||
asset metadata (AssetMetadata) -or- None if not found
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(asset_key.course_key)
|
||||
return store.find_asset_thumbnail_metadata(asset_key, **kwargs)
|
||||
|
||||
@strip_key
|
||||
@contract(course_key='CourseKey', start=int, maxresults=int, sort='list | None')
|
||||
def get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=None, **kwargs):
|
||||
"""
|
||||
Returns a list of static assets for a course.
|
||||
By default all assets are returned, but start and maxresults can be provided to limit the query.
|
||||
|
||||
Args:
|
||||
course_key (CourseKey): course identifier
|
||||
start (int): optional - start at this asset number
|
||||
maxresults (int): optional - return at most this many, -1 means no limit
|
||||
sort (array): optional - None means no sort
|
||||
(sort_by (str), sort_order (str))
|
||||
sort_by - one of 'uploadDate' or 'displayname'
|
||||
sort_order - one of 'ascending' or 'descending'
|
||||
|
||||
Returns:
|
||||
List of asset data dictionaries, which have the following keys:
|
||||
asset_key (AssetKey): asset identifier
|
||||
displayname: The human-readable name of the asset
|
||||
uploadDate (datetime.datetime): The date and time that the file was uploaded
|
||||
contentType: The mimetype string of the asset
|
||||
md5: An md5 hash of the asset content
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(course_key)
|
||||
return store.get_all_asset_metadata(course_key, start, maxresults, sort, **kwargs)
|
||||
|
||||
@strip_key
|
||||
@contract(course_key='CourseKey')
|
||||
def get_all_asset_thumbnail_metadata(self, course_key, **kwargs):
|
||||
"""
|
||||
Returns a list of thumbnails for all course assets.
|
||||
|
||||
Args:
|
||||
course_key (CourseKey): course identifier
|
||||
|
||||
Returns:
|
||||
List of AssetThumbnailMetadata objects.
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(course_key)
|
||||
return store.get_all_asset_thumbnail_metadata(course_key, **kwargs)
|
||||
|
||||
@contract(asset_key='AssetKey')
|
||||
def delete_asset_metadata(self, asset_key, user_id):
|
||||
"""
|
||||
Deletes a single asset's metadata.
|
||||
|
||||
Arguments:
|
||||
asset_id (AssetKey): locator containing original asset filename
|
||||
|
||||
Returns:
|
||||
Number of asset metadata entries deleted (0 or 1)
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(asset_key.course_key)
|
||||
return store.delete_asset_metadata(asset_key, user_id)
|
||||
|
||||
@contract(asset_key='AssetKey')
|
||||
def delete_asset_thumbnail_metadata(self, asset_key, user_id):
|
||||
"""
|
||||
Deletes a single asset's metadata.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): locator containing original asset filename
|
||||
|
||||
Returns:
|
||||
Number of asset metadata entries deleted (0 or 1)
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(asset_key.course_key)
|
||||
return store.delete_asset_thumbnail_metadata(asset_key, user_id)
|
||||
|
||||
@contract(course_key='CourseKey')
|
||||
def delete_all_asset_metadata(self, course_key, user_id):
|
||||
"""
|
||||
Delete all of the assets which use this course_key as an identifier.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): course_identifier
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(course_key)
|
||||
return store.delete_all_asset_metadata(course_key, user_id)
|
||||
|
||||
@contract(source_course_key='CourseKey', dest_course_key='CourseKey')
|
||||
def copy_all_asset_metadata(self, source_course_key, dest_course_key, user_id):
|
||||
"""
|
||||
Copy all the course assets from source_course_key to dest_course_key.
|
||||
|
||||
Arguments:
|
||||
source_course_key (CourseKey): identifier of course to copy from
|
||||
dest_course_key (CourseKey): identifier of course to copy to
|
||||
"""
|
||||
# When implementing this in https://openedx.atlassian.net/browse/PLAT-78 , consider this:
|
||||
# Check the modulestores of both the source and dest course_keys. If in different modulestores,
|
||||
# export all asset data from one modulestore and import it into the dest one.
|
||||
store = self._get_modulestore_for_courseid(source_course_key)
|
||||
return store.copy_all_asset_metadata(source_course_key, dest_course_key, user_id)
|
||||
|
||||
@contract(asset_key='AssetKey', attr=str)
|
||||
def set_asset_metadata_attr(self, asset_key, attr, value, user_id):
|
||||
"""
|
||||
Add/set the given attr on the asset at the given location. Value can be any type which pymongo accepts.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): asset identifier
|
||||
attr (str): which attribute to set
|
||||
value: the value to set it to (any type pymongo accepts such as datetime, number, string)
|
||||
|
||||
Raises:
|
||||
NotFoundError if no such item exists
|
||||
AttributeError is attr is one of the build in attrs.
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(asset_key.course_key)
|
||||
return store.set_asset_metadata_attrs(asset_key, {attr: value}, user_id)
|
||||
|
||||
@contract(asset_key='AssetKey', attr_dict=dict)
|
||||
def set_asset_metadata_attrs(self, asset_key, attr_dict, user_id):
|
||||
"""
|
||||
Add/set the given dict of attrs on the asset at the given location. Value can be any type which pymongo accepts.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): asset identifier
|
||||
attr_dict (dict): attribute/value pairs to set
|
||||
|
||||
Raises:
|
||||
NotFoundError if no such item exists
|
||||
AttributeError is attr is one of the build in attrs.
|
||||
"""
|
||||
store = self._get_modulestore_for_courseid(asset_key.course_key)
|
||||
return store.set_asset_metadata_attrs(asset_key, attr_dict, user_id)
|
||||
|
||||
@strip_key
|
||||
def get_parent_location(self, location, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -24,6 +24,7 @@ from fs.osfs import OSFS
|
||||
from path import path
|
||||
from datetime import datetime
|
||||
from pytz import UTC
|
||||
from contracts import contract, new_contract
|
||||
|
||||
from importlib import import_module
|
||||
from xmodule.errortracker import null_error_tracker, exc_info_to_str
|
||||
@@ -41,12 +42,18 @@ from xmodule.modulestore.inheritance import InheritanceMixin, inherit_metadata,
|
||||
from xblock.core import XBlock
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from opaque_keys.edx.keys import UsageKey, CourseKey
|
||||
from opaque_keys.edx.keys import UsageKey, CourseKey, AssetKey
|
||||
from xmodule.exceptions import HeartbeatFailure
|
||||
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
|
||||
from xmodule.assetstore import AssetMetadata, AssetThumbnailMetadata
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
new_contract('CourseKey', CourseKey)
|
||||
new_contract('AssetKey', AssetKey)
|
||||
new_contract('AssetMetadata', AssetMetadata)
|
||||
new_contract('AssetThumbnailMetadata', AssetThumbnailMetadata)
|
||||
|
||||
# sort order that returns DRAFT items first
|
||||
SORT_REVISION_FAVOR_DRAFT = ('_id.revision', pymongo.DESCENDING)
|
||||
|
||||
@@ -195,7 +202,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
|
||||
category = json_data['location']['category']
|
||||
class_ = self.load_block_type(category)
|
||||
|
||||
|
||||
definition = json_data.get('definition', {})
|
||||
metadata = json_data.get('metadata', {})
|
||||
for old_name, new_name in getattr(class_, 'metadata_translations', {}).items():
|
||||
@@ -443,7 +449,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
super(MongoModuleStore, self).__init__(contentstore=contentstore, **kwargs)
|
||||
|
||||
def do_connection(
|
||||
db, collection, host, port=27017, tz_aware=True, user=None, password=None, **kwargs
|
||||
db, collection, host, port=27017, tz_aware=True, user=None, password=None, asset_collection=None, **kwargs
|
||||
):
|
||||
"""
|
||||
Create & open the connection, authenticate, and provide pointers to the collection
|
||||
@@ -460,6 +466,11 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
)
|
||||
self.collection = self.database[collection]
|
||||
|
||||
# Collection which stores asset metadata.
|
||||
self.asset_collection = None
|
||||
if asset_collection is not None:
|
||||
self.asset_collection = self.database[asset_collection]
|
||||
|
||||
if user is not None and password is not None:
|
||||
self.database.authenticate(user, password)
|
||||
|
||||
@@ -1436,6 +1447,147 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
field_data = KvsFieldData(kvs)
|
||||
return field_data
|
||||
|
||||
def _find_course_assets(self, course_key):
|
||||
"""
|
||||
Internal; finds (or creates) course asset info about all assets for a particular course
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): course identifier
|
||||
|
||||
Returns:
|
||||
Asset info for the course
|
||||
"""
|
||||
if self.asset_collection is None:
|
||||
return None
|
||||
|
||||
# Using the course_key, find or insert the course asset metadata document.
|
||||
# A single document exists per course to store the course asset metadata.
|
||||
course_assets = self.asset_collection.find_one(
|
||||
{'course_id': unicode(course_key)},
|
||||
fields=('course_id', 'storage', 'assets', 'thumbnails')
|
||||
)
|
||||
|
||||
if course_assets is None:
|
||||
# Not found, so create.
|
||||
course_assets = {'course_id': unicode(course_key), 'storage': 'FILLMEIN-TMP', 'assets': [], 'thumbnails': []}
|
||||
course_assets['_id'] = self.asset_collection.insert(course_assets)
|
||||
|
||||
return course_assets
|
||||
|
||||
@contract(course_key='CourseKey', asset_metadata='AssetMetadata | AssetThumbnailMetadata')
|
||||
def _save_asset_info(self, course_key, asset_metadata, user_id, thumbnail=False):
|
||||
"""
|
||||
Saves the info for a particular course's asset/thumbnail.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): course identifier
|
||||
asset_metadata (AssetMetadata/AssetThumbnailMetadata): data about the course asset/thumbnail
|
||||
thumbnail (bool): True if saving thumbnail metadata, False if saving asset metadata
|
||||
|
||||
Returns:
|
||||
True if info save was successful, else False
|
||||
"""
|
||||
if self.asset_collection is None:
|
||||
return False
|
||||
|
||||
course_assets, asset_idx = self._find_course_asset(course_key, asset_metadata.asset_id.path, thumbnail)
|
||||
info = 'thumbnails' if thumbnail else 'assets'
|
||||
all_assets = course_assets[info]
|
||||
|
||||
# Set the edited information for assets only - not thumbnails.
|
||||
if not thumbnail:
|
||||
asset_metadata.update({'edited_by': user_id, 'edited_on': datetime.now(UTC)})
|
||||
|
||||
# Translate metadata to Mongo format.
|
||||
metadata_to_insert = asset_metadata.to_mongo()
|
||||
if asset_idx is None:
|
||||
# Append new metadata.
|
||||
# Future optimization: Insert in order & binary search to retrieve.
|
||||
all_assets.append(metadata_to_insert)
|
||||
else:
|
||||
# Replace existing metadata.
|
||||
all_assets[asset_idx] = metadata_to_insert
|
||||
|
||||
# Update the document.
|
||||
self.asset_collection.update({'_id': course_assets['_id']}, {'$set': {info: all_assets}})
|
||||
return True
|
||||
|
||||
@contract(asset_key='AssetKey', attr_dict=dict)
|
||||
def set_asset_metadata_attrs(self, asset_key, attr_dict, user_id):
|
||||
"""
|
||||
Add/set the given dict of attrs on the asset at the given location. Value can be any type which pymongo accepts.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): asset identifier
|
||||
attr_dict (dict): attribute: value pairs to set
|
||||
|
||||
Raises:
|
||||
ItemNotFoundError if no such item exists
|
||||
AttributeError is attr is one of the build in attrs.
|
||||
"""
|
||||
if self.asset_collection is None:
|
||||
return
|
||||
|
||||
course_assets, asset_idx = self._find_course_asset(asset_key.course_key, asset_key.path)
|
||||
if asset_idx is None:
|
||||
raise ItemNotFoundError(asset_key)
|
||||
|
||||
# Form an AssetMetadata.
|
||||
all_assets = course_assets['assets']
|
||||
md = AssetMetadata(asset_key, asset_key.path)
|
||||
md.from_mongo(all_assets[asset_idx])
|
||||
md.update(attr_dict)
|
||||
|
||||
# Generate a Mongo doc from the metadata and update the course asset info.
|
||||
all_assets[asset_idx] = md.to_mongo()
|
||||
|
||||
self.asset_collection.update({'_id': course_assets['_id']}, {"$set": {'assets': all_assets}})
|
||||
|
||||
@contract(asset_key='AssetKey')
|
||||
def _delete_asset_data(self, asset_key, user_id, thumbnail=False):
|
||||
"""
|
||||
Internal; deletes a single asset's metadata -or- thumbnail.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): key containing original asset/thumbnail filename
|
||||
thumbnail: True if thumbnail deletion, False if asset metadata deletion
|
||||
|
||||
Returns:
|
||||
Number of asset metadata/thumbnail entries deleted (0 or 1)
|
||||
"""
|
||||
if self.asset_collection is None:
|
||||
return 0
|
||||
|
||||
course_assets, asset_idx = self._find_course_asset(asset_key.course_key, asset_key.path, get_thumbnail=thumbnail)
|
||||
if asset_idx is None:
|
||||
return 0
|
||||
|
||||
info = 'thumbnails' if thumbnail else 'assets'
|
||||
|
||||
all_asset_info = course_assets[info]
|
||||
all_asset_info.pop(asset_idx)
|
||||
|
||||
# Update the document.
|
||||
self.asset_collection.update({'_id': course_assets['_id']}, {'$set': {info: all_asset_info}})
|
||||
return 1
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@contract(course_key='CourseKey')
|
||||
def delete_all_asset_metadata(self, course_key, user_id):
|
||||
"""
|
||||
Delete all of the assets which use this course_key as an identifier.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): course_identifier
|
||||
"""
|
||||
if self.asset_collection is None:
|
||||
return
|
||||
|
||||
# Using the course_id, find the course asset metadata document.
|
||||
# A single document exists per course to store the course asset metadata.
|
||||
course_assets = self._find_course_assets(course_key)
|
||||
self.asset_collection.remove(course_assets['_id'])
|
||||
|
||||
def heartbeat(self):
|
||||
"""
|
||||
Check that the db is reachable.
|
||||
|
||||
@@ -99,7 +99,7 @@ class MongoConnection(object):
|
||||
Segregation of pymongo functions from the data modeling mechanisms for split modulestore.
|
||||
"""
|
||||
def __init__(
|
||||
self, db, collection, host, port=27017, tz_aware=True, user=None, password=None, **kwargs
|
||||
self, db, collection, host, port=27017, tz_aware=True, user=None, password=None, asset_collection=None, **kwargs
|
||||
):
|
||||
"""
|
||||
Create & open the connection, authenticate, and provide pointers to the collections
|
||||
@@ -114,6 +114,10 @@ class MongoConnection(object):
|
||||
db
|
||||
)
|
||||
|
||||
# Remove when adding official Split support for asset metadata storage.
|
||||
if asset_collection:
|
||||
pass
|
||||
|
||||
if user is not None and password is not None:
|
||||
self.database.authenticate(user, password)
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from collections import defaultdict
|
||||
from types import NoneType
|
||||
from xmodule.assetstore import AssetMetadata
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -1174,7 +1175,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
|
||||
Find the version_history_depth next versions of this definition. Return as a VersionTree
|
||||
'''
|
||||
# TODO implement
|
||||
raise NotImplementedError()
|
||||
pass
|
||||
|
||||
def create_definition_from_data(self, course_key, new_def_data, category, user_id):
|
||||
"""
|
||||
@@ -2120,6 +2121,180 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
|
||||
"""
|
||||
return ModuleStoreEnum.Type.split
|
||||
|
||||
def _find_course_assets(self, course_key):
|
||||
"""
|
||||
Split specific lookup
|
||||
"""
|
||||
return self._lookup_course(course_key).structure
|
||||
|
||||
def _find_course_asset(self, course_key, filename, get_thumbnail=False):
|
||||
structure = self._lookup_course(course_key).structure
|
||||
return structure, self._lookup_course_asset(structure, filename, get_thumbnail)
|
||||
|
||||
def _lookup_course_asset(self, structure, filename, get_thumbnail=False):
|
||||
"""
|
||||
Find the course asset in the structure or return None if it does not exist
|
||||
"""
|
||||
# See if this asset already exists by checking the external_filename.
|
||||
# Studio doesn't currently support using multiple course assets with the same filename.
|
||||
# So use the filename as the unique identifier.
|
||||
accessor = 'thumbnails' if get_thumbnail else 'assets'
|
||||
for idx, asset in enumerate(structure.get(accessor, [])):
|
||||
if asset['filename'] == filename:
|
||||
return idx
|
||||
return None
|
||||
|
||||
def _update_course_assets(self, user_id, asset_key, update_function, get_thumbnail=False):
|
||||
"""
|
||||
A wrapper for functions wanting to manipulate assets. Gets and versions the structure,
|
||||
passes the mutable array for either 'assets' or 'thumbnails' as well as the idx to the function for it to
|
||||
update, then persists the changed data back into the course.
|
||||
|
||||
The update function can raise an exception if it doesn't want to actually do the commit. The
|
||||
surrounding method probably should catch that exception.
|
||||
"""
|
||||
with self.bulk_operations(asset_key.course_key):
|
||||
original_structure = self._lookup_course(asset_key.course_key).structure
|
||||
index_entry = self._get_index_if_valid(asset_key.course_key)
|
||||
new_structure = self.version_structure(asset_key.course_key, original_structure, user_id)
|
||||
|
||||
accessor = 'thumbnails' if get_thumbnail else 'assets'
|
||||
asset_idx = self._lookup_course_asset(new_structure, asset_key.path, get_thumbnail)
|
||||
|
||||
new_structure[accessor] = update_function(new_structure.get(accessor, []), asset_idx)
|
||||
|
||||
# update index if appropriate and structures
|
||||
self.update_structure(asset_key.course_key, new_structure)
|
||||
|
||||
if index_entry is not None:
|
||||
# update the index entry if appropriate
|
||||
self._update_head(asset_key.course_key, index_entry, asset_key.branch, new_structure['_id'])
|
||||
|
||||
def _save_asset_info(self, course_key, asset_metadata, user_id, thumbnail=False):
|
||||
"""
|
||||
The guts of saving a new or updated asset
|
||||
"""
|
||||
metadata_to_insert = asset_metadata.to_mongo()
|
||||
|
||||
def _internal_method(all_assets, asset_idx):
|
||||
"""
|
||||
Either replace the existing entry or add a new one
|
||||
"""
|
||||
if asset_idx is None:
|
||||
all_assets.append(metadata_to_insert)
|
||||
else:
|
||||
all_assets[asset_idx] = metadata_to_insert
|
||||
return all_assets
|
||||
|
||||
return self._update_course_assets(user_id, asset_metadata.asset_id, _internal_method, thumbnail)
|
||||
|
||||
@contract(asset_key='AssetKey', attr_dict=dict)
|
||||
def set_asset_metadata_attrs(self, asset_key, attr_dict, user_id):
|
||||
"""
|
||||
Add/set the given dict of attrs on the asset at the given location. Value can be any type which pymongo accepts.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): asset identifier
|
||||
attr_dict (dict): attribute: value pairs to set
|
||||
|
||||
Raises:
|
||||
ItemNotFoundError if no such item exists
|
||||
AttributeError is attr is one of the build in attrs.
|
||||
"""
|
||||
def _internal_method(all_assets, asset_idx):
|
||||
"""
|
||||
Update the found item
|
||||
"""
|
||||
if asset_idx is None:
|
||||
raise ItemNotFoundError(asset_key)
|
||||
|
||||
# Form an AssetMetadata.
|
||||
mdata = AssetMetadata(asset_key, asset_key.path)
|
||||
mdata.from_mongo(all_assets[asset_idx])
|
||||
mdata.update(attr_dict)
|
||||
|
||||
# Generate a Mongo doc from the metadata and update the course asset info.
|
||||
all_assets[asset_idx] = mdata.to_mongo()
|
||||
return all_assets
|
||||
|
||||
self._update_course_assets(user_id, asset_key, _internal_method, False)
|
||||
|
||||
@contract(asset_key='AssetKey')
|
||||
def _delete_asset_data(self, asset_key, user_id, thumbnail=False):
|
||||
"""
|
||||
Internal; deletes a single asset's metadata -or- thumbnail.
|
||||
|
||||
Arguments:
|
||||
asset_key (AssetKey): key containing original asset/thumbnail filename
|
||||
thumbnail: True if thumbnail deletion, False if asset metadata deletion
|
||||
|
||||
Returns:
|
||||
Number of asset metadata/thumbnail entries deleted (0 or 1)
|
||||
"""
|
||||
def _internal_method(all_asset_info, asset_idx):
|
||||
"""
|
||||
Remove the item if it was found
|
||||
"""
|
||||
if asset_idx is None:
|
||||
raise ItemNotFoundError(asset_key)
|
||||
|
||||
all_asset_info.pop(asset_idx)
|
||||
return all_asset_info
|
||||
|
||||
try:
|
||||
self._update_course_assets(user_id, asset_key, _internal_method, thumbnail)
|
||||
return 1
|
||||
except ItemNotFoundError:
|
||||
return 0
|
||||
|
||||
@contract(course_key='CourseKey')
|
||||
def delete_all_asset_metadata(self, course_key, user_id):
|
||||
"""
|
||||
Delete all of the assets which use this course_key as an identifier.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): course_identifier
|
||||
"""
|
||||
with self.bulk_operations(course_key):
|
||||
original_structure = self._lookup_course(course_key).structure
|
||||
index_entry = self._get_index_if_valid(course_key)
|
||||
new_structure = self.version_structure(course_key, original_structure, user_id)
|
||||
|
||||
new_structure['assets'] = []
|
||||
new_structure['thumbnails'] = []
|
||||
|
||||
# update index if appropriate and structures
|
||||
self.update_structure(course_key, new_structure)
|
||||
|
||||
if index_entry is not None:
|
||||
# update the index entry if appropriate
|
||||
self._update_head(course_key, index_entry, course_key.branch, new_structure['_id'])
|
||||
|
||||
@contract(source_course_key='CourseKey', dest_course_key='CourseKey')
|
||||
def copy_all_asset_metadata(self, source_course_key, dest_course_key, user_id):
|
||||
"""
|
||||
Copy all the course assets from source_course_key to dest_course_key.
|
||||
|
||||
Arguments:
|
||||
source_course_key (CourseKey): identifier of course to copy from
|
||||
dest_course_key (CourseKey): identifier of course to copy to
|
||||
"""
|
||||
source_structure = self._lookup_course(source_course_key).structure
|
||||
with self.bulk_operations(dest_course_key):
|
||||
original_structure = self._lookup_course(dest_course_key).structure
|
||||
index_entry = self._get_index_if_valid(dest_course_key)
|
||||
new_structure = self.version_structure(dest_course_key, original_structure, user_id)
|
||||
|
||||
new_structure['assets'] = source_structure.get('assets', [])
|
||||
new_structure['thumbnails'] = source_structure.get('thumbnails', [])
|
||||
|
||||
# update index if appropriate and structures
|
||||
self.update_structure(dest_course_key, new_structure)
|
||||
|
||||
if index_entry is not None:
|
||||
# update the index entry if appropriate
|
||||
self._update_head(dest_course_key, index_entry, dest_course_key.branch, new_structure['_id'])
|
||||
|
||||
def internal_clean_children(self, course_locator):
|
||||
"""
|
||||
Only intended for rather low level methods to use. Goes through the children attrs of
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Module for the dual-branch fall-back Draft->Published Versioning ModuleStore
|
||||
"""
|
||||
|
||||
from split import SplitMongoModuleStore, EXCLUDE_ALL
|
||||
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore, EXCLUDE_ALL
|
||||
from xmodule.exceptions import InvalidVersionError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.exceptions import InsufficientSpecificationError
|
||||
@@ -13,7 +13,7 @@ from opaque_keys.edx.locator import CourseLocator
|
||||
from xmodule.modulestore.split_mongo import BlockKey
|
||||
|
||||
|
||||
class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleStore):
|
||||
class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPublished):
|
||||
"""
|
||||
A subclass of Split that supports a dual-branch fall-back versioning framework
|
||||
with a Draft branch that falls back to a Published branch.
|
||||
@@ -43,9 +43,9 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
|
||||
# create any other necessary things as a side effect: ensure they populate the draft branch
|
||||
# and rely on auto publish to populate the published branch: split's create course doesn't
|
||||
# call super b/c it needs the auto publish above to have happened before any of the create_items
|
||||
# in this. The explicit use of SplitMongoModuleStore is intentional
|
||||
# in this; so, this manually calls the grandparent and above methods.
|
||||
with self.branch_setting(ModuleStoreEnum.Branch.draft_preferred, item.id):
|
||||
# pylint: disable=bad-super-call
|
||||
# NOTE: DO NOT CHANGE THE SUPER. See comment above
|
||||
super(SplitMongoModuleStore, self).create_course(
|
||||
org, course, run, user_id, runtime=item.runtime, **kwargs
|
||||
)
|
||||
@@ -229,7 +229,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
|
||||
if revision == ModuleStoreEnum.RevisionOption.draft_preferred:
|
||||
revision = ModuleStoreEnum.RevisionOption.draft_only
|
||||
location = self._map_revision_to_branch(location, revision=revision)
|
||||
return SplitMongoModuleStore.get_parent_location(self, location, **kwargs)
|
||||
return super(DraftVersioningModuleStore, self).get_parent_location(location, **kwargs)
|
||||
|
||||
def get_orphans(self, course_key, **kwargs):
|
||||
course_key = self._map_revision_to_branch(course_key)
|
||||
@@ -275,8 +275,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
|
||||
Publishes the subtree under location from the draft branch to the published branch
|
||||
Returns the newly published item.
|
||||
"""
|
||||
SplitMongoModuleStore.copy(
|
||||
self,
|
||||
super(DraftVersioningModuleStore, self).copy(
|
||||
user_id,
|
||||
# Directly using the replace function rather than the for_branch function
|
||||
# because for_branch obliterates the version_guid and will lead to missed version conflicts.
|
||||
@@ -446,3 +445,62 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
|
||||
if published_block is not None:
|
||||
setattr(xblock, '_published_by', published_block['edit_info']['edited_by'])
|
||||
setattr(xblock, '_published_on', published_block['edit_info']['edited_on'])
|
||||
|
||||
def _find_asset_info(self, asset_key, thumbnail=False, **kwargs):
|
||||
return super(DraftVersioningModuleStore, self)._find_asset_info(
|
||||
self._map_revision_to_branch(asset_key), thumbnail, **kwargs
|
||||
)
|
||||
|
||||
def _get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=None, get_thumbnails=False, **kwargs):
|
||||
return super(DraftVersioningModuleStore, self)._get_all_asset_metadata(
|
||||
self._map_revision_to_branch(course_key), start, maxresults, sort, get_thumbnails, **kwargs
|
||||
)
|
||||
|
||||
def _update_course_assets(self, user_id, asset_key, update_function, get_thumbnail=False):
|
||||
"""
|
||||
Updates both the published and draft branches
|
||||
"""
|
||||
# if one call gets an exception, don't do the other call but pass on the exception
|
||||
super(DraftVersioningModuleStore, self)._update_course_assets(
|
||||
user_id, self._map_revision_to_branch(asset_key, ModuleStoreEnum.RevisionOption.published_only),
|
||||
update_function, get_thumbnail
|
||||
)
|
||||
super(DraftVersioningModuleStore, self)._update_course_assets(
|
||||
user_id, self._map_revision_to_branch(asset_key, ModuleStoreEnum.RevisionOption.draft_only),
|
||||
update_function, get_thumbnail
|
||||
)
|
||||
|
||||
def _find_course_asset(self, course_key, filename, get_thumbnail=False):
|
||||
return super(DraftVersioningModuleStore, self)._find_course_asset(
|
||||
self._map_revision_to_branch(course_key), filename, get_thumbnail=get_thumbnail
|
||||
)
|
||||
|
||||
def _find_course_assets(self, course_key):
|
||||
"""
|
||||
Split specific lookup
|
||||
"""
|
||||
return super(DraftVersioningModuleStore, self)._find_course_assets(
|
||||
self._map_revision_to_branch(course_key)
|
||||
)
|
||||
|
||||
def delete_all_asset_metadata(self, course_key, user_id):
|
||||
"""
|
||||
Deletes from both branches
|
||||
"""
|
||||
super(DraftVersioningModuleStore, self).delete_all_asset_metadata(
|
||||
self._map_revision_to_branch(course_key, ModuleStoreEnum.RevisionOption.published_only), user_id
|
||||
)
|
||||
super(DraftVersioningModuleStore, self).delete_all_asset_metadata(
|
||||
self._map_revision_to_branch(course_key, ModuleStoreEnum.RevisionOption.draft_only), user_id
|
||||
)
|
||||
|
||||
def copy_all_asset_metadata(self, source_course_key, dest_course_key, user_id):
|
||||
"""
|
||||
Copies to and from both branches
|
||||
"""
|
||||
for revision in [ModuleStoreEnum.RevisionOption.published_only, ModuleStoreEnum.RevisionOption.draft_only]:
|
||||
super(DraftVersioningModuleStore, self).copy_all_asset_metadata(
|
||||
self._map_revision_to_branch(source_course_key, revision),
|
||||
self._map_revision_to_branch(dest_course_key, revision),
|
||||
user_id
|
||||
)
|
||||
|
||||
394
common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py
Normal file
394
common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py
Normal file
@@ -0,0 +1,394 @@
|
||||
"""
|
||||
Tests for assetstore using any of the modulestores for metadata. May extend to testing the storage options
|
||||
too.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
import unittest
|
||||
import ddt
|
||||
|
||||
from xmodule.assetstore import AssetMetadata, AssetThumbnailMetadata
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.test_cross_modulestore_import_export import (
|
||||
MODULESTORE_SETUPS, MongoContentstoreBuilder,
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestMongoAssetMetadataStorage(unittest.TestCase):
|
||||
"""
|
||||
Tests for storing/querying course asset metadata.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestMongoAssetMetadataStorage, self).setUp()
|
||||
self.addTypeEqualityFunc(datetime, self._compare_datetimes)
|
||||
self.addTypeEqualityFunc(AssetMetadata, self._compare_metadata)
|
||||
|
||||
def _compare_metadata(self, mdata1, mdata2, msg=None):
|
||||
"""
|
||||
So we can use the below date comparison
|
||||
"""
|
||||
if type(mdata1) != type(mdata2):
|
||||
self.fail(self._formatMessage(msg, u"{} is not same type as {}".format(mdata1, mdata2)))
|
||||
for attr in mdata1.ALLOWED_ATTRS:
|
||||
self.assertEqual(getattr(mdata1, attr), getattr(mdata2, attr), msg)
|
||||
|
||||
def _compare_datetimes(self, datetime1, datetime2, msg=None):
|
||||
"""
|
||||
Don't compare microseconds as mongo doesn't encode below milliseconds
|
||||
"""
|
||||
if not timedelta(seconds=-1) < datetime1 - datetime2 < timedelta(seconds=1):
|
||||
self.fail(self._formatMessage(msg, u"{} != {}".format(datetime1, datetime2)))
|
||||
|
||||
def _make_asset_metadata(self, asset_loc):
|
||||
"""
|
||||
Make a single test asset metadata.
|
||||
"""
|
||||
return AssetMetadata(asset_loc, internal_name='EKMND332DDBK',
|
||||
basename='pictures/historical', contenttype='image/jpeg',
|
||||
locked=False, md5='77631ca4f0e08419b70726a447333ab6',
|
||||
edited_by=ModuleStoreEnum.UserID.test, edited_on=datetime.now(pytz.utc),
|
||||
curr_version='v1.0', prev_version='v0.95')
|
||||
|
||||
def _make_asset_thumbnail_metadata(self, asset_key):
|
||||
"""
|
||||
Make a single test asset thumbnail metadata.
|
||||
"""
|
||||
return AssetThumbnailMetadata(asset_key, internal_name='ABC39XJUDN2')
|
||||
|
||||
def setup_assets(self, course1_key, course2_key, store=None):
|
||||
"""
|
||||
Setup assets. Save in store if given
|
||||
"""
|
||||
asset_fields = ('filename', 'internal_name', 'basename', 'locked', 'edited_by', 'edited_on', 'curr_version', 'prev_version')
|
||||
asset1_vals = ('pic1.jpg', 'EKMND332DDBK', 'pix/archive', False, ModuleStoreEnum.UserID.test, datetime.now(pytz.utc), '14', '13')
|
||||
asset2_vals = ('shout.ogg', 'KFMDONSKF39K', 'sounds', True, ModuleStoreEnum.UserID.test, datetime.now(pytz.utc), '1', None)
|
||||
asset3_vals = ('code.tgz', 'ZZB2333YBDMW', 'exercises/14', False, ModuleStoreEnum.UserID.test * 2, datetime.now(pytz.utc), 'AB', 'AA')
|
||||
asset4_vals = ('dog.png', 'PUPY4242X', 'pictures/animals', True, ModuleStoreEnum.UserID.test * 3, datetime.now(pytz.utc), '5', '4')
|
||||
asset5_vals = ('not_here.txt', 'JJJCCC747', '/dev/null', False, ModuleStoreEnum.UserID.test * 4, datetime.now(pytz.utc), '50', '49')
|
||||
|
||||
asset1 = dict(zip(asset_fields[1:], asset1_vals[1:]))
|
||||
asset2 = dict(zip(asset_fields[1:], asset2_vals[1:]))
|
||||
asset3 = dict(zip(asset_fields[1:], asset3_vals[1:]))
|
||||
asset4 = dict(zip(asset_fields[1:], asset4_vals[1:]))
|
||||
non_existent_asset = dict(zip(asset_fields[1:], asset5_vals[1:]))
|
||||
|
||||
# Asset6 and thumbnail6 have equivalent information on purpose.
|
||||
asset6_vals = ('asset.txt', 'JJJCCC747858', '/dev/null', False, ModuleStoreEnum.UserID.test * 4, datetime.now(pytz.utc), '50', '49')
|
||||
asset6 = dict(zip(asset_fields[1:], asset6_vals[1:]))
|
||||
|
||||
asset1_key = course1_key.make_asset_key('asset', asset1_vals[0])
|
||||
asset2_key = course1_key.make_asset_key('asset', asset2_vals[0])
|
||||
asset3_key = course2_key.make_asset_key('asset', asset3_vals[0])
|
||||
asset4_key = course2_key.make_asset_key('asset', asset4_vals[0])
|
||||
asset5_key = course2_key.make_asset_key('asset', asset5_vals[0])
|
||||
asset6_key = course2_key.make_asset_key('asset', asset6_vals[0])
|
||||
|
||||
asset1_md = AssetMetadata(asset1_key, **asset1)
|
||||
asset2_md = AssetMetadata(asset2_key, **asset2)
|
||||
asset3_md = AssetMetadata(asset3_key, **asset3)
|
||||
asset4_md = AssetMetadata(asset4_key, **asset4)
|
||||
asset5_md = AssetMetadata(asset5_key, **non_existent_asset)
|
||||
asset6_md = AssetMetadata(asset6_key, **asset6)
|
||||
|
||||
if store is not None:
|
||||
store.save_asset_metadata(course1_key, asset1_md, ModuleStoreEnum.UserID.test)
|
||||
store.save_asset_metadata(course1_key, asset2_md, ModuleStoreEnum.UserID.test)
|
||||
store.save_asset_metadata(course2_key, asset3_md, ModuleStoreEnum.UserID.test)
|
||||
store.save_asset_metadata(course2_key, asset4_md, ModuleStoreEnum.UserID.test)
|
||||
# 5 & 6 are not saved on purpose!
|
||||
|
||||
return (asset1_md, asset2_md, asset3_md, asset4_md, asset5_md, asset6_md)
|
||||
|
||||
def setup_thumbnails(self, course1_key, course2_key, store=None):
|
||||
"""
|
||||
Setup thumbs. Save in store if given
|
||||
"""
|
||||
thumbnail_fields = ('filename', 'internal_name')
|
||||
thumbnail1_vals = ('cat_thumb.jpg', 'XYXYXYXYXYXY')
|
||||
thumbnail2_vals = ('kitten_thumb.jpg', '123ABC123ABC')
|
||||
thumbnail3_vals = ('puppy_thumb.jpg', 'ADAM12ADAM12')
|
||||
thumbnail4_vals = ('meerkat_thumb.jpg', 'CHIPSPONCH14')
|
||||
thumbnail5_vals = ('corgi_thumb.jpg', 'RON8LDXFFFF10')
|
||||
|
||||
thumbnail1 = dict(zip(thumbnail_fields[1:], thumbnail1_vals[1:]))
|
||||
thumbnail2 = dict(zip(thumbnail_fields[1:], thumbnail2_vals[1:]))
|
||||
thumbnail3 = dict(zip(thumbnail_fields[1:], thumbnail3_vals[1:]))
|
||||
thumbnail4 = dict(zip(thumbnail_fields[1:], thumbnail4_vals[1:]))
|
||||
non_existent_thumbnail = dict(zip(thumbnail_fields[1:], thumbnail5_vals[1:]))
|
||||
|
||||
# Asset6 and thumbnail6 have equivalent information on purpose.
|
||||
thumbnail6_vals = ('asset.txt', 'JJJCCC747858')
|
||||
thumbnail6 = dict(zip(thumbnail_fields[1:], thumbnail6_vals[1:]))
|
||||
|
||||
thumb1_key = course1_key.make_asset_key('thumbnail', thumbnail1_vals[0])
|
||||
thumb2_key = course1_key.make_asset_key('thumbnail', thumbnail2_vals[0])
|
||||
thumb3_key = course2_key.make_asset_key('thumbnail', thumbnail3_vals[0])
|
||||
thumb4_key = course2_key.make_asset_key('thumbnail', thumbnail4_vals[0])
|
||||
thumb5_key = course2_key.make_asset_key('thumbnail', thumbnail5_vals[0])
|
||||
thumb6_key = course2_key.make_asset_key('thumbnail', thumbnail6_vals[0])
|
||||
|
||||
thumb1_md = AssetThumbnailMetadata(thumb1_key, **thumbnail1)
|
||||
thumb2_md = AssetThumbnailMetadata(thumb2_key, **thumbnail2)
|
||||
thumb3_md = AssetThumbnailMetadata(thumb3_key, **thumbnail3)
|
||||
thumb4_md = AssetThumbnailMetadata(thumb4_key, **thumbnail4)
|
||||
thumb5_md = AssetThumbnailMetadata(thumb5_key, **non_existent_thumbnail)
|
||||
thumb6_md = AssetThumbnailMetadata(thumb6_key, **thumbnail6)
|
||||
|
||||
if store is not None:
|
||||
store.save_asset_thumbnail_metadata(course1_key, thumb1_md, ModuleStoreEnum.UserID.test)
|
||||
store.save_asset_thumbnail_metadata(course1_key, thumb2_md, ModuleStoreEnum.UserID.test)
|
||||
store.save_asset_thumbnail_metadata(course2_key, thumb3_md, ModuleStoreEnum.UserID.test)
|
||||
store.save_asset_thumbnail_metadata(course2_key, thumb4_md, ModuleStoreEnum.UserID.test)
|
||||
# thumb5 and thumb6 are not saved on purpose!
|
||||
|
||||
return (thumb1_md, thumb2_md, thumb3_md, thumb4_md, thumb5_md, thumb6_md)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_save_one_and_confirm(self, storebuilder):
|
||||
"""
|
||||
Save the metadata in each store and retrieve it singularly, as all assets, and after deleting all.
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
|
||||
asset_filename = 'burnside.jpg'
|
||||
new_asset_loc = course.id.make_asset_key('asset', asset_filename)
|
||||
# Confirm that the asset's metadata is not present.
|
||||
self.assertIsNone(store.find_asset_metadata(new_asset_loc))
|
||||
# Save the asset's metadata.
|
||||
new_asset_md = self._make_asset_metadata(new_asset_loc)
|
||||
store.save_asset_metadata(course.id, new_asset_md, ModuleStoreEnum.UserID.test)
|
||||
# Find the asset's metadata and confirm it's the same.
|
||||
found_asset_md = store.find_asset_metadata(new_asset_loc)
|
||||
self.assertIsNotNone(found_asset_md)
|
||||
self.assertEquals(new_asset_md, found_asset_md)
|
||||
# Confirm that only two setup plus one asset's metadata exists.
|
||||
self.assertEquals(len(store.get_all_asset_metadata(course.id)), 1)
|
||||
# Delete all metadata and confirm it's gone.
|
||||
store.delete_all_asset_metadata(course.id, ModuleStoreEnum.UserID.test)
|
||||
self.assertEquals(len(store.get_all_asset_metadata(course.id)), 0)
|
||||
# Now delete the non-existent metadata and ensure it doesn't choke
|
||||
store.delete_all_asset_metadata(course.id, ModuleStoreEnum.UserID.test)
|
||||
self.assertEquals(len(store.get_all_asset_metadata(course.id)), 0)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_delete(self, storebuilder):
|
||||
"""
|
||||
Delete non_existent and existent metadata
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
new_asset_loc = course.id.make_asset_key('asset', 'burnside.jpg')
|
||||
# Attempt to delete an asset that doesn't exist.
|
||||
self.assertEquals(store.delete_asset_metadata(new_asset_loc, ModuleStoreEnum.UserID.test), 0)
|
||||
self.assertEquals(len(store.get_all_asset_metadata(course.id)), 0)
|
||||
|
||||
new_asset_md = self._make_asset_metadata(new_asset_loc)
|
||||
store.save_asset_metadata(course.id, new_asset_md, ModuleStoreEnum.UserID.test)
|
||||
self.assertEquals(store.delete_asset_metadata(new_asset_loc, ModuleStoreEnum.UserID.test), 1)
|
||||
self.assertEquals(len(store.get_all_asset_metadata(course.id)), 0)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_find_non_existing_assets(self, storebuilder):
|
||||
"""
|
||||
Save multiple metadata in each store and retrieve it singularly, as all assets, and after deleting all.
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
new_asset_loc = course.id.make_asset_key('asset', 'burnside.jpg')
|
||||
# Find existing asset metadata.
|
||||
asset_md = store.find_asset_metadata(new_asset_loc)
|
||||
self.assertIsNone(asset_md)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_add_same_asset_twice(self, storebuilder):
|
||||
"""
|
||||
Save multiple metadata in each store and retrieve it singularly, as all assets, and after deleting all.
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
new_asset_loc = course.id.make_asset_key('asset', 'burnside.jpg')
|
||||
new_asset_md = self._make_asset_metadata(new_asset_loc)
|
||||
# Add asset metadata.
|
||||
store.save_asset_metadata(course.id, new_asset_md, ModuleStoreEnum.UserID.test)
|
||||
self.assertEquals(len(store.get_all_asset_metadata(course.id)), 1)
|
||||
# Add *the same* asset metadata.
|
||||
store.save_asset_metadata(course.id, new_asset_md, ModuleStoreEnum.UserID.test)
|
||||
# Still one here?
|
||||
self.assertEquals(len(store.get_all_asset_metadata(course.id)), 1)
|
||||
store.delete_all_asset_metadata(course.id, ModuleStoreEnum.UserID.test)
|
||||
self.assertEquals(len(store.get_all_asset_metadata(course.id)), 0)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_lock_unlock_assets(self, storebuilder):
|
||||
"""
|
||||
Save multiple metadata in each store and retrieve it singularly, as all assets, and after deleting all.
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
new_asset_loc = course.id.make_asset_key('asset', 'burnside.jpg')
|
||||
new_asset_md = self._make_asset_metadata(new_asset_loc)
|
||||
store.save_asset_metadata(course.id, new_asset_md, ModuleStoreEnum.UserID.test)
|
||||
|
||||
locked_state = new_asset_md.locked
|
||||
# Flip the course asset's locked status.
|
||||
store.set_asset_metadata_attr(new_asset_loc, "locked", not locked_state, ModuleStoreEnum.UserID.test)
|
||||
# Find the same course and check its locked status.
|
||||
updated_asset_md = store.find_asset_metadata(new_asset_loc)
|
||||
self.assertIsNotNone(updated_asset_md)
|
||||
self.assertEquals(updated_asset_md.locked, not locked_state)
|
||||
# Now flip it back.
|
||||
store.set_asset_metadata_attr(new_asset_loc, "locked", locked_state, ModuleStoreEnum.UserID.test)
|
||||
reupdated_asset_md = store.find_asset_metadata(new_asset_loc)
|
||||
self.assertIsNotNone(reupdated_asset_md)
|
||||
self.assertEquals(reupdated_asset_md.locked, locked_state)
|
||||
|
||||
ALLOWED_ATTRS = (
|
||||
('basename', '/new/path'),
|
||||
('internal_name', 'new_filename.txt'),
|
||||
('locked', True),
|
||||
('contenttype', 'image/png'),
|
||||
('md5', '5346682d948cc3f683635b6918f9b3d0'),
|
||||
('curr_version', 'v1.01'),
|
||||
('prev_version', 'v1.0'),
|
||||
('edited_by', 'Mork'),
|
||||
('edited_on', datetime(1969, 1, 1, tzinfo=pytz.utc)),
|
||||
)
|
||||
|
||||
DISALLOWED_ATTRS = (
|
||||
('asset_id', 'IAmBogus'),
|
||||
)
|
||||
|
||||
UNKNOWN_ATTRS = (
|
||||
('lunch_order', 'burger_and_fries'),
|
||||
('villain', 'Khan')
|
||||
)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_set_all_attrs(self, storebuilder):
|
||||
"""
|
||||
Save setting each attr one at a time
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
new_asset_loc = course.id.make_asset_key('asset', 'burnside.jpg')
|
||||
new_asset_md = self._make_asset_metadata(new_asset_loc)
|
||||
store.save_asset_metadata(course.id, new_asset_md, ModuleStoreEnum.UserID.test)
|
||||
for attr, value in self.ALLOWED_ATTRS:
|
||||
# Set the course asset's attr.
|
||||
store.set_asset_metadata_attr(new_asset_loc, attr, value, ModuleStoreEnum.UserID.test)
|
||||
# Find the same course asset and check its changed attr.
|
||||
updated_asset_md = store.find_asset_metadata(new_asset_loc)
|
||||
self.assertIsNotNone(updated_asset_md)
|
||||
self.assertIsNotNone(getattr(updated_asset_md, attr, None))
|
||||
self.assertEquals(getattr(updated_asset_md, attr, None), value)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_set_disallowed_attrs(self, storebuilder):
|
||||
"""
|
||||
setting disallowed attrs should fail
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
new_asset_loc = course.id.make_asset_key('asset', 'burnside.jpg')
|
||||
new_asset_md = self._make_asset_metadata(new_asset_loc)
|
||||
store.save_asset_metadata(course.id, new_asset_md, ModuleStoreEnum.UserID.test)
|
||||
for attr, value in self.DISALLOWED_ATTRS:
|
||||
original_attr_val = getattr(new_asset_md, attr)
|
||||
# Set the course asset's attr.
|
||||
store.set_asset_metadata_attr(new_asset_loc, attr, value, ModuleStoreEnum.UserID.test)
|
||||
# Find the same course and check its changed attr.
|
||||
updated_asset_md = store.find_asset_metadata(new_asset_loc)
|
||||
self.assertIsNotNone(updated_asset_md)
|
||||
self.assertIsNotNone(getattr(updated_asset_md, attr, None))
|
||||
# Make sure that the attr is unchanged from its original value.
|
||||
self.assertEquals(getattr(updated_asset_md, attr, None), original_attr_val)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_set_unknown_attrs(self, storebuilder):
|
||||
"""
|
||||
setting unknown attrs should fail
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
new_asset_loc = course.id.make_asset_key('asset', 'burnside.jpg')
|
||||
new_asset_md = self._make_asset_metadata(new_asset_loc)
|
||||
store.save_asset_metadata(course.id, new_asset_md, ModuleStoreEnum.UserID.test)
|
||||
for attr, value in self.UNKNOWN_ATTRS:
|
||||
# Set the course asset's attr.
|
||||
store.set_asset_metadata_attr(new_asset_loc, attr, value, ModuleStoreEnum.UserID.test)
|
||||
# Find the same course and check its changed attr.
|
||||
updated_asset_md = store.find_asset_metadata(new_asset_loc)
|
||||
self.assertIsNotNone(updated_asset_md)
|
||||
# Make sure the unknown field was *not* added.
|
||||
with self.assertRaises(AttributeError):
|
||||
self.assertEquals(getattr(updated_asset_md, attr), value)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_save_one_thumbnail_and_delete_one_thumbnail(self, storebuilder):
|
||||
"""
|
||||
saving and deleting thumbnails
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
thumbnail_filename = 'burn_thumb.jpg'
|
||||
asset_key = course.id.make_asset_key('thumbnail', thumbnail_filename)
|
||||
new_asset_thumbnail = self._make_asset_thumbnail_metadata(asset_key)
|
||||
store.save_asset_thumbnail_metadata(course.id, new_asset_thumbnail, ModuleStoreEnum.UserID.test)
|
||||
self.assertEquals(len(store.get_all_asset_thumbnail_metadata(course.id)), 1)
|
||||
self.assertEquals(store.delete_asset_thumbnail_metadata(asset_key, ModuleStoreEnum.UserID.test), 1)
|
||||
self.assertEquals(len(store.get_all_asset_thumbnail_metadata(course.id)), 0)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_find_thumbnail(self, storebuilder):
|
||||
"""
|
||||
finding thumbnails
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
thumbnail_filename = 'burn_thumb.jpg'
|
||||
asset_key = course.id.make_asset_key('thumbnail', thumbnail_filename)
|
||||
new_asset_thumbnail = self._make_asset_thumbnail_metadata(asset_key)
|
||||
store.save_asset_thumbnail_metadata(course.id, new_asset_thumbnail, ModuleStoreEnum.UserID.test)
|
||||
|
||||
self.assertIsNotNone(store.find_asset_thumbnail_metadata(asset_key))
|
||||
unknown_asset_key = course.id.make_asset_key('thumbnail', 'nosuchfile.jpg')
|
||||
self.assertIsNone(store.find_asset_thumbnail_metadata(unknown_asset_key))
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
def test_delete_all_thumbnails(self, storebuilder):
|
||||
"""
|
||||
deleting all thumbnails
|
||||
"""
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
with storebuilder.build(contentstore) as store:
|
||||
course = CourseFactory.create(modulestore=store)
|
||||
thumbnail_filename = 'burn_thumb.jpg'
|
||||
asset_key = course.id.make_asset_key('thumbnail', thumbnail_filename)
|
||||
new_asset_thumbnail = self._make_asset_thumbnail_metadata(asset_key)
|
||||
store.save_asset_thumbnail_metadata(
|
||||
course.id, new_asset_thumbnail, ModuleStoreEnum.UserID.test
|
||||
)
|
||||
|
||||
self.assertEquals(len(store.get_all_asset_thumbnail_metadata(course.id)), 1)
|
||||
store.delete_all_asset_metadata(course.id, ModuleStoreEnum.UserID.test)
|
||||
self.assertEquals(len(store.get_all_asset_thumbnail_metadata(course.id)), 0)
|
||||
|
||||
def test_get_all_assets_with_paging(self):
|
||||
pass
|
||||
|
||||
def test_copy_all_assets(self):
|
||||
pass
|
||||
@@ -87,6 +87,7 @@ class MongoModulestoreBuilder(object):
|
||||
doc_store_config = dict(
|
||||
db='modulestore{}'.format(random.randint(0, 10000)),
|
||||
collection='xmodule',
|
||||
asset_collection='asset_metadata',
|
||||
**COMMON_DOCSTORE_CONFIG
|
||||
)
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class TestMixedModuleStore(CourseComparisonTest):
|
||||
PORT = MONGO_PORT_NUM
|
||||
DB = 'test_mongo_%s' % uuid4().hex[:5]
|
||||
COLLECTION = 'modulestore'
|
||||
ASSET_COLLECTION = 'assetstore'
|
||||
FS_ROOT = DATA_DIR
|
||||
DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
|
||||
RENDER_TEMPLATE = lambda t_n, d, ctx = None, nsp = 'main': ''
|
||||
@@ -67,6 +68,7 @@ class TestMixedModuleStore(CourseComparisonTest):
|
||||
'port': PORT,
|
||||
'db': DB,
|
||||
'collection': COLLECTION,
|
||||
'asset_collection': ASSET_COLLECTION,
|
||||
}
|
||||
OPTIONS = {
|
||||
'mappings': {
|
||||
|
||||
@@ -45,6 +45,7 @@ HOST = MONGO_HOST
|
||||
PORT = MONGO_PORT_NUM
|
||||
DB = 'test_mongo_%s' % uuid4().hex[:5]
|
||||
COLLECTION = 'modulestore'
|
||||
ASSET_COLLECTION = 'assetstore'
|
||||
FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item
|
||||
DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
|
||||
RENDER_TEMPLATE = lambda t_n, d, ctx = None, nsp = 'main': ''
|
||||
@@ -60,8 +61,10 @@ class ReferenceTestXBlock(XBlock, XModuleMixin):
|
||||
reference_dict = ReferenceValueDict(scope=Scope.settings)
|
||||
|
||||
|
||||
class TestMongoModuleStore(unittest.TestCase):
|
||||
'''Tests!'''
|
||||
class TestMongoModuleStoreBase(unittest.TestCase):
|
||||
'''
|
||||
Basic setup for all tests
|
||||
'''
|
||||
# Explicitly list the courses to load (don't want the big one)
|
||||
courses = ['toy', 'simple', 'simple_with_draft', 'test_unicode']
|
||||
|
||||
@@ -87,6 +90,13 @@ class TestMongoModuleStore(unittest.TestCase):
|
||||
cls.connection.drop_database(DB)
|
||||
cls.connection.close()
|
||||
|
||||
@classmethod
|
||||
def add_asset_collection(cls, doc_store_config):
|
||||
"""
|
||||
No asset collection.
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def initdb(cls):
|
||||
# connect to the db
|
||||
@@ -95,7 +105,10 @@ class TestMongoModuleStore(unittest.TestCase):
|
||||
'port': PORT,
|
||||
'db': DB,
|
||||
'collection': COLLECTION,
|
||||
#'asset_collection': ASSET_COLLECTION,
|
||||
}
|
||||
cls.add_asset_collection(doc_store_config)
|
||||
|
||||
# since MongoModuleStore and MongoContentStore are basically assumed to be together, create this class
|
||||
# as well
|
||||
content_store = MongoContentStore(HOST, DB, port=PORT)
|
||||
@@ -136,14 +149,33 @@ class TestMongoModuleStore(unittest.TestCase):
|
||||
# Destroy the test db.
|
||||
connection.drop_database(DB)
|
||||
|
||||
def setUp(self):
|
||||
# make a copy for convenience
|
||||
self.connection = TestMongoModuleStore.connection
|
||||
self.dummy_user = ModuleStoreEnum.UserID.test
|
||||
@classmethod
|
||||
def setUp(cls):
|
||||
cls.dummy_user = ModuleStoreEnum.UserID.test
|
||||
|
||||
def tearDown(self):
|
||||
@classmethod
|
||||
def tearDown(cls):
|
||||
pass
|
||||
|
||||
|
||||
class TestMongoModuleStore(TestMongoModuleStoreBase):
|
||||
'''Module store tests'''
|
||||
|
||||
@classmethod
|
||||
def add_asset_collection(cls, doc_store_config):
|
||||
"""
|
||||
No asset collection - it's not used in the tests below.
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def setupClass(cls):
|
||||
super(TestMongoModuleStore, cls).setupClass()
|
||||
|
||||
@classmethod
|
||||
def teardownClass(cls):
|
||||
super(TestMongoModuleStore, cls).teardownClass()
|
||||
|
||||
def test_init(self):
|
||||
'''Make sure the db loads'''
|
||||
ids = list(self.connection[DB][COLLECTION].find({}, {'_id': True}))
|
||||
@@ -233,7 +265,6 @@ class TestMongoModuleStore(unittest.TestCase):
|
||||
self.draft_store.get_item(Location('edX', 'test_unicode', '2012_Fall', 'chapter', 'Overview')),
|
||||
)
|
||||
|
||||
|
||||
def test_find_one(self):
|
||||
assert_not_none(
|
||||
self.draft_store._find_one(Location('edX', 'toy', '2012_Fall', 'course', '2012_Fall')),
|
||||
@@ -632,6 +663,37 @@ class TestMongoModuleStore(unittest.TestCase):
|
||||
shutil.rmtree(root_dir)
|
||||
|
||||
|
||||
class TestMongoModuleStoreWithNoAssetCollection(TestMongoModuleStore):
|
||||
'''
|
||||
Tests a situation where no asset_collection is specified.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def add_asset_collection(cls, doc_store_config):
|
||||
"""
|
||||
No asset collection.
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def setupClass(cls):
|
||||
super(TestMongoModuleStoreWithNoAssetCollection, cls).setupClass()
|
||||
|
||||
@classmethod
|
||||
def teardownClass(cls):
|
||||
super(TestMongoModuleStoreWithNoAssetCollection, cls).teardownClass()
|
||||
|
||||
def test_no_asset_collection(self):
|
||||
courses = self.draft_store.get_courses()
|
||||
course = courses[0]
|
||||
# Confirm that no asset collection means no asset metadata.
|
||||
self.assertEquals(self.draft_store.get_all_asset_metadata(course.id), None)
|
||||
# Now delete the non-existent asset metadata.
|
||||
self.draft_store.delete_all_asset_metadata(course.id, ModuleStoreEnum.UserID.test)
|
||||
# Should still be nothing.
|
||||
self.assertEquals(self.draft_store.get_all_asset_metadata(course.id), None)
|
||||
|
||||
|
||||
class TestMongoKeyValueStore(object):
|
||||
"""
|
||||
Tests for MongoKeyValueStore.
|
||||
|
||||
@@ -13,6 +13,7 @@ import random
|
||||
import os
|
||||
import textwrap
|
||||
import unittest
|
||||
import ddt
|
||||
|
||||
from mock import Mock, patch
|
||||
import webob
|
||||
@@ -31,6 +32,7 @@ from xblock.fields import ScopeIds
|
||||
from . import get_test_system
|
||||
from pytz import UTC
|
||||
from capa.correctmap import CorrectMap
|
||||
from ..capa_base_constants import RANDOMIZATION
|
||||
|
||||
|
||||
class CapaFactory(object):
|
||||
@@ -140,7 +142,6 @@ class CapaFactory(object):
|
||||
|
||||
return module
|
||||
|
||||
|
||||
class CapaFactoryWithFiles(CapaFactory):
|
||||
"""
|
||||
A factory for creating a Capa problem with files attached.
|
||||
@@ -182,6 +183,7 @@ if submission[0] == '':
|
||||
""")
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@@ -540,39 +542,42 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that number of attempts NOT incremented
|
||||
self.assertEqual(module.attempts, 3)
|
||||
|
||||
def test_check_problem_resubmitted_with_randomize(self):
|
||||
rerandomize_values = ['always', 'true']
|
||||
@ddt.data(
|
||||
RANDOMIZATION.ALWAYS,
|
||||
'true'
|
||||
)
|
||||
def test_check_problem_resubmitted_with_randomize(self, rerandomize):
|
||||
# Randomize turned on
|
||||
module = CapaFactory.create(rerandomize=rerandomize, attempts=0)
|
||||
|
||||
for rerandomize in rerandomize_values:
|
||||
# Randomize turned on
|
||||
module = CapaFactory.create(rerandomize=rerandomize, attempts=0)
|
||||
# Simulate that the problem is completed
|
||||
module.done = True
|
||||
|
||||
# Simulate that the problem is completed
|
||||
module.done = True
|
||||
|
||||
# Expect that we cannot submit
|
||||
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that number of attempts NOT incremented
|
||||
self.assertEqual(module.attempts, 0)
|
||||
|
||||
def test_check_problem_resubmitted_no_randomize(self):
|
||||
rerandomize_values = ['never', 'false', 'per_student']
|
||||
|
||||
for rerandomize in rerandomize_values:
|
||||
# Randomize turned off
|
||||
module = CapaFactory.create(rerandomize=rerandomize, attempts=0, done=True)
|
||||
|
||||
# Expect that we can submit successfully
|
||||
# Expect that we cannot submit
|
||||
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
module.check_problem(get_request_dict)
|
||||
|
||||
self.assertEqual(result['success'], 'correct')
|
||||
# Expect that number of attempts NOT incremented
|
||||
self.assertEqual(module.attempts, 0)
|
||||
|
||||
# Expect that number of attempts IS incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
@ddt.data(
|
||||
RANDOMIZATION.NEVER,
|
||||
'false',
|
||||
RANDOMIZATION.PER_STUDENT
|
||||
)
|
||||
def test_check_problem_resubmitted_no_randomize(self, rerandomize):
|
||||
# Randomize turned off
|
||||
module = CapaFactory.create(rerandomize=rerandomize, attempts=0, done=True)
|
||||
|
||||
# Expect that we can submit successfully
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
self.assertEqual(result['success'], 'correct')
|
||||
|
||||
# Expect that number of attempts IS incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
def test_check_problem_queued(self):
|
||||
module = CapaFactory.create(attempts=1)
|
||||
@@ -813,7 +818,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
|
||||
def test_reset_problem_closed(self):
|
||||
# pre studio default
|
||||
module = CapaFactory.create(rerandomize="always")
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS)
|
||||
|
||||
# Simulate that the problem is closed
|
||||
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
|
||||
@@ -944,35 +949,36 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the result is failure
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
def test_save_problem_submitted_with_randomize(self):
|
||||
|
||||
@ddt.data(
|
||||
RANDOMIZATION.ALWAYS,
|
||||
'true'
|
||||
)
|
||||
def test_save_problem_submitted_with_randomize(self, rerandomize):
|
||||
# Capa XModule treats 'always' and 'true' equivalently
|
||||
rerandomize_values = ['always', 'true']
|
||||
module = CapaFactory.create(rerandomize=rerandomize, done=True)
|
||||
|
||||
for rerandomize in rerandomize_values:
|
||||
module = CapaFactory.create(rerandomize=rerandomize, done=True)
|
||||
# Try to save
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Try to save
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that we cannot save
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
def test_save_problem_submitted_no_randomize(self):
|
||||
# Expect that we cannot save
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
@ddt.data(
|
||||
RANDOMIZATION.NEVER,
|
||||
'false',
|
||||
RANDOMIZATION.PER_STUDENT
|
||||
)
|
||||
def test_save_problem_submitted_no_randomize(self, rerandomize):
|
||||
# Capa XModule treats 'false' and 'per_student' equivalently
|
||||
rerandomize_values = ['never', 'false', 'per_student']
|
||||
module = CapaFactory.create(rerandomize=rerandomize, done=True)
|
||||
|
||||
for rerandomize in rerandomize_values:
|
||||
module = CapaFactory.create(rerandomize=rerandomize, done=True)
|
||||
# Try to save
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Try to save
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that we succeed
|
||||
self.assertTrue('success' in result and result['success'])
|
||||
# Expect that we succeed
|
||||
self.assertTrue('success' in result and result['success'])
|
||||
|
||||
def test_check_button_name(self):
|
||||
|
||||
@@ -1066,7 +1072,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# If user submitted a problem but hasn't reset,
|
||||
# do NOT show the check button
|
||||
# Note: we can only reset when rerandomize="always" or "true"
|
||||
module = CapaFactory.create(rerandomize="always", done=True)
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True)
|
||||
self.assertFalse(module.should_show_check_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="true", done=True)
|
||||
@@ -1080,13 +1086,13 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# and we do NOT have a reset button, then we can show the check button
|
||||
# Setting rerandomize to "never" or "false" ensures that the reset button
|
||||
# is not shown
|
||||
module = CapaFactory.create(rerandomize="never", done=True)
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, done=True)
|
||||
self.assertTrue(module.should_show_check_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="false", done=True)
|
||||
self.assertTrue(module.should_show_check_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="per_student", done=True)
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.PER_STUDENT, done=True)
|
||||
self.assertTrue(module.should_show_check_button())
|
||||
|
||||
def test_should_show_reset_button(self):
|
||||
@@ -1101,30 +1107,36 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module = CapaFactory.create(attempts=attempts, max_attempts=attempts, done=True)
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If we're NOT randomizing, then do NOT show the reset button
|
||||
module = CapaFactory.create(rerandomize="never", done=True)
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If we're NOT randomizing, then do NOT show the reset button
|
||||
module = CapaFactory.create(rerandomize="per_student", done=True)
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If we're NOT randomizing, then do NOT show the reset button
|
||||
module = CapaFactory.create(rerandomize="false", done=True)
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If the user hasn't submitted an answer yet,
|
||||
# then do NOT show the reset button
|
||||
module = CapaFactory.create(done=False)
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# pre studio default value, DO show the reset button
|
||||
module = CapaFactory.create(rerandomize="always", done=True)
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True)
|
||||
self.assertTrue(module.should_show_reset_button())
|
||||
|
||||
# If survey question for capa (max_attempts = 0),
|
||||
# DO show the reset button
|
||||
module = CapaFactory.create(rerandomize="always", max_attempts=0, done=True)
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True)
|
||||
self.assertTrue(module.should_show_reset_button())
|
||||
|
||||
# If the question is not correct
|
||||
# DO show the reset button
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True, correct=False)
|
||||
self.assertTrue(module.should_show_reset_button())
|
||||
|
||||
# If the question is correct and randomization is never
|
||||
# DO not show the reset button
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, max_attempts=0, done=True, correct=True)
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If the question is correct and randomization is always
|
||||
# Show the reset button
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True, correct=True)
|
||||
self.assertTrue(module.should_show_reset_button())
|
||||
|
||||
# Don't show reset button if randomization is turned on and the question is not done
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, show_reset_button=False, done=False)
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# Show reset button if randomization is turned on and the problem is done
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, show_reset_button=False, done=True)
|
||||
self.assertTrue(module.should_show_reset_button())
|
||||
|
||||
def test_should_show_save_button(self):
|
||||
@@ -1140,7 +1152,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
# If user submitted a problem but hasn't reset, do NOT show the save button
|
||||
module = CapaFactory.create(rerandomize="always", done=True)
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True)
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="true", done=True)
|
||||
@@ -1149,27 +1161,27 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# If the user has unlimited attempts and we are not randomizing,
|
||||
# then do NOT show a save button
|
||||
# because they can keep using "Check"
|
||||
module = CapaFactory.create(max_attempts=None, rerandomize="never", done=False)
|
||||
module = CapaFactory.create(max_attempts=None, rerandomize=RANDOMIZATION.NEVER, done=False)
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(max_attempts=None, rerandomize="false", done=True)
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(max_attempts=None, rerandomize="per_student", done=True)
|
||||
module = CapaFactory.create(max_attempts=None, rerandomize=RANDOMIZATION.PER_STUDENT, done=True)
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
# pre-studio default, DO show the save button
|
||||
module = CapaFactory.create(rerandomize="always", done=False)
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=False)
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
# If we're not randomizing and we have limited attempts, then we can save
|
||||
module = CapaFactory.create(rerandomize="never", max_attempts=2, done=True)
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, max_attempts=2, done=True)
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="false", max_attempts=2, done=True)
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
module = CapaFactory.create(rerandomize="per_student", max_attempts=2, done=True)
|
||||
module = CapaFactory.create(rerandomize=RANDOMIZATION.PER_STUDENT, max_attempts=2, done=True)
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
# If survey question for capa (max_attempts = 0),
|
||||
@@ -1197,7 +1209,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# then show it even if we would ordinarily
|
||||
# require a reset first
|
||||
module = CapaFactory.create(force_save_button="true",
|
||||
rerandomize="always",
|
||||
rerandomize=RANDOMIZATION.ALWAYS,
|
||||
done=True)
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
@@ -1331,48 +1343,66 @@ class CapaModuleTest(unittest.TestCase):
|
||||
context = render_args[1]
|
||||
self.assertTrue(error_msg in context['problem']['html'])
|
||||
|
||||
def test_random_seed_no_change(self):
|
||||
@ddt.data(
|
||||
'false',
|
||||
'true',
|
||||
RANDOMIZATION.NEVER,
|
||||
RANDOMIZATION.PER_STUDENT,
|
||||
RANDOMIZATION.ALWAYS,
|
||||
RANDOMIZATION.ONRESET
|
||||
)
|
||||
def test_random_seed_no_change(self, rerandomize):
|
||||
|
||||
# Run the test for each possible rerandomize value
|
||||
for rerandomize in ['false', 'never',
|
||||
'per_student', 'always',
|
||||
'true', 'onreset']:
|
||||
module = CapaFactory.create(rerandomize=rerandomize)
|
||||
|
||||
# Get the seed
|
||||
# By this point, the module should have persisted the seed
|
||||
seed = module.seed
|
||||
self.assertTrue(seed is not None)
|
||||
module = CapaFactory.create(rerandomize=rerandomize)
|
||||
|
||||
# If we're not rerandomizing, the seed is always set
|
||||
# to the same value (1)
|
||||
if rerandomize in ['never']:
|
||||
self.assertEqual(seed, 1,
|
||||
msg="Seed should always be 1 when rerandomize='%s'" % rerandomize)
|
||||
# Get the seed
|
||||
# By this point, the module should have persisted the seed
|
||||
seed = module.seed
|
||||
self.assertTrue(seed is not None)
|
||||
|
||||
# Check the problem
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
module.check_problem(get_request_dict)
|
||||
# If we're not rerandomizing, the seed is always set
|
||||
# to the same value (1)
|
||||
if rerandomize == RANDOMIZATION.NEVER:
|
||||
self.assertEqual(seed, 1,
|
||||
msg="Seed should always be 1 when rerandomize='%s'" % rerandomize)
|
||||
|
||||
# Expect that the seed is the same
|
||||
self.assertEqual(seed, module.seed)
|
||||
# Check the problem
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
module.check_problem(get_request_dict)
|
||||
|
||||
# Save the problem
|
||||
module.save_problem(get_request_dict)
|
||||
# Expect that the seed is the same
|
||||
self.assertEqual(seed, module.seed)
|
||||
|
||||
# Expect that the seed is the same
|
||||
self.assertEqual(seed, module.seed)
|
||||
# Save the problem
|
||||
module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that the seed is the same
|
||||
self.assertEqual(seed, module.seed)
|
||||
|
||||
@ddt.data(
|
||||
'false',
|
||||
'true',
|
||||
RANDOMIZATION.NEVER,
|
||||
RANDOMIZATION.PER_STUDENT,
|
||||
RANDOMIZATION.ALWAYS,
|
||||
RANDOMIZATION.ONRESET
|
||||
)
|
||||
def test_random_seed_with_reset(self, rerandomize):
|
||||
"""
|
||||
Run the test for each possible rerandomize value
|
||||
"""
|
||||
|
||||
def test_random_seed_with_reset(self):
|
||||
|
||||
def _reset_and_get_seed(module):
|
||||
'''
|
||||
"""
|
||||
Reset the XModule and return the module's seed
|
||||
'''
|
||||
"""
|
||||
|
||||
# Simulate submitting an attempt
|
||||
# We need to do this, or reset_problem() will
|
||||
# fail with a complaint that we haven't submitted
|
||||
# fail because it won't re-randomize until the problem has been submitted
|
||||
# the problem yet.
|
||||
module.done = True
|
||||
|
||||
@@ -1397,45 +1427,83 @@ class CapaModuleTest(unittest.TestCase):
|
||||
break
|
||||
return success
|
||||
|
||||
# Run the test for each possible rerandomize value
|
||||
for rerandomize in ['never', 'false', 'per_student',
|
||||
'always', 'true', 'onreset']:
|
||||
module = CapaFactory.create(rerandomize=rerandomize)
|
||||
module = CapaFactory.create(rerandomize=rerandomize, done=True)
|
||||
|
||||
# Get the seed
|
||||
# By this point, the module should have persisted the seed
|
||||
seed = module.seed
|
||||
self.assertTrue(seed is not None)
|
||||
# Get the seed
|
||||
# By this point, the module should have persisted the seed
|
||||
seed = module.seed
|
||||
self.assertTrue(seed is not None)
|
||||
|
||||
# We do NOT want the seed to reset if rerandomize
|
||||
# is set to 'never' -- it should still be 1
|
||||
# The seed also stays the same if we're randomizing
|
||||
# 'per_student': the same student should see the same problem
|
||||
if rerandomize in ['never', 'false', 'per_student']:
|
||||
self.assertEqual(seed, _reset_and_get_seed(module))
|
||||
# We do NOT want the seed to reset if rerandomize
|
||||
# is set to 'never' -- it should still be 1
|
||||
# The seed also stays the same if we're randomizing
|
||||
# 'per_student': the same student should see the same problem
|
||||
if rerandomize in [RANDOMIZATION.NEVER,
|
||||
'false',
|
||||
RANDOMIZATION.PER_STUDENT]:
|
||||
self.assertEqual(seed, _reset_and_get_seed(module))
|
||||
|
||||
# Otherwise, we expect the seed to change
|
||||
# to another valid seed
|
||||
else:
|
||||
# Otherwise, we expect the seed to change
|
||||
# to another valid seed
|
||||
else:
|
||||
|
||||
# Since there's a small chance we might get the
|
||||
# same seed again, give it 5 chances
|
||||
# to generate a different seed
|
||||
success = _retry_and_check(5, lambda: _reset_and_get_seed(module) != seed)
|
||||
# Since there's a small chance we might get the
|
||||
# same seed again, give it 5 chances
|
||||
# to generate a different seed
|
||||
success = _retry_and_check(5, lambda: _reset_and_get_seed(module) != seed)
|
||||
|
||||
self.assertTrue(module.seed is not None)
|
||||
msg = 'Could not get a new seed from reset after 5 tries'
|
||||
self.assertTrue(success, msg)
|
||||
self.assertTrue(module.seed is not None)
|
||||
msg = 'Could not get a new seed from reset after 5 tries'
|
||||
self.assertTrue(success, msg)
|
||||
|
||||
def test_random_seed_bins(self):
|
||||
@ddt.data(
|
||||
'false',
|
||||
'true',
|
||||
RANDOMIZATION.NEVER,
|
||||
RANDOMIZATION.PER_STUDENT,
|
||||
RANDOMIZATION.ALWAYS,
|
||||
RANDOMIZATION.ONRESET
|
||||
)
|
||||
def test_random_seed_with_reset_question_unsubmitted(self, rerandomize):
|
||||
"""
|
||||
Run the test for each possible rerandomize value
|
||||
"""
|
||||
def _reset_and_get_seed(module):
|
||||
"""
|
||||
Reset the XModule and return the module's seed
|
||||
"""
|
||||
|
||||
# Reset the problem
|
||||
# By default, the problem is instantiated as unsubmitted
|
||||
module.reset_problem({})
|
||||
|
||||
# Return the seed
|
||||
return module.seed
|
||||
|
||||
module = CapaFactory.create(rerandomize=rerandomize, done=False)
|
||||
|
||||
# Get the seed
|
||||
# By this point, the module should have persisted the seed
|
||||
seed = module.seed
|
||||
self.assertTrue(seed is not None)
|
||||
|
||||
#the seed should never change because the student hasn't finished the problem
|
||||
self.assertEqual(seed, _reset_and_get_seed(module))
|
||||
|
||||
@ddt.data(
|
||||
RANDOMIZATION.ALWAYS,
|
||||
RANDOMIZATION.PER_STUDENT,
|
||||
'true',
|
||||
RANDOMIZATION.ONRESET
|
||||
)
|
||||
def test_random_seed_bins(self, rerandomize):
|
||||
# Assert that we are limiting the number of possible seeds.
|
||||
|
||||
# Check the conditions that generate random seeds
|
||||
for rerandomize in ['always', 'per_student', 'true', 'onreset']:
|
||||
# Get a bunch of seeds, they should all be in 0-999.
|
||||
for i in range(200):
|
||||
module = CapaFactory.create(rerandomize=rerandomize)
|
||||
assert 0 <= module.seed < 1000
|
||||
# Get a bunch of seeds, they should all be in 0-999.
|
||||
i = 200
|
||||
while i > 0:
|
||||
module = CapaFactory.create(rerandomize=rerandomize)
|
||||
assert 0 <= module.seed < 1000
|
||||
i -= 1
|
||||
|
||||
@patch('xmodule.capa_base.log')
|
||||
@patch('xmodule.capa_base.Progress')
|
||||
@@ -1765,7 +1833,7 @@ class TestProblemCheckTracking(unittest.TestCase):
|
||||
|
||||
def test_rerandomized_inputs(self):
|
||||
factory = CapaFactory
|
||||
module = factory.create(rerandomize='always')
|
||||
module = factory.create(rerandomize=RANDOMIZATION.ALWAYS)
|
||||
|
||||
answer_input_dict = {
|
||||
factory.input_key(2): '3.14'
|
||||
|
||||
@@ -185,13 +185,13 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
assertion(a_score, b_score)
|
||||
|
||||
start_advertised_settings = [
|
||||
# start, advertised, result, is_still_default
|
||||
('2012-12-02T12:00', None, 'Dec 02, 2012', False),
|
||||
('2012-12-02T12:00', '2011-11-01T12:00', 'Nov 01, 2011', False),
|
||||
('2012-12-02T12:00', 'Spring 2012', 'Spring 2012', False),
|
||||
('2012-12-02T12:00', 'November, 2011', 'November, 2011', False),
|
||||
(xmodule.course_module.CourseFields.start.default, None, 'TBD', True),
|
||||
(xmodule.course_module.CourseFields.start.default, 'January 2014', 'January 2014', False),
|
||||
# start, advertised, result, is_still_default, date_time_result
|
||||
('2012-12-02T12:00', None, 'Dec 02, 2012', False, u'Dec 02, 2012 at 12:00 UTC'),
|
||||
('2012-12-02T12:00', '2011-11-01T12:00', 'Nov 01, 2011', False, u'Nov 01, 2011 at 12:00 UTC'),
|
||||
('2012-12-02T12:00', 'Spring 2012', 'Spring 2012', False, 'Spring 2012'),
|
||||
('2012-12-02T12:00', 'November, 2011', 'November, 2011', False, 'November, 2011'),
|
||||
(xmodule.course_module.CourseFields.start.default, None, 'TBD', True, 'TBD'),
|
||||
(xmodule.course_module.CourseFields.start.default, 'January 2014', 'January 2014', False, 'January 2014'),
|
||||
]
|
||||
|
||||
@patch('xmodule.course_module.datetime.now')
|
||||
@@ -200,7 +200,15 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
for s in self.start_advertised_settings:
|
||||
d = get_dummy_course(start=s[0], advertised_start=s[1])
|
||||
print "Checking start=%s advertised=%s" % (s[0], s[1])
|
||||
self.assertEqual(d.start_date_text, s[2])
|
||||
self.assertEqual(d.start_datetime_text(), s[2])
|
||||
|
||||
@patch('xmodule.course_module.datetime.now')
|
||||
def test_start_date_time_text(self, gmtime_mock):
|
||||
gmtime_mock.return_value = NOW
|
||||
for setting in self.start_advertised_settings:
|
||||
course = get_dummy_course(start=setting[0], advertised_start=setting[1])
|
||||
print "Checking start=%s advertised=%s" % (setting[0], setting[1])
|
||||
self.assertEqual(course.start_datetime_text("DATE_TIME"), setting[4])
|
||||
|
||||
def test_start_date_is_default(self):
|
||||
for s in self.start_advertised_settings:
|
||||
@@ -242,10 +250,18 @@ class IsNewCourseTestCase(unittest.TestCase):
|
||||
def test_end_date_text(self):
|
||||
# No end date set, returns empty string.
|
||||
d = get_dummy_course('2012-12-02T12:00')
|
||||
self.assertEqual('', d.end_date_text)
|
||||
self.assertEqual('', d.end_datetime_text())
|
||||
|
||||
d = get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
|
||||
self.assertEqual('Sep 04, 2014', d.end_date_text)
|
||||
self.assertEqual('Sep 04, 2014', d.end_datetime_text())
|
||||
|
||||
def test_end_date_time_text(self):
|
||||
# No end date set, returns empty string.
|
||||
course = get_dummy_course('2012-12-02T12:00')
|
||||
self.assertEqual('', course.end_datetime_text("DATE_TIME"))
|
||||
|
||||
course = get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
|
||||
self.assertEqual('Sep 04, 2014 at 12:00 UTC', course.end_datetime_text("DATE_TIME"))
|
||||
|
||||
|
||||
class DiscussionTopicsTestCase(unittest.TestCase):
|
||||
|
||||
4
common/static/js/vendor/date.js
vendored
4
common/static/js/vendor/date.js
vendored
@@ -3,7 +3,7 @@
|
||||
* @author: Coolite Inc. http://www.coolite.com/
|
||||
* @date: 2008-05-13
|
||||
* @copyright: Copyright (c) 2006-2008, Coolite Inc. (http://www.coolite.com/). All rights reserved.
|
||||
* @license: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
|
||||
* @license: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
|
||||
* @website: http://www.datejs.com/
|
||||
*/
|
||||
Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|aft(er)?|from|hence)/i,subtract:/^(\-|bef(ore)?|ago)/i,yesterday:/^yes(terday)?/i,today:/^t(od(ay)?)?/i,tomorrow:/^tom(orrow)?/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^mn|min(ute)?s?/i,hour:/^h(our)?s?/i,week:/^w(eek)?s?/i,month:/^m(onth)?s?/i,day:/^d(ay)?s?/i,year:/^y(ear)?s?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt|utc)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a(?!u|p)|p)/i},timezones:[{name:"UTC",offset:"-000"},{name:"GMT",offset:"-000"},{name:"EST",offset:"-0500"},{name:"EDT",offset:"-0400"},{name:"CST",offset:"-0600"},{name:"CDT",offset:"-0500"},{name:"MST",offset:"-0700"},{name:"MDT",offset:"-0600"},{name:"PST",offset:"-0800"},{name:"PDT",offset:"-0700"}]};
|
||||
@@ -44,7 +44,7 @@ f(this.getUTCDate())+'T'+
|
||||
f(this.getUTCHours())+':'+
|
||||
f(this.getUTCMinutes())+':'+
|
||||
f(this.getUTCSeconds())+'Z"';};}
|
||||
$P._toString=$P.toString;$P.toString=function(format){var x=this;if(format&&format.length==1){var c=$C.formatPatterns;x.t=x.toString;switch(format){case"d":return x.t(c.shortDate);case"D":return x.t(c.longDate);case"F":return x.t(c.fullDateTime);case"m":return x.t(c.monthDay);case"r":return x.t(c.rfc1123);case"s":return x.t(c.sortableDateTime);case"t":return x.t(c.shortTime);case"T":return x.t(c.longTime);case"u":return x.t(c.universalSortableDateTime);case"y":return x.t(c.yearMonth);}}
|
||||
if(typeof $P._toString === 'undefined'){$P._toString=$P.toString;}$P.toString=function(format){var x=this;if(format&&format.length==1){var c=$C.formatPatterns;x.t=x.toString;switch(format){case"d":return x.t(c.shortDate);case"D":return x.t(c.longDate);case"F":return x.t(c.fullDateTime);case"m":return x.t(c.monthDay);case"r":return x.t(c.rfc1123);case"s":return x.t(c.sortableDateTime);case"t":return x.t(c.shortTime);case"T":return x.t(c.longTime);case"u":return x.t(c.universalSortableDateTime);case"y":return x.t(c.yearMonth);}}
|
||||
var ord=function(n){switch(n*1){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};return format?format.replace(/(\\)?(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|S)/g,function(m){if(m.charAt(0)==="\\"){return m.replace("\\","");}
|
||||
x.h=x.getHours;switch(m){case"hh":return p(x.h()<13?(x.h()===0?12:x.h()):(x.h()-12));case"h":return x.h()<13?(x.h()===0?12:x.h()):(x.h()-12);case"HH":return p(x.h());case"H":return x.h();case"mm":return p(x.getMinutes());case"m":return x.getMinutes();case"ss":return p(x.getSeconds());case"s":return x.getSeconds();case"yyyy":return p(x.getFullYear(),4);case"yy":return p(x.getFullYear());case"dddd":return $C.dayNames[x.getDay()];case"ddd":return $C.abbreviatedDayNames[x.getDay()];case"dd":return p(x.getDate());case"d":return x.getDate();case"MMMM":return $C.monthNames[x.getMonth()];case"MMM":return $C.abbreviatedMonthNames[x.getMonth()];case"MM":return p((x.getMonth()+1));case"M":return x.getMonth()+1;case"t":return x.h()<12?$C.amDesignator.substring(0,1):$C.pmDesignator.substring(0,1);case"tt":return x.h()<12?$C.amDesignator:$C.pmDesignator;case"S":return ord(x.getDate());default:return m;}}):this._toString();};}());
|
||||
(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,$N=Number.prototype;$P._orient=+1;$P._nth=null;$P._is=false;$P._same=false;$P._isSecond=false;$N._dateElement="day";$P.next=function(){this._orient=+1;return this;};$D.next=function(){return $D.today().next();};$P.last=$P.prev=$P.previous=function(){this._orient=-1;return this;};$D.last=$D.prev=$D.previous=function(){return $D.today().last();};$P.is=function(){this._is=true;return this;};$P.same=function(){this._same=true;this._isSecond=false;return this;};$P.today=function(){return this.same().day();};$P.weekday=function(){if(this._is){this._is=false;return(!this.is().sat()&&!this.is().sun());}
|
||||
|
||||
458
common/static/js/vendor/ova/share-annotator.js
vendored
458
common/static/js/vendor/ova/share-annotator.js
vendored
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Share Annotation Plugin v1.0 (https://github.com/danielcebrian/share-annotator)
|
||||
Share Annotation Plugin v1.1 (https://github.com/danielcebrian/share-annotator)
|
||||
Copyright (C) 2014 Daniel Cebrian Robles
|
||||
License: https://github.com/danielcebrian/share-annotator/blob/master/License.rst
|
||||
|
||||
@@ -29,13 +29,13 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
Share.prototype.options = {
|
||||
shareIn: ['facebook', 'twitter', 'email', 'google'],
|
||||
getUrl: {
|
||||
'facebook':function(title, link, noteText){
|
||||
'facebook': function(title, link, noteText) {
|
||||
return 'https://www.facebook.com/sharer/sharer.php?s=100&p[url]=' + link + '&p[title]=' + encodeURIComponent('Open Video Annotation') + '&p[summary]=' + noteText;
|
||||
},
|
||||
'twitter':function(title, link, noteText){
|
||||
'twitter': function(title, link, noteText) {
|
||||
return 'https://twitter.com/intent/tweet?original_referer=' + link + '&source=tweetbutton&url=' + link + "&via=OpenVideoAnnotation&text=" + encodeURIComponent('I want to share the following Open Video Annotation: ');
|
||||
},
|
||||
'google':function(title, link, noteText){
|
||||
'google': function(title, link, noteText) {
|
||||
return 'https://plus.google.com/share?url=' + link;
|
||||
},
|
||||
'email': function(title, link, noteText){
|
||||
@@ -46,8 +46,9 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
};
|
||||
|
||||
function Share(element, options) {
|
||||
if (typeof options!='undefined')
|
||||
this.options.shareIn = typeof options.shareIn!='undefined'?options.shareIn:this.options.shareIn;
|
||||
if (typeof options !== 'undefined') {
|
||||
this.options.shareIn = typeof options.shareIn !== 'undefined' ? options.shareIn : this.options.shareIn;
|
||||
}
|
||||
this.buildHTMLShareButton = __bind(this.buildHTMLShareButton, this);
|
||||
this.runningAPI = __bind(this.runningAPI, this);
|
||||
this.updateViewer = __bind(this.updateViewer, this);
|
||||
@@ -74,7 +75,6 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
var newfield = Annotator.$('<li class="annotator-item">' + this.buildHTMLShareButton('Share without saving:') + '</li>');
|
||||
Annotator.$(this.field).replaceWith(newfield);
|
||||
this.field=newfield[0];
|
||||
|
||||
// Create the actions for the buttons
|
||||
this.buttonsActions(this.field, 2, this.options.baseUrl); // 2 is the method of the API that will be for share without saving
|
||||
|
||||
@@ -94,25 +94,26 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
// Share button HTML
|
||||
Share.prototype.buildHTMLShareButton = function(title, id) {
|
||||
var title = title || '';
|
||||
var id = typeof id!='undefined'?'annotationId="' + id + '"':'';
|
||||
var titleText = title!=''?'<div class="share-text-annotator">' + title + '</div>':'';
|
||||
var id = typeof id !== 'undefined' ? 'annotationId="' + id + '"' : '';
|
||||
var titleText = title !== '' ? '<div class="share-text-annotator">' + title + '</div>' : '';
|
||||
var shareButton = '<div class="share-button-annotator share-button" ' + id + '></div>';
|
||||
var popup = '<div class="share-popup-overlay-bg" style="z-index:30000000000"><div class="share-popup"><div class="share-popup-items"></div><div class="close-btn">Close</div></div></div>';
|
||||
// checks to make sure that no popup overlay already exists (though hidden) and creates a new one if it does not exist
|
||||
if($('.share-popup-overlay-bg').length === 0)
|
||||
if ($('.share-popup-overlay-bg').length === 0) {
|
||||
$('.annotator-wrapper').append(popup);
|
||||
}
|
||||
return '<div class="share-container-annotator">' + titleText + shareButton + '</div>';
|
||||
}
|
||||
|
||||
// template for the design of the Share Plugin
|
||||
Share.prototype.buildHTMLPopup = function(title) {
|
||||
var buttons = '';
|
||||
if (typeof this.options.shareIn != 'undefined'){
|
||||
if (typeof this.options.shareIn !== 'undefined'){
|
||||
this.options.shareIn.forEach(function(item) {
|
||||
buttons += '<div class="share-' + item + '-annotator share-button">' + item.charAt(0).toUpperCase() + item.slice(1) + '</div>';
|
||||
});
|
||||
}
|
||||
this.uri = (typeof this.uri != 'undefined') ? this.uri : '';
|
||||
this.uri = (typeof this.uri !== 'undefined') ? this.uri : '';
|
||||
var title = '<div class="share-popup-title">' + title.replace(":", "") + '</div>';
|
||||
var copy = '<div class="share-popup-copy">Copy and Share:</div>';
|
||||
var uri = '<input type="text" class="share-popup-uri" onclick="javascript:this.select();" readonly="true" value="' + this.uri + '">';
|
||||
@@ -121,7 +122,7 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
}
|
||||
|
||||
// Create the actions for the buttons
|
||||
Share.prototype.buttonsActions = function(field, method, url, annotation) {
|
||||
Share.prototype.buttonsActions = function(field, method, url) {
|
||||
var share = this;
|
||||
|
||||
// hide popup when user clicks on close button
|
||||
@@ -139,18 +140,12 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
// Share button
|
||||
$(field).find('.share-button-annotator.share-button').click(function(event) {
|
||||
event.preventDefault(); // disable normal link function so that it doesn't refresh the page
|
||||
annotation = share.getAnnotationFromId(event.currentTarget.attributes.annotationid);
|
||||
var _field = this;
|
||||
var ovaId = annotation.id;
|
||||
var title;
|
||||
if (method == 1) {
|
||||
title = 'Share';
|
||||
} else {
|
||||
title = 'Share without saving';
|
||||
}
|
||||
var ovaId = $(this).attr('annotationId');
|
||||
var title = method === 1 ? 'Share' : 'Share without saving';
|
||||
|
||||
// share.uri will be useful for buildHTMLPopup functions
|
||||
share.uri = share.createAPIURL(method, ovaId, url, annotation);
|
||||
share.uri = share.createAPIURL(method, ovaId, url);
|
||||
|
||||
// display your popup
|
||||
$('.share-popup-overlay-bg').show();
|
||||
@@ -159,22 +154,23 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
$('.share-popup-items').html(share.buildHTMLPopup(title));
|
||||
|
||||
// buttons actions
|
||||
if (typeof share.options.shareIn!='undefined'){
|
||||
if (typeof share.options.shareIn !== 'undefined') {
|
||||
share.options.shareIn.forEach(function(item) {
|
||||
$('.share-' + item + '-annotator.share-button').click(function() {
|
||||
var url = share.createAPIURL(method, ovaId, url, annotation);
|
||||
var url = share.createAPIURL(method, ovaId, url);
|
||||
var title = "Sharing a annotation with Open Video Annotation";
|
||||
var link = encodeURIComponent(url);
|
||||
var noteText = share.getSource('ovaText');
|
||||
var finalUrl = '';
|
||||
if (method==1){
|
||||
if (method === 1) {
|
||||
var viewer = share.annotator.viewer;
|
||||
var textarea = $(viewer.element).find('div:first').html();
|
||||
noteText = encodeURIComponent(textarea);
|
||||
}
|
||||
finalUrl = typeof share.options.getUrl[item]!='undefined'?share.options.getUrl[item](title, link, noteText):'';
|
||||
if (typeof share.options.getUrl[item]!='undefined')
|
||||
finalUrl = typeof share.options.getUrl[item] !== 'undefined' ? share.options.getUrl[item](title, link, noteText) : '';
|
||||
if (typeof share.options.getUrl[item] !== 'undefined') {
|
||||
window.open(finalUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -182,34 +178,59 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
};
|
||||
|
||||
|
||||
Share.prototype.createAPIURL = function(method, ovaId, url, annotation) {
|
||||
Share.prototype.createAPIURL = function(method, ovaId, url) {
|
||||
var annotator = this.annotator;
|
||||
var editor = annotator.editor;
|
||||
var method = method || 1;
|
||||
var url = annotation.uri || window.location.href;
|
||||
var url = url || window.location.href;
|
||||
|
||||
url += (url.indexOf('?') >= 0)? '&' : '?';
|
||||
url += (url.indexOf('?') >= 0) ? '&' : '?';
|
||||
|
||||
if (method === 1){
|
||||
var ovaId = (typeof ovaId !='undefined') ? ovaId : '';
|
||||
if (method === 1) {
|
||||
var ovaId = (typeof ovaId !== 'undefined') ? ovaId : '';
|
||||
url += 'ovaId=' + ovaId;
|
||||
} else if (method === 2){
|
||||
var ovaStart = this.getSource('ovaStart');
|
||||
var ovaEnd = this.getSource('ovaEnd');
|
||||
var ovaText = this.getSource('ovaText');
|
||||
|
||||
url += 'ovaStart=' + ovaStart
|
||||
+ '&ovaEnd=' + ovaEnd
|
||||
+ '&ovaText=' + ovaText;
|
||||
if (typeof editor.VideoJS != 'undefined' && editor.VideoJS !== -1){// Video Annotation
|
||||
var ovaContainer = this.getSource('ovaContainer');
|
||||
var ovaSrc = this.getSource('ovaSrc');
|
||||
url += '&ovaContainer=' + ovaContainer
|
||||
+ '&ovaSrc=' + ovaSrc;
|
||||
} else {// Text Annotation
|
||||
var ovastartOffset = this.getSource('ovastartOffset');
|
||||
var ovaendOffset = this.getSource('ovaendOffset');
|
||||
url += '&ovastartOffset=' + ovastartOffset + '&ovaendOffset=' + ovaendOffset;
|
||||
} else if (method === 2) {
|
||||
var ovaText = this.getSource('ovaText') || " ";
|
||||
url += 'ovaText=' + ovaText;
|
||||
if (typeof editor.VideoJS !== 'undefined' && editor.VideoJS !== -1) { // Video Annotation
|
||||
var ovaStart = this.getSource('ovaStart') || " ";
|
||||
var ovaEnd = this.getSource('ovaEnd') || " ";
|
||||
var ovaContainer = this.getSource('ovaContainer') || " ";
|
||||
var ovaSrc = this.getSource('ovaSrc') || " ";
|
||||
url += '&ovaStart=' + ovaStart
|
||||
+ '&ovaEnd=' + ovaEnd
|
||||
+ '&ovaContainer=' + ovaContainer
|
||||
+ '&ovaSrc=' + ovaSrc;
|
||||
} else if (typeof editor.OpenSeaDragon !== 'undefined' && editor.OpenSeaDragon !== -1) { // Image Annotation
|
||||
var ovaLeft = this.getSource('ovaLeft') || " ";
|
||||
var ovaTop = this.getSource('ovaTop') || " ";
|
||||
var ovaWidth = this.getSource('ovaWidth') || " ";
|
||||
var ovaHeight = this.getSource('ovaHeight') || " ";
|
||||
var ovaLeftZoom = this.getSource('ovaLeftZoom') || " ";
|
||||
var ovaTopZoom = this.getSource('ovaTopZoom') || " ";
|
||||
var ovaWidthZoom = this.getSource('ovaWidthZoom') || " ";
|
||||
var ovaHeightZoom = this.getSource('ovaHeightZoom') || " ";
|
||||
var ovaContainer = this.getSource('ovaContainer') || " ";
|
||||
var ovaSrc = this.getSource('ovaSrc') || " ";
|
||||
url += '&ovaLeft=' + ovaLeft
|
||||
+ '&ovaTop=' + ovaTop
|
||||
+ '&ovaWidth=' + ovaWidth
|
||||
+ '&ovaHeight=' + ovaHeight
|
||||
+ '&ovaLeftZoom=' + ovaLeftZoom
|
||||
+ '&ovaTopZoom=' + ovaTopZoom
|
||||
+ '&ovaWidthZoom=' + ovaWidthZoom
|
||||
+ '&ovaHeightZoom=' + ovaHeightZoom
|
||||
+ '&ovaContainer=' + ovaContainer
|
||||
+ '&ovaSrc=' + ovaSrc;
|
||||
} else { // Text Annotation
|
||||
var ovaStart = this.getSource('ovaStart') || " ";
|
||||
var ovaEnd = this.getSource('ovaEnd') || " ";
|
||||
var ovastartOffset = this.getSource('ovastartOffset') || " ";
|
||||
var ovaendOffset = this.getSource('ovaendOffset') || " ";
|
||||
url += '&ovaStart=' + ovaStart
|
||||
+ '&ovaEnd=' + ovaEnd
|
||||
+ '&ovastartOffset=' + ovastartOffset
|
||||
+ '&ovaendOffset=' + ovaendOffset;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
@@ -217,36 +238,68 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
|
||||
Share.prototype.getSource = function(source) {
|
||||
var source = source || '';
|
||||
if (source == 'ovaId') {// method 1
|
||||
source=this.annotation.id;
|
||||
} else {// method 2
|
||||
if (source === 'ovaId') { // method 1
|
||||
source = this.annotation.id;
|
||||
} else { // method 2
|
||||
var annotator = this.annotator;
|
||||
var editor = annotator.editor;
|
||||
var textarea = $(editor.element).find('textarea')[0];
|
||||
|
||||
if (source == 'ovaText')
|
||||
if (source === 'ovaText') {
|
||||
source = textarea.value;
|
||||
}
|
||||
|
||||
if (typeof editor.VideoJS!='undefined' && editor.VideoJS !== -1){// Video Annotation
|
||||
if (source == 'ovaContainer')
|
||||
if (typeof editor.VideoJS !== 'undefined' && editor.VideoJS !== -1) { // Video Annotation
|
||||
if (source === 'ovaContainer') {
|
||||
source = editor.VideoJS;
|
||||
else if (source == 'ovaSrc')
|
||||
}
|
||||
else if (source === 'ovaSrc') {
|
||||
source = annotator.mplayer[editor.VideoJS].tech.options_.source.src;
|
||||
else if (source == 'ovaStart')
|
||||
} else if (source === 'ovaStart') {
|
||||
source = annotator.mplayer[editor.VideoJS].rangeslider.getValues().start;
|
||||
else if (source == 'ovaEnd')
|
||||
} else if (source === 'ovaEnd') {
|
||||
source = annotator.mplayer[editor.VideoJS].rangeslider.getValues().end;
|
||||
|
||||
} else {// Text Annotation
|
||||
}
|
||||
} else if (typeof editor.OpenSeaDragon !== 'undefined' && editor.OpenSeaDragon !== -1) { // Image Annotation
|
||||
var annotation = editor.annotation;
|
||||
if(source == 'ovastartOffset')
|
||||
source = annotation.ranges[0].startOffset;
|
||||
else if (source == 'ovaendOffset')
|
||||
source = annotation.ranges[0].endOffset;
|
||||
else if (source == 'ovaStart')
|
||||
source = annotation.ranges[0].start;
|
||||
else if (source == 'ovaEnd')
|
||||
source = annotation.ranges[0].end;
|
||||
if (source === 'ovaLeft') {
|
||||
source = annotator.osda.rectPosition ? annotator.osda.rectPosition.left : annotation.rangePosition.left;
|
||||
} else if (source === 'ovaTop') {
|
||||
source = annotator.osda.rectPosition ? annotator.osda.rectPosition.top : annotation.rangePosition.top;
|
||||
} else if (source === 'ovaWidth') {
|
||||
source = annotator.osda.rectPosition ? annotator.osda.rectPosition.width : annotation.rangePosition.width;
|
||||
} else if (source ==='ovaHeight') {
|
||||
source = annotator.osda.rectPosition ? annotator.osda.rectPosition.height : annotation.rangePosition.height;
|
||||
} else if (source === 'ovaLeftZoom') {
|
||||
source = annotator.osda.viewer.drawer.viewport.getBounds().x;
|
||||
} else if (source === 'ovaTopZoom') {
|
||||
source = annotator.osda.viewer.drawer.viewport.getBounds().y;
|
||||
} else if (source === 'ovaWidthZoom') {
|
||||
source = annotator.osda.viewer.drawer.viewport.getBounds().width;
|
||||
} else if (source === 'ovaHeightZoom') {
|
||||
source = annotator.osda.viewer.drawer.viewport.getBounds().height;
|
||||
} else if (source === 'ovaContainer') {
|
||||
source = annotator.osda.viewer.id;
|
||||
} else if (source === 'ovaSrc') {
|
||||
var source = annotator.osda.viewer.source;
|
||||
var tilesUrl = typeof source.tilesUrl !== 'undefined' ? source.tilesUrl : '';
|
||||
var functionUrl = typeof source.getTileUrl !== 'undefined' ? source.getTileUrl : '';
|
||||
source = tilesUrl !== '' ? tilesUrl : ('' + functionUrl).replace(/\s+/g, ' '); // - target.src (media source)
|
||||
}
|
||||
} else { // Text Annotation
|
||||
var annotation = editor.annotation;
|
||||
// if ranges is 0 then it is a comment
|
||||
if (annotation.ranges.length > 0) {
|
||||
if(source === 'ovastartOffset') {
|
||||
source = annotation.ranges[0].startOffset;
|
||||
} else if (source === 'ovaendOffset') {
|
||||
source = annotation.ranges[0].endOffset;
|
||||
} else if (source === 'ovaStart') {
|
||||
source = annotation.ranges[0].start;
|
||||
} else if (source === 'ovaEnd') {
|
||||
source = annotation.ranges[0].end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return encodeURIComponent(source);
|
||||
@@ -266,36 +319,54 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
var src = this.getParameterByName('ovaSrc'); // Method 2 (Obligatory)
|
||||
var text = this.getParameterByName('ovaText'); // Method 2
|
||||
var user = this.getParameterByName('ovaUser'); // Method 2
|
||||
var Left = this.getParameterByName('ovaLeft'); // Method 2
|
||||
var Top = this.getParameterByName('ovaTop'); // Method 2
|
||||
var Width = this.getParameterByName('ovaWidth'); // Method 2
|
||||
var Height = this.getParameterByName('ovaHeight'); // Method 2
|
||||
var leftZoom = this.getParameterByName('ovaLeftZoom'); // Method 2
|
||||
var topZoom = this.getParameterByName('ovaTopZoom'); // Method 2
|
||||
var widthZoom = this.getParameterByName('ovaWidthZoom'); // Method 2
|
||||
var heightZoom = this.getParameterByName('ovaHeightZoom'); // Method 2
|
||||
var startOffset = this.getParameterByName('ovastartOffset'); // Method 2
|
||||
var endOffset = this.getParameterByName('ovaendOffset');// Method 2
|
||||
var endOffset = this.getParameterByName('ovaendOffset'); // Method 2
|
||||
|
||||
// remove the variables from the url browser
|
||||
var stripped_url = top.location.href;
|
||||
if (ovaId != '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaId');
|
||||
if (start != '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaStart');
|
||||
if (end != '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaEnd');
|
||||
if (container != '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaContainer');
|
||||
if (src != '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaSrc');
|
||||
if (text != '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaText');
|
||||
if (user != '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaUser');
|
||||
if (startOffset != '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovastartOffset');
|
||||
if (endOffset != '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaendOffset');
|
||||
if (ovaId !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaId');
|
||||
if (start !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaStart');
|
||||
if (end !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaEnd');
|
||||
if (container !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaContainer');
|
||||
if (src !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaSrc');
|
||||
if (text !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaText');
|
||||
if (user !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaUser');
|
||||
if (Left !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaLeft');
|
||||
if (Top !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaTop');
|
||||
if (Width !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaWidth');
|
||||
if (Height !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaHeight');
|
||||
if (leftZoom !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaLeftZoom');
|
||||
if (topZoom !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaTopZoom');
|
||||
if (widthZoom !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaWidthZoom');
|
||||
if (heightZoom !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaHeightZoom');
|
||||
if (startOffset !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovastartOffset');
|
||||
if (endOffset !== '') stripped_url = this.removeVariableFromURL(stripped_url, 'ovaendOffset');
|
||||
window.history.pushState("object or string", "Title", stripped_url);
|
||||
|
||||
|
||||
// Method 1 API with the Id of the annotation
|
||||
// Example: http://danielcebrian.com/annotations/demo.html?&ovaId=wtva_SjnQb2HtqppDihKug
|
||||
if (ovaId != '') {
|
||||
$.extend(API, {method:1, ovaId:ovaId});
|
||||
if (ovaId !== '') {
|
||||
$.extend(API, {method: 1, ovaId: ovaId});
|
||||
}
|
||||
// Method 2 API with all the parameter to load the annotation
|
||||
// Example with video: http://danielcebrian.com/annotations/demo.html?ovaContainer=vid1&ovaSrc=http%3A%2F%2Fvideo-js.zencoder.com%2Foceans-clip.mp4&ovaStart=2&ovaEnd=10&ovaText=This%20is%20test&ovaUser=Test%20User
|
||||
// Example with text: http://danielcebrian.com/annotations/demo.html?ovaStart=%2Fp%5B1%5D&ovaEnd=%2Fp%5B1%5D&ovastartOffset=542&ovaendOffset=572&ovaText=API
|
||||
|
||||
if (start!='' && end!='' && container!='' && src!='') {// video api
|
||||
$.extend(API, {method:2, start:start, end:end, container:container, src:src, text:text, user:user});
|
||||
} else if(start!='' && end!='' && startOffset!='' && endOffset!='') {// text api
|
||||
$.extend(API, {method:2, start:start, end:end, startOffset:startOffset, endOffset:endOffset, text:text, user:user});
|
||||
if (start !== '' && end !== '' && container !== '' && src !== '') { // video api
|
||||
$.extend(API, {method: 2, start: start, end: end, container: container, src: src, text: text, user: user});
|
||||
} else if (Left !== '' && Top !== '' && Width !== '' && Height !== '' && leftZoom !== '' && topZoom !== '' && widthZoom !== '' && heightZoom !== '') { // image api
|
||||
$.extend(API, {method: 2, Left: Left, Top: Top, Width: Width, Height: Height, leftZoom: leftZoom, topZoom: topZoom, widthZoom: widthZoom, heightZoom: heightZoom, container: container, src: src, text: text, user: user});
|
||||
} else if (start !== '' && end !== '' && startOffset !== '' && endOffset !== '') { // text api
|
||||
$.extend(API, {method: 2, start: start, end: end, startOffset: startOffset, endOffset: endOffset, text: text, user: user});
|
||||
}
|
||||
return API;
|
||||
}
|
||||
@@ -303,30 +374,31 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
var wrapper = $('.annotator-wrapper').parent()[0];
|
||||
var mplayer;
|
||||
var osda;
|
||||
var self=this;
|
||||
var self = this;
|
||||
|
||||
// Set Annotator in wrapper to fix quick DOM
|
||||
$.data(wrapper, 'annotator', self.annotator);// Set the object in the span
|
||||
annotator = window.annotator = $.data(wrapper, 'annotator');
|
||||
mplayer = (typeof annotator.mplayer != 'undefined') ? annotator.mplayer : [];
|
||||
osda = (typeof annotator.osda != 'undefined') ? annotator.osda : [];
|
||||
mplayer = (typeof annotator.mplayer !== 'undefined') ? annotator.mplayer : [];
|
||||
osda = (typeof annotator.osda !== 'undefined') ? annotator.osda : [];
|
||||
|
||||
// Detect if the URL has an API element
|
||||
if (typeof API != 'undefined' && typeof API.method != 'undefined' && (API.method == '1' || API.method == '2')) {
|
||||
if (API.method=='1'){
|
||||
if (typeof API !== 'undefined' && typeof API.method !== 'undefined' && (parseInt(API.method, 10) === 1 || parseInt(API.method, 10) === 2)) {
|
||||
if (parseInt(API.method, 10) === 1) {
|
||||
var allannotations = annotator.plugins['Store'].annotations;
|
||||
var ovaId = decodeURIComponent(API.ovaId);
|
||||
var ovaId = parseInt(decodeURIComponent(API.ovaId), 10);
|
||||
|
||||
for (var item in allannotations) {
|
||||
var an = allannotations[item];
|
||||
if (typeof an.id!='undefined' && an.id == ovaId){// this is the annotation
|
||||
if (self._isVideo(an)){// It is a video
|
||||
if (typeof mplayer[an.target.container] != 'undefined'){
|
||||
var olditem;
|
||||
if (typeof an.id !== 'undefined' && parseInt(an.id, 10) === ovaId) { // this is the annotation
|
||||
if (self._isVideo(an)) {// It is a video
|
||||
if (typeof mplayer[an.target.container] !== 'undefined') {
|
||||
var player = mplayer[an.target.container];
|
||||
if (player.id_ == an.target.container){
|
||||
if (player.id_ === an.target.container) {
|
||||
var anFound = an;
|
||||
videojs(player.id_).ready(function(){
|
||||
if (player.techName != 'Youtube'){
|
||||
videojs(player.id_).ready(function() {
|
||||
if (player.techName !== 'Youtube') {
|
||||
player.preload('auto');
|
||||
}
|
||||
player.autoPlayAPI = anFound;
|
||||
@@ -334,49 +406,84 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (self._isImage(an)) {// It is a OpenSeaDragon Annotation
|
||||
var bounds = new OpenSeadragon.Rect(an.bounds.x, an.bounds.y, an.bounds.width, an.bounds.height);
|
||||
setTimeout(function() {
|
||||
osda.viewer.viewport.fitBounds(bounds, false);
|
||||
$('html, body').animate( {
|
||||
scrollTop: $("#" + an.target.container).offset().top
|
||||
}, 'slow');
|
||||
}, 250);
|
||||
} else {// It is a text
|
||||
var hasRanges = typeof an.ranges != 'undefined' && typeof an.ranges[0] != 'undefined';
|
||||
var startOffset = hasRanges?an.ranges[0].startOffset:'';
|
||||
var endOffset = hasRanges?an.ranges[0].endOffset:'';
|
||||
} else if (an.media === "image") { // It is a OpenSeaDragon Annotation
|
||||
if ($("div#" + an.target.container).length) {
|
||||
var isOpenViewer = typeof annotator.osda !== "undefined" && typeof annotator.osda.viewer !== "undefined";
|
||||
function waitingOsda(){
|
||||
isOpenViewer = typeof annotator.osda !== "undefined" && typeof annotator.osda.viewer !== "undefined";
|
||||
if (typeof olditem === "undefined") {
|
||||
olditem = item;
|
||||
}
|
||||
if (!isOpenViewer) {
|
||||
setTimeout(waitingOsda, 200);
|
||||
} else {
|
||||
an = allannotations[olditem];
|
||||
$(an.highlights).parent().find('.annotator-hl').removeClass('api');
|
||||
// change the color
|
||||
$(an.highlights).addClass('api');
|
||||
// change zoom
|
||||
if (typeof annotator !== 'undefined' && typeof annotator.osda !== 'undefined') {
|
||||
var currentBounds = annotator.osda.viewer.drawer.viewport.getBounds();
|
||||
var bounds = typeof an.bounds !== 'undefined' ? an.bounds : {};
|
||||
|
||||
if (typeof bounds.x !== 'undefined') {
|
||||
currentBounds.x = bounds.x;
|
||||
}
|
||||
if (typeof bounds.y !== 'undefined') {
|
||||
currentBounds.y = bounds.y;
|
||||
}
|
||||
if (typeof bounds.width !== 'undefined') {
|
||||
currentBounds.width = bounds.width;
|
||||
}
|
||||
if (typeof bounds.height !== 'undefined') {
|
||||
currentBounds.height = bounds.height;
|
||||
}
|
||||
annotator.osda.viewer.drawer.viewport.fitBounds(currentBounds);
|
||||
}
|
||||
// animate to the annotation
|
||||
$('html,body').animate({
|
||||
scrollTop: $(annotator.osda.viewer.element).offset().top
|
||||
}, 'slow');
|
||||
}
|
||||
}
|
||||
waitingOsda();
|
||||
}
|
||||
} else { // It is a text
|
||||
var hasRanges = typeof an.ranges !== 'undefined' && typeof an.ranges[0] !== 'undefined';
|
||||
var startOffset = hasRanges ? an.ranges[0].startOffset : '';
|
||||
var endOffset = hasRanges ? an.ranges[0].endOffset : '';
|
||||
|
||||
if (typeof startOffset != 'undefined' && typeof endOffset != 'undefined'){
|
||||
if (typeof startOffset !== 'undefined' && typeof endOffset !== 'undefined') {
|
||||
// change the color
|
||||
$(an.highlights).addClass('api');
|
||||
// animate to the annotation
|
||||
$('html, body').animate({
|
||||
scrollTop: $(an.highlights[0]).offset().top}, 'slow');
|
||||
$('html,body').animate({
|
||||
scrollTop: $(an.highlights[0]).offset().top
|
||||
}, 'slow');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (API.method == '2'){
|
||||
if (typeof mplayer != 'undefined'){
|
||||
} else if (parseInt(API.method, 10) === 2) {
|
||||
if (typeof mplayer !== 'undefined') {
|
||||
// variable for Video
|
||||
var container = decodeURIComponent(API.container);
|
||||
var container = decodeURIComponent(API.container);
|
||||
var player = mplayer[container];
|
||||
var isVideo = (typeof player != 'undefined' && container == player.id_);
|
||||
var isVideo = (typeof player !== 'undefined' && container === player.id_);
|
||||
var isNumber = (!isNaN(parseFloat(API.start)) && isFinite(API.start) && !isNaN(parseFloat(API.end)) && isFinite(API.end));
|
||||
var isSource = false;
|
||||
|
||||
if (isVideo){
|
||||
if (isVideo) {
|
||||
// Compare without extension
|
||||
var src = decodeURIComponent(API.src);
|
||||
var targetSrc = src.substring(0, src.lastIndexOf("."));
|
||||
var playerSrc = (player.tech.options_.source.src == '') ? player.tag.currentSrc : player.tech.options_.source.src;
|
||||
playerSrc = playerSrc.substring(0, playerSrc.lastIndexOf("."))
|
||||
isSource = (targetSrc == playerSrc);
|
||||
var playerSrc = (player.tech.options_.source.src === '') ? player.tag.currentSrc : player.tech.options_.source.src;
|
||||
playerSrc = playerSrc.substring(0, playerSrc.lastIndexOf("."));
|
||||
isSource = (targetSrc === playerSrc);
|
||||
}
|
||||
|
||||
// Open Video Annotation
|
||||
if (isVideo && isNumber && isSource){
|
||||
if (isVideo && isNumber && isSource) {
|
||||
var annotation = {
|
||||
rangeTime: {
|
||||
start: API.start,
|
||||
@@ -393,7 +500,7 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
user: decodeURIComponent(API.user)
|
||||
};
|
||||
videojs(player.id_).ready(function(){
|
||||
if (player.techName != 'Youtube'){
|
||||
if (player.techName !== 'Youtube'){
|
||||
player.preload('auto');
|
||||
}
|
||||
player.autoPlayAPI = annotation;
|
||||
@@ -406,7 +513,7 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
var endOffset = API.endOffset;
|
||||
|
||||
// Text Annotation
|
||||
if (!isVideo && typeof startOffset != 'undefined' && typeof endOffset != 'undefined'){
|
||||
if (!isVideo && typeof startOffset !== 'undefined' && typeof endOffset !== 'undefined') {
|
||||
var annotation = {
|
||||
ranges: [{
|
||||
start: decodeURIComponent(API.start),
|
||||
@@ -426,8 +533,79 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
$(annotation.highlights).addClass('api');
|
||||
// animate to the annotation
|
||||
$('html, body').animate({
|
||||
scrollTop: $(annotation.highlights[0]).offset().top},
|
||||
'slow');
|
||||
scrollTop: $(annotation.highlights[0]).offset().top
|
||||
}, 'slow');
|
||||
}
|
||||
// variables for images
|
||||
var Left = API.Left;
|
||||
var Top = API.Top;
|
||||
var Width = API.Width;
|
||||
var Height = API.Height;
|
||||
var leftZoom = API.leftZoom;
|
||||
var topZoom = API.topZoom;
|
||||
var widthZoom = API.widthZoom;
|
||||
var heightZoom = API.heightZoom;
|
||||
|
||||
// Image Annotation
|
||||
if (!isVideo && typeof Left !== 'undefined' && typeof Top !== 'undefined' && typeof Width !== 'undefined' && typeof Height !== 'undefined' && typeof leftZoom !== 'undefined' && typeof topZoom !== 'undefined' && typeof widthZoom !== 'undefined' && typeof heightZoom !== 'undefined') {
|
||||
var an = {
|
||||
rangePosition: {
|
||||
width: parseFloat(decodeURIComponent(API.Width)),
|
||||
top: parseFloat(decodeURIComponent(API.Top)),
|
||||
left: parseFloat(decodeURIComponent(API.Left)),
|
||||
height: parseFloat(decodeURIComponent(API.Height)),
|
||||
},
|
||||
bounds: {
|
||||
width: parseFloat(decodeURIComponent(API.widthZoom)),
|
||||
x: parseFloat(decodeURIComponent(API.leftZoom)),
|
||||
y: parseFloat(decodeURIComponent(API.topZoom)),
|
||||
height: parseFloat(decodeURIComponent(API.heightZoom)),
|
||||
},
|
||||
target:{
|
||||
container: API.container,
|
||||
src: API.src
|
||||
},
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
media: 'image',
|
||||
text: decodeURIComponent(API.text),
|
||||
user: decodeURIComponent(API.user)
|
||||
};
|
||||
|
||||
var isOpenViewer = typeof annotator.osda !== "undefined" && typeof annotator.osda.viewer !== "undefined";
|
||||
function waitingOsda() {
|
||||
isOpenViewer = typeof annotator.osda !== "undefined" && typeof annotator.osda.viewer !== "undefined";
|
||||
if (!isOpenViewer) {
|
||||
setTimeout(waitingOsda, 200);
|
||||
} else {
|
||||
// show the annotation
|
||||
annotator.plugins['Store'].annotations.push(an);
|
||||
annotator.osda.viewer.annotationInstance.drawRect(an);
|
||||
// change the color
|
||||
$(an.highlights).addClass('api');
|
||||
// change zoom
|
||||
var currentBounds = annotator.osda.viewer.drawer.viewport.getBounds();
|
||||
var bounds = typeof an.bounds !== 'undefined' ? an.bounds : {};
|
||||
if (typeof bounds.x !== 'undefined') {
|
||||
currentBounds.x = bounds.x;
|
||||
}
|
||||
if (typeof bounds.y !== 'undefined') {
|
||||
currentBounds.y = bounds.y;
|
||||
}
|
||||
if (typeof bounds.width !== 'undefined') {
|
||||
currentBounds.width = bounds.width;
|
||||
}
|
||||
if (typeof bounds.height !== 'undefined') {
|
||||
currentBounds.height = bounds.height;
|
||||
}
|
||||
annotator.osda.viewer.drawer.viewport.fitBounds(currentBounds);
|
||||
// animate to the annotation
|
||||
$('html,body').animate({
|
||||
scrollTop: $(annotator.osda.viewer.element).offset().top
|
||||
}, 'slow');
|
||||
}
|
||||
}
|
||||
waitingOsda();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -447,25 +625,32 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
.subscribe("annotationsLoaded", func);
|
||||
}
|
||||
|
||||
Share.prototype._isVideo = function(an){
|
||||
Share.prototype._isVideo = function(an) {
|
||||
// Detect if the annotation is a Open Video Annotation
|
||||
var an = an || {};
|
||||
var rt = an.rangeTime;
|
||||
var isVideo = (typeof an.media != 'undefined' && an.media == 'video');
|
||||
var hasContainer = (typeof an.target != 'undefined' && typeof an.target.container != 'undefined' );
|
||||
var isNumber = (typeof rt != 'undefined' && !isNaN(parseFloat(rt.start)) && isFinite(rt.start) && !isNaN(parseFloat(rt.end)) && isFinite(rt.end));
|
||||
var isVideo = (typeof an.media !== 'undefined' && an.media === 'video');
|
||||
var hasContainer = (typeof an.target !== 'undefined' && typeof an.target.container !== 'undefined' );
|
||||
var isNumber = (typeof rt !== 'undefined' && !isNaN(parseFloat(rt.start)) && isFinite(rt.start) && !isNaN(parseFloat(rt.end)) && isFinite(rt.end));
|
||||
return (isVideo && hasContainer && isNumber);
|
||||
}
|
||||
|
||||
Share.prototype._isImage = function(annotation) {
|
||||
return annotation.media === 'image';
|
||||
var wrapper = $('.annotator-wrapper').parent()[0];
|
||||
var annotator = window.annotator = $.data(wrapper, 'annotator');
|
||||
var rp = an.rangePosition;
|
||||
var isOpenSeaDragon = (typeof annotator.osda !== 'undefined');
|
||||
var isContainer = (typeof an.target !== 'undefined' && typeof an.target.container !== 'undefined');
|
||||
var isImage = (typeof an.media !== 'undefined' && an.media === 'image');
|
||||
var isRP = (typeof rp !== 'undefined');
|
||||
return (isOpenSeaDragon && isContainer && isImage && isRP);
|
||||
}
|
||||
|
||||
|
||||
Share.prototype.getParameterByName = function(name) {
|
||||
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
|
||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
|
||||
var results = regex.exec('?' + window.location.href.split('?')[1]);
|
||||
return results == null ? "" : decodeURIComponent(results[1].replace(/\ + /g, " "));
|
||||
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
};
|
||||
|
||||
Share.prototype.removeVariableFromURL = function(url_string, variable_name) {
|
||||
@@ -478,13 +663,6 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
regex = null;
|
||||
return URL;
|
||||
}
|
||||
|
||||
Share.prototype.getAnnotationFromId = function(ovaId) {
|
||||
var annotationList = this.annotator.plugins.Store.annotations;
|
||||
return $.grep(annotationList, function(elementOfArray, indexInArray){
|
||||
return parseInt(elementOfArray.id, 10) === parseInt(ovaId.nodeValue, 10);
|
||||
})[0];
|
||||
}
|
||||
|
||||
Share.prototype.updateViewer = function(field, annotation) {
|
||||
this.annotation = annotation;
|
||||
@@ -492,15 +670,15 @@ Annotator.Plugin.Share = (function(_super) {
|
||||
var self = this;
|
||||
var field = $(field);
|
||||
var ret = field.addClass('share-viewer-annotator').html(function() {
|
||||
var string;
|
||||
return self.buildHTMLShareButton('Share:', self.getSource('ovaId'));
|
||||
});
|
||||
|
||||
// Create the actions for the buttons
|
||||
this.buttonsActions(field[0], 1, this.options.baseUrl, annotation); // 1 is the method of the API that will be for share some annotation in the database
|
||||
this.buttonsActions(field[0], 1, this.options.baseUrl); // 1 is the method of the API that will be for share some annotation in the database
|
||||
return ret;
|
||||
};
|
||||
|
||||
return Share;
|
||||
|
||||
})(Annotator.Plugin);
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<%block name="bodyclass">register verification-process step-select-track ${'is-upgrading' if upgrade else ''}</%block>
|
||||
<%block name="pagetitle">
|
||||
% if upgrade:
|
||||
${_("Upgrade Your Registration for {} | Choose Your Track").format(course_name)}
|
||||
${_("Upgrade Your Enrollment for {} | Choose Your Track").format(course_name)}
|
||||
% else:
|
||||
${_("Register for {} | Choose Your Track").format(course_name)}
|
||||
${_("Enroll In {} | Choose Your Track").format(course_name)}
|
||||
%endif
|
||||
</%block>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<div class=" msg msg-error">
|
||||
<i class="msg-icon icon-warning-sign"></i>
|
||||
<div class="msg-content">
|
||||
<h3 class="title">${_("Sorry, there was an error when trying to register you")}</h3>
|
||||
<h3 class="title">${_("Sorry, there was an error when trying to enroll you")}</h3>
|
||||
<div class="copy">
|
||||
<p>${error}</p>
|
||||
</div>
|
||||
@@ -104,7 +104,7 @@
|
||||
<ul class="list-actions">
|
||||
<li class="action action-select">
|
||||
% if upgrade:
|
||||
<input type="submit" name="verified_mode" value="${_('Upgrade Your Registration')}" />
|
||||
<input type="submit" name="verified_mode" value="${_('Upgrade Your Enrollment')}" />
|
||||
% else:
|
||||
<input type="submit" name="verified_mode" value="${_('Pursue a Verified Certificate')}" />
|
||||
% endif
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Course Advanced Settings page
|
||||
"""
|
||||
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from .course_page import CoursePage
|
||||
from .utils import press_the_notification_button, type_in_codemirror, get_codemirror_value
|
||||
|
||||
@@ -13,6 +14,7 @@ MODAL_SELECTOR = ".validation-error-modal-content"
|
||||
ERROR_ITEM_NAME_SELECTOR = ".error-item-title strong"
|
||||
ERROR_ITEM_CONTENT_SELECTOR = ".error-item-message"
|
||||
|
||||
|
||||
class AdvancedSettingsPage(CoursePage):
|
||||
"""
|
||||
Course Advanced Settings page.
|
||||
@@ -21,6 +23,10 @@ class AdvancedSettingsPage(CoursePage):
|
||||
url_path = "settings/advanced"
|
||||
|
||||
def is_browser_on_page(self):
|
||||
def _is_finished_loading():
|
||||
return len(self.q(css='.course-advanced-policy-list-item')) > 0
|
||||
|
||||
EmptyPromise(_is_finished_loading, 'Finished rendering the advanced policy items.').fulfill()
|
||||
return self.q(css='body.advanced').present
|
||||
|
||||
def wait_for_modal_load(self):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Course Group Configurations page.
|
||||
"""
|
||||
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from .course_page import CoursePage
|
||||
from .utils import confirm_prompt
|
||||
|
||||
@@ -14,7 +14,20 @@ class GroupConfigurationsPage(CoursePage):
|
||||
url_path = "group_configurations"
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='body.view-group-configurations').present
|
||||
"""
|
||||
Verify that the browser is on the page and it is not still loading.
|
||||
"""
|
||||
EmptyPromise(
|
||||
lambda: self.q(css='body.view-group-configurations').present,
|
||||
'On the group configuration page'
|
||||
).fulfill()
|
||||
|
||||
EmptyPromise(
|
||||
lambda: not self.q(css='span.spin').visible,
|
||||
'Group Configurations are finished loading'
|
||||
).fulfill()
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def group_configurations(self):
|
||||
|
||||
@@ -4,6 +4,7 @@ Utility methods useful for Studio page tests.
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from bok_choy.javascript import js_defined
|
||||
|
||||
from ...tests.helpers import disable_animations
|
||||
|
||||
@@ -50,6 +51,7 @@ def wait_for_notification(page):
|
||||
EmptyPromise(_is_saving_done, 'Notification should have been hidden.', timeout=60).fulfill()
|
||||
|
||||
|
||||
@js_defined('window.jQuery')
|
||||
def press_the_notification_button(page, name):
|
||||
# Because the notification uses a CSS transition,
|
||||
# Selenium will always report it as being visible.
|
||||
@@ -101,6 +103,7 @@ def add_advanced_component(page, menu_index, name):
|
||||
click_css(page, component_css, 0)
|
||||
|
||||
|
||||
@js_defined('window.jQuery')
|
||||
def type_in_codemirror(page, index, text, find_prefix="$"):
|
||||
script = """
|
||||
var cm = {find_prefix}('div.CodeMirror:eq({index})').get(0).CodeMirror;
|
||||
@@ -110,6 +113,7 @@ def type_in_codemirror(page, index, text, find_prefix="$"):
|
||||
page.browser.execute_script(script, str(text))
|
||||
|
||||
|
||||
@js_defined('window.jQuery')
|
||||
def get_codemirror_value(page, index=0, find_prefix="$"):
|
||||
return page.browser.execute_script(
|
||||
"""
|
||||
|
||||
Binary file not shown.
@@ -38,7 +38,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: edx-platform\n"
|
||||
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
|
||||
"POT-Creation-Date: 2014-10-20 10:16-0400\n"
|
||||
"POT-Creation-Date: 2014-10-27 10:50-0400\n"
|
||||
"PO-Revision-Date: 2014-06-02 13:44+0000\n"
|
||||
"Last-Translator: Sarina Canelake <sarina@edx.org>\n"
|
||||
"Language-Team: Amharic (http://www.transifex.com/projects/p/edx-platform/language/am/)\n"
|
||||
@@ -1275,6 +1275,16 @@ msgstr ""
|
||||
msgid "Whether to force the save button to appear on the page"
|
||||
msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/capa_base.py
|
||||
msgid "Show Reset Button"
|
||||
msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/capa_base.py
|
||||
msgid ""
|
||||
"Determines whether a 'Reset' button is shown so the user may reset their "
|
||||
"answer. A default value can be set in Advanced Settings."
|
||||
msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/capa_base.py
|
||||
#: common/lib/xmodule/xmodule/modulestore/inheritance.py
|
||||
msgid "Randomization"
|
||||
@@ -2890,9 +2900,12 @@ msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/modulestore/inheritance.py
|
||||
msgid ""
|
||||
"Enter the maximum number of times a student can try to answer problems. This"
|
||||
" is a course-wide setting, but you can specify a different number when you "
|
||||
"create an individual problem. To allow unlimited attempts, enter null."
|
||||
"Enter the maximum number of times a student can try to answer problems. By "
|
||||
"default, Maximum Attempts is set to null, meaning that students have an "
|
||||
"unlimited number of attempts for problems. You can override this course-wide"
|
||||
" setting for individual problems. However, if the course-wide setting is a "
|
||||
"specific number, you cannot set the Maximum Attempts for individual problems"
|
||||
" to unlimited."
|
||||
msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/modulestore/inheritance.py
|
||||
@@ -2928,6 +2941,17 @@ msgid ""
|
||||
"Enter true or false. If true, video caching will be used for HTML5 videos."
|
||||
msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/modulestore/inheritance.py
|
||||
msgid "Show Reset Button for Problems"
|
||||
msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/modulestore/inheritance.py
|
||||
msgid ""
|
||||
"Enter true or false. If true, problems default to displaying a 'Reset' "
|
||||
"button. This value may be overriden in each problem's settings. Existing "
|
||||
"problems whose reset setting have not been changed are affected."
|
||||
msgstr ""
|
||||
|
||||
#. Translators: "Self" is used to denote an openended response that is self-
|
||||
#. graded
|
||||
#: common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
|
||||
@@ -3404,6 +3428,9 @@ msgstr ""
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
#: lms/templates/open_ended_problems/open_ended_flagged_problems.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
@@ -3532,6 +3559,8 @@ msgid "Unable to switch to specified branch. Please check your branch name."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/dashboard/support.py
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Email Address"
|
||||
msgstr ""
|
||||
|
||||
@@ -3726,6 +3755,7 @@ msgstr ""
|
||||
|
||||
#: lms/djangoapps/dashboard/sysadmin.py lms/djangoapps/dashboard/sysadmin.py
|
||||
#: cms/templates/course-create-rerun.html cms/templates/index.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Course Name"
|
||||
msgstr ""
|
||||
|
||||
@@ -3847,6 +3877,34 @@ msgstr ""
|
||||
msgid "Task is already running."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid "Could not read uploaded file."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid ""
|
||||
"Data in row #{row_num} must have exactly four columns: email, username, full"
|
||||
" name, and country"
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid "Invalid email {email_address}."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid ""
|
||||
"An account with email {email} exists but the provided username {username} is"
|
||||
" different. Enrolling anyway with {email}."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid "Username {user} already exists."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid "File is not attached."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid "Invoice number '{0}' does not exist."
|
||||
msgstr ""
|
||||
@@ -4671,6 +4729,12 @@ msgstr ""
|
||||
msgid "Order Payment Confirmation"
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/shoppingcart/models.py
|
||||
msgid ""
|
||||
"Confirmation and Registration Codes for the following courses: "
|
||||
"{course_name_list}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/shoppingcart/models.py
|
||||
msgid "Trying to add a different currency into the cart"
|
||||
msgstr ""
|
||||
@@ -4685,6 +4749,10 @@ msgid ""
|
||||
"new course."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/shoppingcart/models.py
|
||||
msgid "Enrollment codes for Course: {course_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/shoppingcart/models.py
|
||||
msgid "[Refund] User-Requested Refund"
|
||||
msgstr ""
|
||||
@@ -4781,8 +4849,6 @@ msgstr ""
|
||||
|
||||
#: lms/djangoapps/shoppingcart/reports.py
|
||||
#: lms/templates/instructor/instructor_dashboard_2/set_course_mode_price_modal.html
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Currency"
|
||||
msgstr ""
|
||||
|
||||
@@ -4790,8 +4856,6 @@ msgstr ""
|
||||
#: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/e-commerce.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/edit_coupon_modal.html
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: wiki/plugins/attachments/forms.py
|
||||
@@ -4923,6 +4987,10 @@ msgstr ""
|
||||
msgid "Coupon '{0}' is not valid for any course in the shopping cart."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/shoppingcart/views.py
|
||||
msgid "success"
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/shoppingcart/views.py
|
||||
msgid "You do not have permission to view this page."
|
||||
msgstr ""
|
||||
@@ -6683,18 +6751,14 @@ msgstr ""
|
||||
msgid "Current Courses"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard.html
|
||||
msgid "Looks like you haven't registered for any courses yet."
|
||||
#: lms/templates/dashboard.html lms/templates/dashboard.html
|
||||
msgid "Looks like you haven't enrolled in any courses yet."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard.html
|
||||
msgid "Find courses now!"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard.html
|
||||
msgid "Looks like you haven't been enrolled in any courses yet."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard.html
|
||||
msgid "Course-loading errors"
|
||||
msgstr ""
|
||||
@@ -6787,7 +6851,8 @@ msgstr ""
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid "Unregister"
|
||||
#: lms/templates/instructor/instructor_dashboard_2/membership.html
|
||||
msgid "Unenroll"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/edit_unit_link.html
|
||||
@@ -6998,7 +7063,6 @@ msgstr ""
|
||||
|
||||
#: lms/templates/forgot_password_modal.html
|
||||
#: lms/templates/forgot_password_modal.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html
|
||||
msgid "Password Reset"
|
||||
msgstr ""
|
||||
|
||||
@@ -7697,6 +7761,7 @@ msgstr ""
|
||||
|
||||
#: lms/templates/register.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/generate_registarion_codes_modal.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "City"
|
||||
msgstr ""
|
||||
|
||||
@@ -7706,6 +7771,7 @@ msgstr ""
|
||||
|
||||
#: lms/templates/register.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/generate_registarion_codes_modal.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Country"
|
||||
msgstr ""
|
||||
|
||||
@@ -8975,6 +9041,8 @@ msgid "Day"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Students"
|
||||
msgstr ""
|
||||
|
||||
@@ -9091,16 +9159,16 @@ msgid "Access Courseware"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/courseware/mktg_course_about.html
|
||||
msgid "You Are Registered"
|
||||
msgid "You Are Enrolled"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/courseware/mktg_course_about.html
|
||||
msgid "Register for"
|
||||
msgid "Enroll in"
|
||||
msgstr ""
|
||||
|
||||
#. Translators: This is the second line on a button users can click. The
|
||||
#. first
|
||||
#. line is "Register for COURSE_NAME"
|
||||
#. line is "Enroll in COURSE_NAME"
|
||||
#. The "choose your student track" means users can select between taking the
|
||||
#. course as an auditor, as a verified student, etc
|
||||
#: lms/templates/courseware/mktg_course_about.html
|
||||
@@ -9109,7 +9177,7 @@ msgstr ""
|
||||
|
||||
#. Translators: This is the second line on a button users can click. The
|
||||
#. first
|
||||
#. line is "Register for COURSE_NAME"
|
||||
#. line is "Enroll in COURSE_NAME"
|
||||
#. 'Verification' here refers to verifying one's identity in order to receive
|
||||
#. a
|
||||
#. verified certificate.
|
||||
@@ -9118,7 +9186,7 @@ msgid "and proceed to verification"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/courseware/mktg_course_about.html
|
||||
msgid "Registration Is Closed"
|
||||
msgid "Enrollment Is Closed"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/courseware/news.html
|
||||
@@ -9338,7 +9406,7 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid "unregister"
|
||||
msgid "unenroll"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
@@ -9360,12 +9428,12 @@ msgstr ""
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid "Are you sure you want to unregister from the purchased course"
|
||||
msgid "Are you sure you want to unenroll from the purchased course"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid "Are you sure you want to unregister from"
|
||||
msgid "Are you sure you want to unenroll from"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
@@ -9373,8 +9441,8 @@ msgstr ""
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid ""
|
||||
"Are you sure you want to unregister from the verified {cert_name_long} track"
|
||||
" of"
|
||||
"Are you sure you want to unenroll from the verified {cert_name_long} track "
|
||||
"of"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
@@ -9970,6 +10038,36 @@ msgstr ""
|
||||
msgid "Active Threads"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "Welcome to {course_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid ""
|
||||
"To get started, please visit https://{site_name}. The login information for "
|
||||
"your account follows."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "email: {email}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "password: {password}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "It is recommended that you change your password."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "Sincerely yours,"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "The {course_name} Team"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/activation_email.txt
|
||||
msgid "Thank you for signing up for {platform_name}."
|
||||
msgstr ""
|
||||
@@ -9992,6 +10090,8 @@ msgid "We hope you enjoy learning with {platform_name}!"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/activation_email.txt
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "The {platform_name} Team"
|
||||
msgstr ""
|
||||
|
||||
@@ -10059,6 +10159,131 @@ msgstr ""
|
||||
msgid "You have been invited to a beta test for {course_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Hi {name},"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Thank you for your purchase of "
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Your payment was successful."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid ""
|
||||
"If you have billing questions, please read the FAQ ({faq_url}) or contact "
|
||||
"{billing_email}."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "If you have billing questions, please contact {billing_email}."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"{order_placed_by} placed an order and mentioned your name as the "
|
||||
"Organization contact."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"{order_placed_by} placed an order and mentioned your name as the additional "
|
||||
"receipt recipient."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "The items in your order are:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Quantity - Description - Price"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Total billed to credit/debit card: {currency_symbol}{total_cost}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Company Name:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Purchase Order Number:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Company Contact Name:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Company Contact Email:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Recipient Name:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Recipient Email:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "#:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Order Number: {order_number}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"A CSV file of your registration URLs is attached. Please distribute "
|
||||
"registration URLs to each student planning to enroll using the email "
|
||||
"template below."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Warm regards,"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"(1) Register for an account at <a href='https://{site_name}' "
|
||||
">https://{site_name}</a>."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"(2) Once registered, copy the redeem URL and paste it in your web browser."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"(3) On the enrollment confirmation page, Click the 'Activate Enrollment "
|
||||
"Code' button. This will show the enrollment confirmation."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"(4) You should be able to click on 'view course' button or see your course "
|
||||
"on your student dashboard at <a "
|
||||
"href='https://{dashboard_url}'>https://{dashboard_url}</a>"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"(5) Course materials will not be available until the course start date."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/confirm_email_change.txt
|
||||
msgid ""
|
||||
"This is to confirm that you changed the e-mail associated with "
|
||||
@@ -10174,41 +10399,10 @@ msgid ""
|
||||
"{merchant_name}."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid ""
|
||||
"If you have billing questions, please read the FAQ ({faq_url}) or contact "
|
||||
"{billing_email}."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "If you have billing questions, please contact {billing_email}."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "-The {platform_name} Team"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Your order number is: {order_number}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "The items in your order are:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Quantity - Description - Price"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Total billed to credit/debit card: {currency_symbol}{total_cost}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "#:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/registration_codes_sale_email.txt
|
||||
msgid "Thank you for your purchase of {course_name}!"
|
||||
msgstr ""
|
||||
@@ -10479,6 +10673,7 @@ msgstr ""
|
||||
msgid "Skip"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/e-commerce.html
|
||||
@@ -10520,6 +10715,8 @@ msgid "Honor"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/course_info.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
@@ -10742,7 +10939,11 @@ msgid "Click to generate a CSV file for all sales records in this course"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/e-commerce.html
|
||||
msgid "Download All e-Commerce Sales"
|
||||
msgid "Download All Invoice Sales"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/e-commerce.html
|
||||
msgid "Download All Order Sales"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/e-commerce.html
|
||||
@@ -10941,6 +11142,7 @@ msgid "What is the company the seats were sold to?"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/generate_registarion_codes_modal.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Organization Contact"
|
||||
msgstr ""
|
||||
|
||||
@@ -11112,7 +11314,19 @@ msgid "Enroll"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/membership.html
|
||||
msgid "Unenroll"
|
||||
msgid "Register/Enroll Students"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/membership.html
|
||||
msgid ""
|
||||
"To register and enroll a list of users in this course, choose a CSV file "
|
||||
"that contains the following columns in this exact order: email, username, "
|
||||
"name, and country. Please include one student per row and do not include any"
|
||||
" headers, footers, or blank lines."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/membership.html
|
||||
msgid "Upload CSV"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/membership.html
|
||||
@@ -11735,6 +11949,59 @@ msgid ""
|
||||
"address you submitted. You should be receiving it shortly."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Billing Details"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid ""
|
||||
"You can proceed to payment at any point in time. Any additional information "
|
||||
"you provide will be included in your receipt."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Purchasing Organizational Details"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Purchasing organization"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Purchase order number (if any)"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "email@example.com"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Additional Receipt Recipient"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid ""
|
||||
"If no additional billing details are populated the payment confirmation will"
|
||||
" be sent to the user making the purchase"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Payment processing occurs on a separate secure site."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Your Shopping cart is currently empty."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "View Courses"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/download_report.html
|
||||
msgid "Download CSV Data"
|
||||
msgstr ""
|
||||
@@ -11774,68 +12041,149 @@ msgstr ""
|
||||
msgid "There was an error processing your order!"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
msgid "Your Shopping Cart"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
msgid "Your selected items:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Price"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Total Amount"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
msgid "You have selected no items for purchase."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Register for [Course Name] | Receipt (Order"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Thank you for your Purchase!"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "View Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid ""
|
||||
"Please print this receipt page for your records. You should also have "
|
||||
"received a receipt in your email."
|
||||
"You have successfully been enrolled for <b>{appended_course_names}</b>. The "
|
||||
"following receipt has been emailed to "
|
||||
"<strong>{appended_recipient_emails}</strong>"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "{platform_name} ({site_name}) Electronic Receipt"
|
||||
msgid ""
|
||||
"You have successfully purchased <b>{total_registration_codes} course "
|
||||
"registration codes</b> for <b>{appended_course_names}. </b>The following "
|
||||
"receipt has been emailed to <strong>{appended_recipient_emails}</strong>"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Order #"
|
||||
msgid ""
|
||||
"Please send each professional one of these unique registration codes to "
|
||||
"enroll into the course. The confirmation/receipt email you will receive has "
|
||||
"an example email template with directions for the individuals enrolling."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Date:"
|
||||
msgid "Enrollment Code"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Items ordered:"
|
||||
msgid "Enrollment Link"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Qty"
|
||||
#: lms/templates/shoppingcart/registration_code_receipt.html
|
||||
msgid "{course_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "URL"
|
||||
msgid "Invoice"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Unit Price"
|
||||
msgid "Date of purchase"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Print Receipt"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Billed To Details"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Company Name"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "{company_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Purchase Order Number"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "{customer_reference_number}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Company Contact Name"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "{company_contact_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Company Contact Email"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Recipient Name"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "{recipient_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Recipient Email"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Card Type"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Credit Card Number"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Address 1"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Address 2"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Registration for"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Course Dates"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid " {course_name} "
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Price per student:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Discount Applied:"
|
||||
msgstr ""
|
||||
|
||||
#. Translators: Please keep the "<del>" and "</del>" tags around your
|
||||
@@ -11845,10 +12193,6 @@ msgstr ""
|
||||
msgid "Note: items with strikethough like <del>this</del> have been refunded."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Billed To:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/registration_code_receipt.html
|
||||
msgid "Confirm Enrollment"
|
||||
msgstr ""
|
||||
@@ -11865,10 +12209,6 @@ msgstr ""
|
||||
msgid "course dates"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/registration_code_receipt.html
|
||||
msgid "{course_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/registration_code_receipt.html
|
||||
msgid "{start_date}"
|
||||
msgstr ""
|
||||
@@ -11907,6 +12247,72 @@ msgstr ""
|
||||
msgid "View Course ▸"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Registration for:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Course Dates:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Students:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "code has been applied"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Total:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid ""
|
||||
"After this purchase is complete, a receipt is generated with relative "
|
||||
"billing details and registration codes for students."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "After this purchase is complete,"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "will be enrolled in this course."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart_flow.html
|
||||
msgid "Shopping cart"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart_flow.html
|
||||
msgid "{platform_name} - Shopping Cart"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart_flow.html
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/verify_student/photo_reverification.html
|
||||
#: lms/templates/verify_student/photo_verification.html
|
||||
#: lms/templates/verify_student/reverification_confirmation.html
|
||||
#: lms/templates/verify_student/show_requirements.html
|
||||
#: lms/templates/verify_student/verified.html
|
||||
msgid "Review"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart_flow.html
|
||||
msgid "Payment"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart_flow.html
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/verify_student/photo_reverification.html
|
||||
#: lms/templates/verify_student/photo_verification.html
|
||||
#: lms/templates/verify_student/reverification_confirmation.html
|
||||
#: lms/templates/verify_student/show_requirements.html
|
||||
#: lms/templates/verify_student/verified.html
|
||||
msgid "Confirmation"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
msgid "Receipt (Order"
|
||||
msgstr ""
|
||||
@@ -11960,15 +12366,6 @@ msgstr ""
|
||||
msgid "Take ID Photo"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/verify_student/photo_reverification.html
|
||||
#: lms/templates/verify_student/photo_verification.html
|
||||
#: lms/templates/verify_student/reverification_confirmation.html
|
||||
#: lms/templates/verify_student/show_requirements.html
|
||||
#: lms/templates/verify_student/verified.html
|
||||
msgid "Review"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/verify_student/photo_verification.html
|
||||
#: lms/templates/verify_student/show_requirements.html
|
||||
@@ -11976,15 +12373,6 @@ msgstr ""
|
||||
msgid "Make Payment"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/verify_student/photo_reverification.html
|
||||
#: lms/templates/verify_student/photo_verification.html
|
||||
#: lms/templates/verify_student/reverification_confirmation.html
|
||||
#: lms/templates/verify_student/show_requirements.html
|
||||
#: lms/templates/verify_student/verified.html
|
||||
msgid "Confirmation"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
msgid "Congratulations! You are now verified on "
|
||||
msgstr ""
|
||||
@@ -12174,7 +12562,6 @@ msgstr ""
|
||||
msgid "Student Account"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/student_profile/index.html
|
||||
#: lms/templates/student_profile/index.html
|
||||
msgid "Student Profile"
|
||||
msgstr ""
|
||||
@@ -14336,34 +14723,6 @@ msgid ""
|
||||
"students' problem scores."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "There was an error during the upload process."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "There was an error while unpacking the file."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "There was an error while verifying the file you submitted."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "There was an error while importing the new course to our database."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "Your import has failed."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/import.html cms/templates/import.html
|
||||
msgid "Choose new file"
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "Your import is in progress; navigating away will abort it."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/index.html cms/templates/index.html
|
||||
#: cms/templates/widgets/header.html
|
||||
msgid "My Courses"
|
||||
@@ -15070,14 +15429,6 @@ msgid ""
|
||||
"new courses to take."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/settings_advanced.html
|
||||
msgid "Hide Deprecated Settings"
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/settings_advanced.html cms/templates/settings_advanced.html
|
||||
msgid "Show Deprecated Settings"
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/settings_advanced.html
|
||||
msgid "Your policy changes have been saved."
|
||||
msgstr ""
|
||||
@@ -15096,6 +15447,10 @@ msgid ""
|
||||
"familiar with their purpose."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/settings_advanced.html
|
||||
msgid "Show Deprecated Settings"
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/settings_advanced.html
|
||||
msgid "What do advanced settings do?"
|
||||
msgstr ""
|
||||
@@ -15190,10 +15545,6 @@ msgstr ""
|
||||
msgid "This block contains multiple components."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/textbooks.html
|
||||
msgid "You have unsaved changes. Do you really want to leave this page?"
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/textbooks.html
|
||||
msgid "New Textbook"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -26,7 +26,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: edx-platform\n"
|
||||
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
|
||||
"POT-Creation-Date: 2014-10-20 10:15-0400\n"
|
||||
"POT-Creation-Date: 2014-10-27 10:49-0400\n"
|
||||
"PO-Revision-Date: 2014-10-08 18:08+0000\n"
|
||||
"Last-Translator: Sarina Canelake <sarina@edx.org>\n"
|
||||
"Language-Team: Amharic (http://www.transifex.com/projects/p/edx-platform/language/am/)\n"
|
||||
@@ -39,7 +39,8 @@ msgstr ""
|
||||
|
||||
#. Translators: this is a message from the raw HTML editor displayed in the
|
||||
#. browser when a user needs to edit HTML
|
||||
#: cms/static/coffee/src/views/tabs.js
|
||||
#: cms/static/coffee/src/views/tabs.js cms/static/js/factories/manage_users.js
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
#: cms/static/js/views/course_info_update.js
|
||||
#: cms/static/js/views/modals/edit_xblock.js
|
||||
#: common/lib/xmodule/xmodule/js/src/html/edit.js
|
||||
@@ -50,7 +51,8 @@ msgstr ""
|
||||
#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-#
|
||||
#. Translators: this is a message from the raw HTML editor displayed in the
|
||||
#. browser when a user needs to edit HTML
|
||||
#: cms/static/coffee/src/views/tabs.js cms/static/js/views/asset.js
|
||||
#: cms/static/coffee/src/views/tabs.js cms/static/js/factories/export.js
|
||||
#: cms/static/js/factories/manage_users.js cms/static/js/views/asset.js
|
||||
#: cms/static/js/views/course_info_update.js
|
||||
#: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js
|
||||
#: cms/static/js/views/modals/base_modal.js
|
||||
@@ -75,7 +77,14 @@ msgstr ""
|
||||
msgid "This link will open in a new browser window/tab"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/views/asset.js
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
msgid "Unknown"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js cms/static/js/views/asset.js
|
||||
#: cms/static/js/views/group_configuration_item.js
|
||||
#: cms/static/js/views/show_textbook.js
|
||||
#: common/static/js/vendor/ova/catch/js/catch.js
|
||||
@@ -2391,6 +2400,18 @@ msgstr ""
|
||||
msgid "Error: You cannot remove yourself from the Instructor group!"
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/membership.js
|
||||
msgid "The following errors were generated:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/membership.js
|
||||
msgid "The following warnings were generated:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/membership.js
|
||||
msgid "All accounts were created successfully."
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/membership.js
|
||||
msgid "Error adding/removing users as beta testers."
|
||||
msgstr ""
|
||||
@@ -2715,12 +2736,6 @@ msgid ""
|
||||
" technical support if the problem persists."
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
msgid "Unknown"
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
msgid "Subject"
|
||||
msgstr ""
|
||||
@@ -2994,6 +3009,16 @@ msgstr ""
|
||||
msgid "Please enter a valid password"
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/js/student_account/account.js
|
||||
msgid ""
|
||||
"Password reset email sent. Follow the link in the email to change your "
|
||||
"password."
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/js/student_account/account.js
|
||||
msgid "We weren't able to send you a password reset email."
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/js/student_account/account.js
|
||||
msgid "Please check your email to confirm the change"
|
||||
msgstr ""
|
||||
@@ -3165,6 +3190,139 @@ msgstr ""
|
||||
msgid "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "There has been an error while exporting."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid ""
|
||||
"There has been a failure to export to XML at least one component. It is "
|
||||
"recommended that you go to the edit page and repair the error before "
|
||||
"attempting another export. Please check that all components on the page are "
|
||||
"valid and do not display any error messages."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "Correct failed component"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "Return to Export"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid ""
|
||||
"There has been a failure to export your course to XML. Unfortunately, we do "
|
||||
"not have specific enough information to assist you in identifying the failed"
|
||||
" component. It is recommended that you inspect your courseware to identify "
|
||||
"any components in error and try again."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "The raw error message is:"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "There has been an error with your export."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "Yes, take me to the main course page"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "There was an error during the upload process."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "There was an error while unpacking the file."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "There was an error while verifying the file you submitted."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "There was an error while importing the new course to our database."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "Your import has failed."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js cms/static/js/factories/import.js
|
||||
#: cms/static/js/views/import.js cms/static/js/views/import.js.c
|
||||
msgid "Choose new file"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "Your import is in progress; navigating away will abort it."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "A valid email address is required"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "You must enter a valid email address in order to add a new team member"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Return and add email address"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Already a course team member"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid ""
|
||||
"{email} is already on the “{course}” team. If you're trying to add a new "
|
||||
"member, please double-check the email address you provided."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Return to team listing"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Error adding user"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid ""
|
||||
"Are you sure you want to delete {email} from the course team for “{course}”?"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Error removing user"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "There was an error changing the user's role"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Try Again"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/settings_advanced.js
|
||||
msgid "Hide Deprecated Settings"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/settings_advanced.js
|
||||
msgid "Show Deprecated Settings"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/textbooks.js
|
||||
#: cms/static/js/views/pages/group_configurations.js
|
||||
msgid "You have unsaved changes. Do you really want to leave this page?"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/models/course.js cms/static/js/models/section.js
|
||||
msgid "You must specify a name"
|
||||
msgstr ""
|
||||
@@ -3363,10 +3521,6 @@ msgstr ""
|
||||
msgid "Deleting"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/views/import.js cms/static/js/views/import.js.c
|
||||
msgid "Choose new file"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/views/import.js
|
||||
msgid "Error importing course"
|
||||
msgstr ""
|
||||
@@ -3591,10 +3745,6 @@ msgstr ""
|
||||
msgid "Making Visible to Students…"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/views/pages/group_configurations.js
|
||||
msgid "You have unsaved changes. Do you really want to leave this page?"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/views/settings/advanced.js
|
||||
msgid ""
|
||||
"Your changes will not take effect until you save your progress. Take care "
|
||||
@@ -3834,6 +3984,10 @@ msgstr ""
|
||||
msgid "Change My Email Address"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/student_account/account.underscore
|
||||
msgid "Reset Password"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/student_profile/profile.underscore
|
||||
msgid "Full Name"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -63,6 +63,7 @@
|
||||
# Mahmoud Elkhateeb <eng.elkhteeb@gmail.com>, 2013
|
||||
# Mahmoud Elkhateeb <eng.elkhteeb@gmail.com>, 2013
|
||||
# may <may@qordoba.com>, 2014
|
||||
# mustafasadiq <mustafasadiq0@gmail.com>, 2014
|
||||
# Nabeel El-Dughailib <nabeel@qordoba.com>, 2014
|
||||
# Najwan Al Rousan <najwanrousan@gmail.com>, 2013-2014
|
||||
# Najwan Al Rousan <najwanrousan@gmail.com>, 2013
|
||||
@@ -113,7 +114,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: edx-platform\n"
|
||||
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
|
||||
"POT-Creation-Date: 2014-10-20 10:16-0400\n"
|
||||
"POT-Creation-Date: 2014-10-27 10:50-0400\n"
|
||||
"PO-Revision-Date: 2014-09-02 00:40+0000\n"
|
||||
"Last-Translator: Nabeel El-Dughailib <nabeel@qordoba.com>\n"
|
||||
"Language-Team: Arabic (http://www.transifex.com/projects/p/edx-platform/language/ar/)\n"
|
||||
@@ -1443,6 +1444,16 @@ msgstr "أبداً"
|
||||
msgid "Whether to force the save button to appear on the page"
|
||||
msgstr "اختيار فرض إظهار زرّ الحفظ على الصفحة "
|
||||
|
||||
#: common/lib/xmodule/xmodule/capa_base.py
|
||||
msgid "Show Reset Button"
|
||||
msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/capa_base.py
|
||||
msgid ""
|
||||
"Determines whether a 'Reset' button is shown so the user may reset their "
|
||||
"answer. A default value can be set in Advanced Settings."
|
||||
msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/capa_base.py
|
||||
#: common/lib/xmodule/xmodule/modulestore/inheritance.py
|
||||
msgid "Randomization"
|
||||
@@ -3294,13 +3305,13 @@ msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/modulestore/inheritance.py
|
||||
msgid ""
|
||||
"Enter the maximum number of times a student can try to answer problems. This"
|
||||
" is a course-wide setting, but you can specify a different number when you "
|
||||
"create an individual problem. To allow unlimited attempts, enter null."
|
||||
"Enter the maximum number of times a student can try to answer problems. By "
|
||||
"default, Maximum Attempts is set to null, meaning that students have an "
|
||||
"unlimited number of attempts for problems. You can override this course-wide"
|
||||
" setting for individual problems. However, if the course-wide setting is a "
|
||||
"specific number, you cannot set the Maximum Attempts for individual problems"
|
||||
" to unlimited."
|
||||
msgstr ""
|
||||
"إدخال الحدّ الأقصى لعدد المرّات التي يسمح فيها للطالب بمحاولة الإجابة على "
|
||||
"المسائل. إنّه إعداد على نطاق المساق، ولكن يمكنك تحديد رقم مختلف عندما تقوم "
|
||||
"بإنشاء مسألة فردية. للسماح بعدد محاولات غير محدّد، الرجاء إدخال \"لا يوجد\"."
|
||||
|
||||
#: common/lib/xmodule/xmodule/modulestore/inheritance.py
|
||||
msgid "Matlab API key"
|
||||
@@ -3345,6 +3356,17 @@ msgstr ""
|
||||
"الرجاء إدخال صواب أو خطأ، في حال الصواب سيتمّ اعتماد التخزين المؤثت لمقاطع "
|
||||
"الفيديو بلغة HTML5."
|
||||
|
||||
#: common/lib/xmodule/xmodule/modulestore/inheritance.py
|
||||
msgid "Show Reset Button for Problems"
|
||||
msgstr ""
|
||||
|
||||
#: common/lib/xmodule/xmodule/modulestore/inheritance.py
|
||||
msgid ""
|
||||
"Enter true or false. If true, problems default to displaying a 'Reset' "
|
||||
"button. This value may be overriden in each problem's settings. Existing "
|
||||
"problems whose reset setting have not been changed are affected."
|
||||
msgstr ""
|
||||
|
||||
#. Translators: "Self" is used to denote an openended response that is self-
|
||||
#. graded
|
||||
#: common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
|
||||
@@ -3883,6 +3905,9 @@ msgstr "حقوق الطبع والنشر "
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
#: lms/templates/open_ended_problems/open_ended_flagged_problems.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Name"
|
||||
msgstr "الاسم "
|
||||
|
||||
@@ -4225,14 +4250,8 @@ msgid "Loaded course {course_name}<br/>Errors:"
|
||||
msgstr "المساق الذي تمّ تحميله {course_name} <br/> الأخطاء:"
|
||||
|
||||
#: lms/djangoapps/dashboard/sysadmin.py lms/djangoapps/dashboard/sysadmin.py
|
||||
#: cms/templates/course-create-rerun.html cms/templates/index.html
|
||||
#, fuzzy
|
||||
msgid "Course Name"
|
||||
msgstr ""
|
||||
"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n"
|
||||
"اسم المساق \n"
|
||||
"#-#-#-#-# mako-studio.po (edx-platform) #-#-#-#-#\n"
|
||||
"اسم المساق"
|
||||
msgstr "اسم المساق "
|
||||
|
||||
#: lms/djangoapps/dashboard/sysadmin.py
|
||||
msgid "Directory/ID"
|
||||
@@ -4355,6 +4374,34 @@ msgstr "المستخدم غير موجود."
|
||||
msgid "Task is already running."
|
||||
msgstr "المهمة قيد التشغيل بالفعل."
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid "Could not read uploaded file."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid ""
|
||||
"Data in row #{row_num} must have exactly four columns: email, username, full"
|
||||
" name, and country"
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid "Invalid email {email_address}."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid ""
|
||||
"An account with email {email} exists but the provided username {username} is"
|
||||
" different. Enrolling anyway with {email}."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid "Username {user} already exists."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid "File is not attached."
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/instructor/views/api.py
|
||||
msgid "Invoice number '{0}' does not exist."
|
||||
msgstr "رقم الفاتورة '{0}' غير موجود."
|
||||
@@ -5289,6 +5336,12 @@ msgstr ""
|
||||
msgid "Order Payment Confirmation"
|
||||
msgstr "تأكيد الدفع لطلب الشراء"
|
||||
|
||||
#: lms/djangoapps/shoppingcart/models.py
|
||||
msgid ""
|
||||
"Confirmation and Registration Codes for the following courses: "
|
||||
"{course_name_list}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/shoppingcart/models.py
|
||||
msgid "Trying to add a different currency into the cart"
|
||||
msgstr "محاولة إضافة عملة مختلفة لسلة التسوق"
|
||||
@@ -5305,6 +5358,10 @@ msgstr ""
|
||||
"يرجى زيارة <a href=\"{dashboard_link}\">لوحة المعلومات الخاصة بك</a> لمشاهدة"
|
||||
" مساقك الجديد."
|
||||
|
||||
#: lms/djangoapps/shoppingcart/models.py
|
||||
msgid "Enrollment codes for Course: {course_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/shoppingcart/models.py
|
||||
msgid "[Refund] User-Requested Refund"
|
||||
msgstr "[Refund] مبلغ الاسترداد الذي طلبه المستخدم "
|
||||
@@ -5413,8 +5470,6 @@ msgstr "التكلفة الإجمالية"
|
||||
|
||||
#: lms/djangoapps/shoppingcart/reports.py
|
||||
#: lms/templates/instructor/instructor_dashboard_2/set_course_mode_price_modal.html
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Currency"
|
||||
msgstr "العملة "
|
||||
|
||||
@@ -5422,8 +5477,6 @@ msgstr "العملة "
|
||||
#: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/e-commerce.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/edit_coupon_modal.html
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: wiki/plugins/attachments/forms.py
|
||||
@@ -5560,6 +5613,10 @@ msgstr "يُسمح باسترداد قسيمة واحدة فقط للطلب ال
|
||||
msgid "Coupon '{0}' is not valid for any course in the shopping cart."
|
||||
msgstr "ليست القسيمة '{0}' صالحة لإي مساق في عربة التسوّق."
|
||||
|
||||
#: lms/djangoapps/shoppingcart/views.py
|
||||
msgid "success"
|
||||
msgstr ""
|
||||
|
||||
#: lms/djangoapps/shoppingcart/views.py
|
||||
msgid "You do not have permission to view this page."
|
||||
msgstr "ليست لديك الصلاحيات اللازمة للاطلاع على مكونات هذه الصفحة."
|
||||
@@ -7580,18 +7637,14 @@ msgstr "إعادة ضبط كلمة السر"
|
||||
msgid "Current Courses"
|
||||
msgstr "المساقات الحالية"
|
||||
|
||||
#: lms/templates/dashboard.html
|
||||
msgid "Looks like you haven't registered for any courses yet."
|
||||
msgstr "يبدو أنك لم تقم بالتسجيل في أي من المساقات حتى الآن."
|
||||
#: lms/templates/dashboard.html lms/templates/dashboard.html
|
||||
msgid "Looks like you haven't enrolled in any courses yet."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard.html
|
||||
msgid "Find courses now!"
|
||||
msgstr "قم بإيجاد المساقات الآن!"
|
||||
|
||||
#: lms/templates/dashboard.html
|
||||
msgid "Looks like you haven't been enrolled in any courses yet."
|
||||
msgstr "يبدو أنك لم تقم بالتسجيل في أيٍ من المساقات حتى الآن. "
|
||||
|
||||
#: lms/templates/dashboard.html
|
||||
msgid "Course-loading errors"
|
||||
msgstr "حدوث أخطاء في تحميل المساق "
|
||||
@@ -7694,8 +7747,9 @@ msgstr ""
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid "Unregister"
|
||||
msgstr "إلغاء التسجيل"
|
||||
#: lms/templates/instructor/instructor_dashboard_2/membership.html
|
||||
msgid "Unenroll"
|
||||
msgstr "إلغاء التسجيل "
|
||||
|
||||
#: lms/templates/edit_unit_link.html
|
||||
msgid "View Unit in Studio"
|
||||
@@ -7819,7 +7873,7 @@ msgstr ""
|
||||
"الأحياء والأعمال والكيمياء وعلوم الكومبيوتر والاقتصاد والتمويل والإلكترونيات"
|
||||
" والهندسة وعلوم التغذية والتاريخ والعلوم الإنسانية والحقوق والآداب "
|
||||
"والرياضيات والطب والموسيقى والفلسفة والفيزياء والعلوم والإحصاء وغيرها. {EdX}"
|
||||
" هي مبادرة إلكترونية لاربحية أسسها الشركاء {HarvardX} و {MITx}."
|
||||
" هي مبادرة إلكترونية لاربحية أسسها الشركاء {Harvard} و {MIT}."
|
||||
|
||||
#: lms/templates/footer-edx-new.html lms/templates/footer-legacy.html
|
||||
msgid "© 2014 edX, some rights reserved."
|
||||
@@ -7920,7 +7974,6 @@ msgstr "© 2014 {platform_name}، بعض الحقوق محفوظة."
|
||||
|
||||
#: lms/templates/forgot_password_modal.html
|
||||
#: lms/templates/forgot_password_modal.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html
|
||||
msgid "Password Reset"
|
||||
msgstr "إعادة ضبط كلمة السر"
|
||||
|
||||
@@ -8682,6 +8735,7 @@ msgstr "معلومات شخصية إضافية"
|
||||
|
||||
#: lms/templates/register.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/generate_registarion_codes_modal.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "City"
|
||||
msgstr "المدينة"
|
||||
|
||||
@@ -8691,6 +8745,7 @@ msgstr "مثال: نيويورك"
|
||||
|
||||
#: lms/templates/register.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/generate_registarion_codes_modal.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Country"
|
||||
msgstr "الدولة"
|
||||
|
||||
@@ -10059,6 +10114,8 @@ msgid "Day"
|
||||
msgstr "اليوم "
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Students"
|
||||
msgstr "الطلاب "
|
||||
|
||||
@@ -10179,16 +10236,16 @@ msgid "Access Courseware"
|
||||
msgstr "الدخول لمنهاج المساق "
|
||||
|
||||
#: lms/templates/courseware/mktg_course_about.html
|
||||
msgid "You Are Registered"
|
||||
msgstr "أنت مسجَل "
|
||||
msgid "You Are Enrolled"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/courseware/mktg_course_about.html
|
||||
msgid "Register for"
|
||||
msgstr "قم بالتسجيل في "
|
||||
msgid "Enroll in"
|
||||
msgstr ""
|
||||
|
||||
#. Translators: This is the second line on a button users can click. The
|
||||
#. first
|
||||
#. line is "Register for COURSE_NAME"
|
||||
#. line is "Enroll in COURSE_NAME"
|
||||
#. The "choose your student track" means users can select between taking the
|
||||
#. course as an auditor, as a verified student, etc
|
||||
#: lms/templates/courseware/mktg_course_about.html
|
||||
@@ -10197,7 +10254,7 @@ msgstr "واختر مساقك كطالب"
|
||||
|
||||
#. Translators: This is the second line on a button users can click. The
|
||||
#. first
|
||||
#. line is "Register for COURSE_NAME"
|
||||
#. line is "Enroll in COURSE_NAME"
|
||||
#. 'Verification' here refers to verifying one's identity in order to receive
|
||||
#. a
|
||||
#. verified certificate.
|
||||
@@ -10206,8 +10263,8 @@ msgid "and proceed to verification"
|
||||
msgstr "وتابع لتستكمل التحقق"
|
||||
|
||||
#: lms/templates/courseware/mktg_course_about.html
|
||||
msgid "Registration Is Closed"
|
||||
msgstr "التسجيل مغلق "
|
||||
msgid "Enrollment Is Closed"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/courseware/news.html
|
||||
msgid "News - MITx 6.002x"
|
||||
@@ -10451,8 +10508,8 @@ msgstr ""
|
||||
"href=\"#\">التواصل مع صاحب الحساب </a>لطلب دفعة، أو يمكنك"
|
||||
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid "unregister"
|
||||
msgstr "إلغاء التسجيل"
|
||||
msgid "unenroll"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid "for this course."
|
||||
@@ -10473,24 +10530,22 @@ msgstr "استعراض المساق "
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid "Are you sure you want to unregister from the purchased course"
|
||||
msgstr "هل أنت متأكد من رغبتك بإلغاء تسجيلك من المساق الذي اشتركت فيه"
|
||||
msgid "Are you sure you want to unenroll from the purchased course"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid "Are you sure you want to unregister from"
|
||||
msgstr "هل أنت واثقٌ من رغبتك إلغاء تسجيلك بـ"
|
||||
msgid "Are you sure you want to unenroll from"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid ""
|
||||
"Are you sure you want to unregister from the verified {cert_name_long} track"
|
||||
" of"
|
||||
"Are you sure you want to unenroll from the verified {cert_name_long} track "
|
||||
"of"
|
||||
msgstr ""
|
||||
"هل أنت واثقٌ من رغبتك إلغاء تسجيلك من مسار الشهادات الموثّقة "
|
||||
"{cert_name_long} لـ"
|
||||
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
@@ -11115,6 +11170,36 @@ msgstr "عرض النقاش"
|
||||
msgid "Active Threads"
|
||||
msgstr "المواضيع الفعالة "
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "Welcome to {course_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid ""
|
||||
"To get started, please visit https://{site_name}. The login information for "
|
||||
"your account follows."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "email: {email}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "password: {password}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "It is recommended that you change your password."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "Sincerely yours,"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/account_creation_and_enroll_emailMessage.txt
|
||||
msgid "The {course_name} Team"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/activation_email.txt
|
||||
msgid "Thank you for signing up for {platform_name}."
|
||||
msgstr "شكرًا لتسجيلك في {platform_name}."
|
||||
@@ -11143,6 +11228,8 @@ msgid "We hope you enjoy learning with {platform_name}!"
|
||||
msgstr "نتمنى لك الاستمتاع بالتعلّم مع {platform_name}!"
|
||||
|
||||
#: lms/templates/emails/activation_email.txt
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "The {platform_name} Team"
|
||||
msgstr "فريق {platform_name}"
|
||||
|
||||
@@ -11226,6 +11313,137 @@ msgstr ""
|
||||
msgid "You have been invited to a beta test for {course_name}"
|
||||
msgstr "لقد تمت دعوتك لإجراء اختبار بيتا للمساق {course_name}"
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Hi {name},"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Thank you for your purchase of "
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Your payment was successful."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid ""
|
||||
"If you have billing questions, please read the FAQ ({faq_url}) or contact "
|
||||
"{billing_email}."
|
||||
msgstr ""
|
||||
"في حال كان لديك أية استفسارات تتعلق بالفواتير والدفعات، الرجاء قراءة قسم "
|
||||
"الأسئلة الأكثر تكراراً على الرابط ({faq_url}) أو الإتصال ب {billing_email}"
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "If you have billing questions, please contact {billing_email}."
|
||||
msgstr ""
|
||||
"في حال كان لديك أية استفسارات تتعلق بالفواتير والدفعات، الرجاء الإتصال ب "
|
||||
"{billing_email}."
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"{order_placed_by} placed an order and mentioned your name as the "
|
||||
"Organization contact."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"{order_placed_by} placed an order and mentioned your name as the additional "
|
||||
"receipt recipient."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "The items in your order are:"
|
||||
msgstr "العناصر التي يشتمل عليها طلبك:"
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Quantity - Description - Price"
|
||||
msgstr "الكمية - الوصف - السعر"
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Total billed to credit/debit card: {currency_symbol}{total_cost}"
|
||||
msgstr ""
|
||||
"المجموع الذي تم اقتطاعه من بطاقة الائتمان/الخصم المباشر: "
|
||||
"{currency_symbol}{total_cost}"
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Company Name:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Purchase Order Number:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Company Contact Name:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Company Contact Email:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Recipient Name:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Recipient Email:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "#:"
|
||||
msgstr "#: "
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid "Order Number: {order_number}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"A CSV file of your registration URLs is attached. Please distribute "
|
||||
"registration URLs to each student planning to enroll using the email "
|
||||
"template below."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Warm regards,"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"(1) Register for an account at <a href='https://{site_name}' "
|
||||
">https://{site_name}</a>."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"(2) Once registered, copy the redeem URL and paste it in your web browser."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"(3) On the enrollment confirmation page, Click the 'Activate Enrollment "
|
||||
"Code' button. This will show the enrollment confirmation."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"(4) You should be able to click on 'view course' button or see your course "
|
||||
"on your student dashboard at <a "
|
||||
"href='https://{dashboard_url}'>https://{dashboard_url}</a>"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/business_order_confirmation_email.txt
|
||||
msgid ""
|
||||
"(5) Course materials will not be available until the course start date."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/confirm_email_change.txt
|
||||
msgid ""
|
||||
"This is to confirm that you changed the e-mail associated with "
|
||||
@@ -11374,47 +11592,10 @@ msgid ""
|
||||
msgstr ""
|
||||
"ستظهر الرسوم المسددة على البيان الخاص بك تحت اسم الشركة {merchant_name}."
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid ""
|
||||
"If you have billing questions, please read the FAQ ({faq_url}) or contact "
|
||||
"{billing_email}."
|
||||
msgstr ""
|
||||
"في حال كان لديك أية استفسارات تتعلق بالفواتير والدفعات، الرجاء قراءة قسم "
|
||||
"الأسئلة الأكثر تكراراً على الرابط ({faq_url}) أو الإتصال ب {billing_email}"
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "If you have billing questions, please contact {billing_email}."
|
||||
msgstr ""
|
||||
"في حال كان لديك أية استفسارات تتعلق بالفواتير والدفعات، الرجاء الإتصال ب "
|
||||
"{billing_email}."
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "-The {platform_name} Team"
|
||||
msgstr "فريق {platform_name} "
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Your order number is: {order_number}"
|
||||
msgstr "رقم طلبك هو : {order_number}"
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "The items in your order are:"
|
||||
msgstr "العناصر التي يشتمل عليها طلبك:"
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Quantity - Description - Price"
|
||||
msgstr "الكمية - الوصف - السعر"
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
msgid "Total billed to credit/debit card: {currency_symbol}{total_cost}"
|
||||
msgstr ""
|
||||
"المجموع الذي تم اقتطاعه من بطاقة الائتمان/الخصم المباشر: "
|
||||
"{currency_symbol}{total_cost}"
|
||||
|
||||
#: lms/templates/emails/order_confirmation_email.txt
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "#:"
|
||||
msgstr "#: "
|
||||
|
||||
#: lms/templates/emails/registration_codes_sale_email.txt
|
||||
msgid "Thank you for your purchase of {course_name}!"
|
||||
msgstr "شكرًا لشرائك {course_name}!"
|
||||
@@ -11730,6 +11911,7 @@ msgstr "إعطاء علامة تدل على محتوى غير مناسب لتت
|
||||
msgid "Skip"
|
||||
msgstr "تخطي "
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/e-commerce.html
|
||||
@@ -11771,6 +11953,8 @@ msgid "Honor"
|
||||
msgstr "منح"
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/course_info.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
msgid "Total"
|
||||
msgstr "المجموع "
|
||||
@@ -12027,8 +12211,12 @@ msgstr ""
|
||||
"انقر هنا لإنشاء ملف بصيغة CSV يحتوي على جميع سجلات المبيعات في هذا المساق"
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/e-commerce.html
|
||||
msgid "Download All e-Commerce Sales"
|
||||
msgstr "تنزيل جميع عمليات مبيعات مساق التجارة الإلكترونية"
|
||||
msgid "Download All Invoice Sales"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/e-commerce.html
|
||||
msgid "Download All Order Sales"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/e-commerce.html
|
||||
msgid "Enter the invoice number to invalidate or re-validate sale"
|
||||
@@ -12238,6 +12426,7 @@ msgid "What is the company the seats were sold to?"
|
||||
msgstr "ما هي الشركة التي بيعَت لها المقاعد؟"
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/generate_registarion_codes_modal.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Organization Contact"
|
||||
msgstr "موظف الاتصال في المنظمة"
|
||||
|
||||
@@ -12429,8 +12618,20 @@ msgid "Enroll"
|
||||
msgstr "تسجيل"
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/membership.html
|
||||
msgid "Unenroll"
|
||||
msgstr "إلغاء التسجيل "
|
||||
msgid "Register/Enroll Students"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/membership.html
|
||||
msgid ""
|
||||
"To register and enroll a list of users in this course, choose a CSV file "
|
||||
"that contains the following columns in this exact order: email, username, "
|
||||
"name, and country. Please include one student per row and do not include any"
|
||||
" headers, footers, or blank lines."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/membership.html
|
||||
msgid "Upload CSV"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/membership.html
|
||||
msgid "Batch Beta Tester Addition"
|
||||
@@ -13134,6 +13335,59 @@ msgstr ""
|
||||
"لقد قمنا بإرسال رسالةٍ على عنوان البريد الإلكتروني الذي زودتنا به تتضمن "
|
||||
"التعليمات الخاصة بضبط كلمة السر، وستصلك قريباً. "
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Billing Details"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid ""
|
||||
"You can proceed to payment at any point in time. Any additional information "
|
||||
"you provide will be included in your receipt."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Purchasing Organizational Details"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Purchasing organization"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Purchase order number (if any)"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "email@example.com"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Additional Receipt Recipient"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid ""
|
||||
"If no additional billing details are populated the payment confirmation will"
|
||||
" be sent to the user making the purchase"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
msgid "Payment processing occurs on a separate secure site."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Your Shopping cart is currently empty."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/billing_details.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "View Courses"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/download_report.html
|
||||
msgid "Download CSV Data"
|
||||
msgstr "تنزيل البيانات بصيغة CSV"
|
||||
@@ -13176,71 +13430,150 @@ msgstr "حدث خطأ في عملية الدفع "
|
||||
msgid "There was an error processing your order!"
|
||||
msgstr "لقد حدث خطأ لدى إجراء العملية الخاصة طلبك! "
|
||||
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
msgid "Your Shopping Cart"
|
||||
msgstr "سلة التسوق الخاصة بك "
|
||||
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
msgid "Your selected items:"
|
||||
msgstr "العناصر المختارة من قبلك: "
|
||||
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Price"
|
||||
msgstr "السعر "
|
||||
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Total Amount"
|
||||
msgstr "المبلغ الإجمالي "
|
||||
|
||||
#: lms/templates/shoppingcart/list.html
|
||||
msgid "You have selected no items for purchase."
|
||||
msgstr "لم تقم باختيار أي عنصر للشراء. "
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Register for [Course Name] | Receipt (Order"
|
||||
msgstr "قم بالتسجيل في [اسم المساق] | إيصال (الطلب"
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Thank you for your Purchase!"
|
||||
msgstr "شكرا لك على الشراء"
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid ""
|
||||
"Please print this receipt page for your records. You should also have "
|
||||
"received a receipt in your email."
|
||||
msgid "View Dashboard"
|
||||
msgstr ""
|
||||
"الرجاء طباعة صفحة الإيصال هذه للاحتفاظ بها في سجلاتك. يجب أن تكون قد استلمت "
|
||||
"أيضاً رسالة إلكترونية تحتوي على إيصال."
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "{platform_name} ({site_name}) Electronic Receipt"
|
||||
msgstr "إيصال إلكتروني من {platform_name} ({site_name})"
|
||||
msgid ""
|
||||
"You have successfully been enrolled for <b>{appended_course_names}</b>. The "
|
||||
"following receipt has been emailed to "
|
||||
"<strong>{appended_recipient_emails}</strong>"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Order #"
|
||||
msgstr "طلبية # "
|
||||
msgid ""
|
||||
"You have successfully purchased <b>{total_registration_codes} course "
|
||||
"registration codes</b> for <b>{appended_course_names}. </b>The following "
|
||||
"receipt has been emailed to <strong>{appended_recipient_emails}</strong>"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Date:"
|
||||
msgstr "التاريخ: "
|
||||
msgid ""
|
||||
"Please send each professional one of these unique registration codes to "
|
||||
"enroll into the course. The confirmation/receipt email you will receive has "
|
||||
"an example email template with directions for the individuals enrolling."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Items ordered:"
|
||||
msgstr "العناصر المطلوبة: "
|
||||
msgid "Enrollment Code"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Qty"
|
||||
msgstr "الكمية "
|
||||
msgid "Enrollment Link"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "URL"
|
||||
msgstr "الرابط"
|
||||
#: lms/templates/shoppingcart/registration_code_receipt.html
|
||||
msgid "{course_name}"
|
||||
msgstr "{course_name}"
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Unit Price"
|
||||
msgstr "سعر الوحدة "
|
||||
msgid "Invoice"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Date of purchase"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Print Receipt"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Billed To Details"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Company Name"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "{company_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Purchase Order Number"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "{customer_reference_number}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Company Contact Name"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "{company_contact_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Company Contact Email"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Recipient Name"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "{recipient_name}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Recipient Email"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Card Type"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Credit Card Number"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Address 1"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Address 2"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Registration for"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Course Dates"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid " {course_name} "
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Price per student:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Discount Applied:"
|
||||
msgstr ""
|
||||
|
||||
#. Translators: Please keep the "<del>" and "</del>" tags around your
|
||||
#. translation of the word "this" in your translation.
|
||||
@@ -13249,10 +13582,6 @@ msgstr "سعر الوحدة "
|
||||
msgid "Note: items with strikethough like <del>this</del> have been refunded."
|
||||
msgstr "ملاحظة: تمّ رد قيمة العناصر المشطوبة على النحو <del>هذا</del>."
|
||||
|
||||
#: lms/templates/shoppingcart/receipt.html
|
||||
msgid "Billed To:"
|
||||
msgstr "تم تسجيل الفاتورة لـ: "
|
||||
|
||||
#: lms/templates/shoppingcart/registration_code_receipt.html
|
||||
msgid "Confirm Enrollment"
|
||||
msgstr "تأكيد التسجيل"
|
||||
@@ -13269,10 +13598,6 @@ msgstr "يرجى تأكيد تسجيلك في:"
|
||||
msgid "course dates"
|
||||
msgstr "تواريخ المساق"
|
||||
|
||||
#: lms/templates/shoppingcart/registration_code_receipt.html
|
||||
msgid "{course_name}"
|
||||
msgstr "{course_name}"
|
||||
|
||||
#: lms/templates/shoppingcart/registration_code_receipt.html
|
||||
msgid "{start_date}"
|
||||
msgstr "{start_date}"
|
||||
@@ -13321,6 +13646,72 @@ msgstr ""
|
||||
msgid "View Course ▸"
|
||||
msgstr "عرض المساق ▸"
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Registration for:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Course Dates:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Students:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "code has been applied"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "Total:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid ""
|
||||
"After this purchase is complete, a receipt is generated with relative "
|
||||
"billing details and registration codes for students."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "After this purchase is complete,"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart.html
|
||||
msgid "will be enrolled in this course."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart_flow.html
|
||||
msgid "Shopping cart"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart_flow.html
|
||||
msgid "{platform_name} - Shopping Cart"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart_flow.html
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/verify_student/photo_reverification.html
|
||||
#: lms/templates/verify_student/photo_verification.html
|
||||
#: lms/templates/verify_student/reverification_confirmation.html
|
||||
#: lms/templates/verify_student/show_requirements.html
|
||||
#: lms/templates/verify_student/verified.html
|
||||
msgid "Review"
|
||||
msgstr "مراجعة "
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart_flow.html
|
||||
msgid "Payment"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/shoppingcart/shopping_cart_flow.html
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/verify_student/photo_reverification.html
|
||||
#: lms/templates/verify_student/photo_verification.html
|
||||
#: lms/templates/verify_student/reverification_confirmation.html
|
||||
#: lms/templates/verify_student/show_requirements.html
|
||||
#: lms/templates/verify_student/verified.html
|
||||
msgid "Confirmation"
|
||||
msgstr "تأكيد "
|
||||
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
msgid "Receipt (Order"
|
||||
msgstr "الإيصال (الطلب"
|
||||
@@ -13374,15 +13765,6 @@ msgstr "قم بأخذ صورة "
|
||||
msgid "Take ID Photo"
|
||||
msgstr "قم بأخذ صورة بطاقة شخصية "
|
||||
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/verify_student/photo_reverification.html
|
||||
#: lms/templates/verify_student/photo_verification.html
|
||||
#: lms/templates/verify_student/reverification_confirmation.html
|
||||
#: lms/templates/verify_student/show_requirements.html
|
||||
#: lms/templates/verify_student/verified.html
|
||||
msgid "Review"
|
||||
msgstr "مراجعة "
|
||||
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/verify_student/photo_verification.html
|
||||
#: lms/templates/verify_student/show_requirements.html
|
||||
@@ -13390,15 +13772,6 @@ msgstr "مراجعة "
|
||||
msgid "Make Payment"
|
||||
msgstr "القيام بالدفع"
|
||||
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
#: lms/templates/verify_student/photo_reverification.html
|
||||
#: lms/templates/verify_student/photo_verification.html
|
||||
#: lms/templates/verify_student/reverification_confirmation.html
|
||||
#: lms/templates/verify_student/show_requirements.html
|
||||
#: lms/templates/verify_student/verified.html
|
||||
msgid "Confirmation"
|
||||
msgstr "تأكيد "
|
||||
|
||||
#: lms/templates/shoppingcart/verified_cert_receipt.html
|
||||
msgid "Congratulations! You are now verified on "
|
||||
msgstr "مبروك! تم التحقق من هويتك على "
|
||||
@@ -13483,9 +13856,9 @@ msgid ""
|
||||
"{link_start}homepage{link_end} or let us know about any pages that may have "
|
||||
"been moved at {email}."
|
||||
msgstr ""
|
||||
"لم يتم إيجاد الصفحة التي كنت تبحث عنها، الرجاء العودة إلى {link_start}الصفحة"
|
||||
" الرئيسية{link_end}، أو إعلامنا عبر البريد الإلكتروني {email} عن أية صفحات "
|
||||
"يحتمل يكون قد تغير موقعها."
|
||||
"عذراً، الصفحة التي تبحث عنها غير متوفرة. الرجاء إعلامنا عبر البريد "
|
||||
"الإلكتروني {email} إن كنت تعتقد انها مشكلة من العنوان المستخدم. يمكنك العودة"
|
||||
" الى {link_start}الصفحة الرئيسية{link_end}."
|
||||
|
||||
#: lms/templates/static_templates/about.html
|
||||
#: lms/templates/static_templates/blog.html
|
||||
@@ -13615,7 +13988,6 @@ msgstr ""
|
||||
msgid "Student Account"
|
||||
msgstr "حساب الطالب"
|
||||
|
||||
#: lms/templates/student_profile/index.html
|
||||
#: lms/templates/student_profile/index.html
|
||||
msgid "Student Profile"
|
||||
msgstr "صفحة الطالب"
|
||||
@@ -16049,35 +16421,6 @@ msgstr ""
|
||||
"url_name) مكوِّن للمسائل، قد يتمّ فقدان بيانات الطالب المرتبطة بمكوِّنات "
|
||||
"المسائل هذه. وتتضمّن هذه البيانات المسائل غير المقيّمة الخاصة بالطالب."
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "There was an error during the upload process."
|
||||
msgstr "حدث خطأ أثناء عملية التحميل. "
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "There was an error while unpacking the file."
|
||||
msgstr "حدث خطأ أثناء عملية تفريغ الملف. "
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "There was an error while verifying the file you submitted."
|
||||
msgstr "حدث خطأ أثناء عملية التحقق من الملف الذي قمت بتقديمه. "
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "There was an error while importing the new course to our database."
|
||||
msgstr "حدث خطأ خلال عملية استيراد المساق الجديد إلى قاعدة البيانات لدينا. "
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "Your import has failed."
|
||||
msgstr "لم تنجح عملية الاستيراد"
|
||||
|
||||
#: cms/templates/import.html cms/templates/import.html
|
||||
msgid "Choose new file"
|
||||
msgstr "اختيار ملف جديد "
|
||||
|
||||
#: cms/templates/import.html
|
||||
msgid "Your import is in progress; navigating away will abort it."
|
||||
msgstr ""
|
||||
"جاري عمل عملية الاستيراد؛ سيؤدّي تصفّح موقعٍ مختلف إلى إيقاف العملية. "
|
||||
|
||||
#: cms/templates/index.html cms/templates/index.html
|
||||
#: cms/templates/widgets/header.html
|
||||
msgid "My Courses"
|
||||
@@ -16883,14 +17226,6 @@ msgstr ""
|
||||
"ومتطلّبات الوقت التقريبية. ويستخدم الطلاب صفحات \"النبذة\" لاختيار المساقات "
|
||||
"الجديدة التي سيلتحقون بها."
|
||||
|
||||
#: cms/templates/settings_advanced.html
|
||||
msgid "Hide Deprecated Settings"
|
||||
msgstr "إخفاء الإعدادات المهملة"
|
||||
|
||||
#: cms/templates/settings_advanced.html cms/templates/settings_advanced.html
|
||||
msgid "Show Deprecated Settings"
|
||||
msgstr "إظهار الإعدادات المهملة"
|
||||
|
||||
#: cms/templates/settings_advanced.html
|
||||
msgid "Your policy changes have been saved."
|
||||
msgstr "تم حفظ التغييرات على السياسة الخاصة بك."
|
||||
@@ -16911,6 +17246,10 @@ msgstr ""
|
||||
"<strong> تحذير </strong>: لا تقم بتعديل هذه السياسات إلا إذا كنت على علمٍ "
|
||||
"بأغراضها."
|
||||
|
||||
#: cms/templates/settings_advanced.html
|
||||
msgid "Show Deprecated Settings"
|
||||
msgstr "إظهار الإعدادات المهملة"
|
||||
|
||||
#: cms/templates/settings_advanced.html
|
||||
msgid "What do advanced settings do?"
|
||||
msgstr "ما هي وظيفة الإعدادات المتقدّمة؟"
|
||||
@@ -17022,10 +17361,6 @@ msgstr "تكبير أو تصغير"
|
||||
msgid "This block contains multiple components."
|
||||
msgstr "تحتوي هذه الخانة على عدة مكوِّنات "
|
||||
|
||||
#: cms/templates/textbooks.html
|
||||
msgid "You have unsaved changes. Do you really want to leave this page?"
|
||||
msgstr "ثّمة بعض التغييرات التي لم يتمّ حفظها. هل تريد حقّاً مغادرة الصفحة؟"
|
||||
|
||||
#: cms/templates/textbooks.html
|
||||
msgid "New Textbook"
|
||||
msgstr "كتاب جديد"
|
||||
|
||||
Binary file not shown.
@@ -67,7 +67,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: edx-platform\n"
|
||||
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
|
||||
"POT-Creation-Date: 2014-10-20 10:15-0400\n"
|
||||
"POT-Creation-Date: 2014-10-27 10:49-0400\n"
|
||||
"PO-Revision-Date: 2014-10-13 16:51+0000\n"
|
||||
"Last-Translator: Nabeel El-Dughailib <nabeel@qordoba.com>\n"
|
||||
"Language-Team: Arabic (http://www.transifex.com/projects/p/edx-platform/language/ar/)\n"
|
||||
@@ -80,7 +80,8 @@ msgstr ""
|
||||
|
||||
#. Translators: this is a message from the raw HTML editor displayed in the
|
||||
#. browser when a user needs to edit HTML
|
||||
#: cms/static/coffee/src/views/tabs.js
|
||||
#: cms/static/coffee/src/views/tabs.js cms/static/js/factories/manage_users.js
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
#: cms/static/js/views/course_info_update.js
|
||||
#: cms/static/js/views/modals/edit_xblock.js
|
||||
#: common/lib/xmodule/xmodule/js/src/html/edit.js
|
||||
@@ -91,7 +92,8 @@ msgstr "حسناً"
|
||||
#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-#
|
||||
#. Translators: this is a message from the raw HTML editor displayed in the
|
||||
#. browser when a user needs to edit HTML
|
||||
#: cms/static/coffee/src/views/tabs.js cms/static/js/views/asset.js
|
||||
#: cms/static/coffee/src/views/tabs.js cms/static/js/factories/export.js
|
||||
#: cms/static/js/factories/manage_users.js cms/static/js/views/asset.js
|
||||
#: cms/static/js/views/course_info_update.js
|
||||
#: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js
|
||||
#: cms/static/js/views/modals/base_modal.js
|
||||
@@ -121,7 +123,14 @@ msgstr ""
|
||||
"#-#-#-#-# underscore-studio.po (edx-platform) #-#-#-#-#\n"
|
||||
"سيُفتح هذا الرابط في نافذة متصفّح/علامة تبويب جديدة"
|
||||
|
||||
#: cms/static/js/views/asset.js
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
msgid "Unknown"
|
||||
msgstr "غير معروف "
|
||||
|
||||
#: cms/static/js/factories/manage_users.js cms/static/js/views/asset.js
|
||||
#: cms/static/js/views/group_configuration_item.js
|
||||
#: cms/static/js/views/show_textbook.js
|
||||
#: common/static/js/vendor/ova/catch/js/catch.js
|
||||
@@ -2179,7 +2188,7 @@ msgstr[1] "انقر مربع الاختيار لإزالة إبلاغ %(واحد
|
||||
msgstr[2] "انقر مربع الاختيار لإزالة إبلاغين %(اثنين)s"
|
||||
msgstr[3] "انقر مربع الاختيار لإزالة %(بضعة)s إبلاغات"
|
||||
msgstr[4] "انقر مربع الاختيار لإزالة %(عدة)s إبلاغات"
|
||||
msgstr[5] "انقر مربع الاختيار لإزالة %(totalFlags)s إبلاغات"
|
||||
msgstr[5] "انقر مربع الاختيار لإزالة %(count)s إبلاغات"
|
||||
|
||||
#: common/static/js/vendor/ova/flagging-annotator.js
|
||||
msgid "All flags have been removed. To undo, uncheck the box."
|
||||
@@ -2583,6 +2592,18 @@ msgstr ""
|
||||
msgid "Error: You cannot remove yourself from the Instructor group!"
|
||||
msgstr "حدث خطأ: لا يمكنك حذف نفسك من مجموعة موجّهي المساق!"
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/membership.js
|
||||
msgid "The following errors were generated:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/membership.js
|
||||
msgid "The following warnings were generated:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/membership.js
|
||||
msgid "All accounts were created successfully."
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/membership.js
|
||||
msgid "Error adding/removing users as beta testers."
|
||||
msgstr ""
|
||||
@@ -2973,12 +2994,6 @@ msgstr ""
|
||||
"لقد وقع خطأ في استرداد بريدك الإلكتروني، يرجى المحاولة مجددًا لاحقًا. اتصل "
|
||||
"بالدعم التقني في حال استمرار المشكلة."
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
msgid "Unknown"
|
||||
msgstr "غير معروف "
|
||||
|
||||
#: lms/static/coffee/src/instructor_dashboard/util.js
|
||||
msgid "Subject"
|
||||
msgstr "الموضوع"
|
||||
@@ -3252,6 +3267,16 @@ msgstr "يرجى إدخال بريد إلكتروني صالح"
|
||||
msgid "Please enter a valid password"
|
||||
msgstr "يرجى إدخال كلمة سر صالحة"
|
||||
|
||||
#: lms/static/js/student_account/account.js
|
||||
msgid ""
|
||||
"Password reset email sent. Follow the link in the email to change your "
|
||||
"password."
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/js/student_account/account.js
|
||||
msgid "We weren't able to send you a password reset email."
|
||||
msgstr ""
|
||||
|
||||
#: lms/static/js/student_account/account.js
|
||||
msgid "Please check your email to confirm the change"
|
||||
msgstr "يرجى التحقق من بريدك الإلكتروني لتأكيد التغيير"
|
||||
@@ -3455,6 +3480,139 @@ msgstr "المجموعة %s"
|
||||
msgid "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
msgstr "أ ب ت ث ج ح خ د ذ ر ز س ش ص ض ط ظ ع غ ف ق ك ل م ن هـ و ي"
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "There has been an error while exporting."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid ""
|
||||
"There has been a failure to export to XML at least one component. It is "
|
||||
"recommended that you go to the edit page and repair the error before "
|
||||
"attempting another export. Please check that all components on the page are "
|
||||
"valid and do not display any error messages."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "Correct failed component"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "Return to Export"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid ""
|
||||
"There has been a failure to export your course to XML. Unfortunately, we do "
|
||||
"not have specific enough information to assist you in identifying the failed"
|
||||
" component. It is recommended that you inspect your courseware to identify "
|
||||
"any components in error and try again."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "The raw error message is:"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "There has been an error with your export."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/export.js
|
||||
msgid "Yes, take me to the main course page"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "There was an error during the upload process."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "There was an error while unpacking the file."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "There was an error while verifying the file you submitted."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "There was an error while importing the new course to our database."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "Your import has failed."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/import.js cms/static/js/factories/import.js
|
||||
#: cms/static/js/views/import.js cms/static/js/views/import.js.c
|
||||
msgid "Choose new file"
|
||||
msgstr "اختر ملف جديد"
|
||||
|
||||
#: cms/static/js/factories/import.js
|
||||
msgid "Your import is in progress; navigating away will abort it."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "A valid email address is required"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "You must enter a valid email address in order to add a new team member"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Return and add email address"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Already a course team member"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid ""
|
||||
"{email} is already on the “{course}” team. If you're trying to add a new "
|
||||
"member, please double-check the email address you provided."
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Return to team listing"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Error adding user"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid ""
|
||||
"Are you sure you want to delete {email} from the course team for “{course}”?"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Error removing user"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "There was an error changing the user's role"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/manage_users.js
|
||||
msgid "Try Again"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/settings_advanced.js
|
||||
msgid "Hide Deprecated Settings"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/settings_advanced.js
|
||||
msgid "Show Deprecated Settings"
|
||||
msgstr ""
|
||||
|
||||
#: cms/static/js/factories/textbooks.js
|
||||
#: cms/static/js/views/pages/group_configurations.js
|
||||
msgid "You have unsaved changes. Do you really want to leave this page?"
|
||||
msgstr "هناك تغييرات غير محفوظة، هل تريد مغادرة الصفحة؟"
|
||||
|
||||
#: cms/static/js/models/course.js cms/static/js/models/section.js
|
||||
msgid "You must specify a name"
|
||||
msgstr "ينبغي أن تقوم بتحديد إسم"
|
||||
@@ -3671,10 +3829,6 @@ msgstr "حذف إعدادات هذه المجموعة دائم ولا يمكن
|
||||
msgid "Deleting"
|
||||
msgstr "جاري الحذف"
|
||||
|
||||
#: cms/static/js/views/import.js cms/static/js/views/import.js.c
|
||||
msgid "Choose new file"
|
||||
msgstr "اختر ملف جديد"
|
||||
|
||||
#: cms/static/js/views/import.js
|
||||
msgid "Error importing course"
|
||||
msgstr "حدث خطأ أثناء استيراد المساق"
|
||||
@@ -3924,10 +4078,6 @@ msgstr ""
|
||||
msgid "Making Visible to Students…"
|
||||
msgstr "تصبح مرئية للطلاب…"
|
||||
|
||||
#: cms/static/js/views/pages/group_configurations.js
|
||||
msgid "You have unsaved changes. Do you really want to leave this page?"
|
||||
msgstr "هناك تغييرات غير محفوظة، هل تريد مغادرة الصفحة؟"
|
||||
|
||||
#: cms/static/js/views/settings/advanced.js
|
||||
msgid ""
|
||||
"Your changes will not take effect until you save your progress. Take care "
|
||||
@@ -4191,6 +4341,10 @@ msgstr "كلمة السر"
|
||||
msgid "Change My Email Address"
|
||||
msgstr "تغيير عنوان بريدي الإلكتروني"
|
||||
|
||||
#: lms/templates/student_account/account.underscore
|
||||
msgid "Reset Password"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/student_profile/profile.underscore
|
||||
msgid "Full Name"
|
||||
msgstr "الإسم الكامل"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user