394
common/static/js/vendor/requirejs/text.js
vendored
Normal file
394
common/static/js/vendor/requirejs/text.js
vendored
Normal file
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* @license RequireJS text 2.0.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
|
||||
* Available via the MIT or new BSD license.
|
||||
* see: http://github.com/requirejs/text for details
|
||||
*/
|
||||
/*jslint regexp: true */
|
||||
/*global require, XMLHttpRequest, ActiveXObject,
|
||||
define, window, process, Packages,
|
||||
java, location, Components, FileUtils */
|
||||
|
||||
// Added by edX: we namespace requirejs and its associated functions.
|
||||
var namespaced_define = window.define !== undefined ? define : RequireJS.define;
|
||||
|
||||
namespaced_define(['module'], function (module) {
|
||||
'use strict';
|
||||
|
||||
var text, fs, Cc, Ci, xpcIsWindows,
|
||||
progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
|
||||
xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
|
||||
bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
|
||||
hasLocation = typeof location !== 'undefined' && location.href,
|
||||
defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''),
|
||||
defaultHostName = hasLocation && location.hostname,
|
||||
defaultPort = hasLocation && (location.port || undefined),
|
||||
buildMap = {},
|
||||
masterConfig = (module.config && module.config()) || {};
|
||||
|
||||
text = {
|
||||
version: '2.0.14',
|
||||
|
||||
strip: function (content) {
|
||||
//Strips <?xml ...?> declarations so that external SVG and XML
|
||||
//documents can be added to a document without worry. Also, if the string
|
||||
//is an HTML document, only the part inside the body tag is returned.
|
||||
if (content) {
|
||||
content = content.replace(xmlRegExp, "");
|
||||
var matches = content.match(bodyRegExp);
|
||||
if (matches) {
|
||||
content = matches[1];
|
||||
}
|
||||
} else {
|
||||
content = "";
|
||||
}
|
||||
return content;
|
||||
},
|
||||
|
||||
jsEscape: function (content) {
|
||||
return content.replace(/(['\\])/g, '\\$1')
|
||||
.replace(/[\f]/g, "\\f")
|
||||
.replace(/[\b]/g, "\\b")
|
||||
.replace(/[\n]/g, "\\n")
|
||||
.replace(/[\t]/g, "\\t")
|
||||
.replace(/[\r]/g, "\\r")
|
||||
.replace(/[\u2028]/g, "\\u2028")
|
||||
.replace(/[\u2029]/g, "\\u2029");
|
||||
},
|
||||
|
||||
createXhr: masterConfig.createXhr || function () {
|
||||
//Would love to dump the ActiveX crap in here. Need IE 6 to die first.
|
||||
var xhr, i, progId;
|
||||
if (typeof XMLHttpRequest !== "undefined") {
|
||||
return new XMLHttpRequest();
|
||||
} else if (typeof ActiveXObject !== "undefined") {
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
progId = progIds[i];
|
||||
try {
|
||||
xhr = new ActiveXObject(progId);
|
||||
} catch (e) {}
|
||||
|
||||
if (xhr) {
|
||||
progIds = [progId]; // so faster next time
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return xhr;
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a resource name into its component parts. Resource names
|
||||
* look like: module/name.ext!strip, where the !strip part is
|
||||
* optional.
|
||||
* @param {String} name the resource name
|
||||
* @returns {Object} with properties "moduleName", "ext" and "strip"
|
||||
* where strip is a boolean.
|
||||
*/
|
||||
parseName: function (name) {
|
||||
var modName, ext, temp,
|
||||
strip = false,
|
||||
index = name.lastIndexOf("."),
|
||||
isRelative = name.indexOf('./') === 0 ||
|
||||
name.indexOf('../') === 0;
|
||||
|
||||
if (index !== -1 && (!isRelative || index > 1)) {
|
||||
modName = name.substring(0, index);
|
||||
ext = name.substring(index + 1);
|
||||
} else {
|
||||
modName = name;
|
||||
}
|
||||
|
||||
temp = ext || modName;
|
||||
index = temp.indexOf("!");
|
||||
if (index !== -1) {
|
||||
//Pull off the strip arg.
|
||||
strip = temp.substring(index + 1) === "strip";
|
||||
temp = temp.substring(0, index);
|
||||
if (ext) {
|
||||
ext = temp;
|
||||
} else {
|
||||
modName = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
moduleName: modName,
|
||||
ext: ext,
|
||||
strip: strip
|
||||
};
|
||||
},
|
||||
|
||||
xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
|
||||
|
||||
/**
|
||||
* Is an URL on another domain. Only works for browser use, returns
|
||||
* false in non-browser environments. Only used to know if an
|
||||
* optimized .js version of a text resource should be loaded
|
||||
* instead.
|
||||
* @param {String} url
|
||||
* @returns Boolean
|
||||
*/
|
||||
useXhr: function (url, protocol, hostname, port) {
|
||||
var uProtocol, uHostName, uPort,
|
||||
match = text.xdRegExp.exec(url);
|
||||
if (!match) {
|
||||
return true;
|
||||
}
|
||||
uProtocol = match[2];
|
||||
uHostName = match[3];
|
||||
|
||||
uHostName = uHostName.split(':');
|
||||
uPort = uHostName[1];
|
||||
uHostName = uHostName[0];
|
||||
|
||||
return (!uProtocol || uProtocol === protocol) &&
|
||||
(!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) &&
|
||||
((!uPort && !uHostName) || uPort === port);
|
||||
},
|
||||
|
||||
finishLoad: function (name, strip, content, onLoad) {
|
||||
content = strip ? text.strip(content) : content;
|
||||
if (masterConfig.isBuild) {
|
||||
buildMap[name] = content;
|
||||
}
|
||||
onLoad(content);
|
||||
},
|
||||
|
||||
load: function (name, req, onLoad, config) {
|
||||
//Name has format: some.module.filext!strip
|
||||
//The strip part is optional.
|
||||
//if strip is present, then that means only get the string contents
|
||||
//inside a body tag in an HTML string. For XML/SVG content it means
|
||||
//removing the <?xml ...?> declarations so the content can be inserted
|
||||
//into the current doc without problems.
|
||||
|
||||
// Do not bother with the work if a build and text will
|
||||
// not be inlined.
|
||||
if (config && config.isBuild && !config.inlineText) {
|
||||
onLoad();
|
||||
return;
|
||||
}
|
||||
|
||||
masterConfig.isBuild = config && config.isBuild;
|
||||
|
||||
var parsed = text.parseName(name),
|
||||
nonStripName = parsed.moduleName +
|
||||
(parsed.ext ? '.' + parsed.ext : ''),
|
||||
url = req.toUrl(nonStripName),
|
||||
useXhr = (masterConfig.useXhr) ||
|
||||
text.useXhr;
|
||||
|
||||
// Do not load if it is an empty: url
|
||||
if (url.indexOf('empty:') === 0) {
|
||||
onLoad();
|
||||
return;
|
||||
}
|
||||
|
||||
//Load the text. Use XHR if possible and in a browser.
|
||||
if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) {
|
||||
text.get(url, function (content) {
|
||||
text.finishLoad(name, parsed.strip, content, onLoad);
|
||||
}, function (err) {
|
||||
if (onLoad.error) {
|
||||
onLoad.error(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
//Need to fetch the resource across domains. Assume
|
||||
//the resource has been optimized into a JS module. Fetch
|
||||
//by the module name + extension, but do not include the
|
||||
//!strip part to avoid file system issues.
|
||||
req([nonStripName], function (content) {
|
||||
text.finishLoad(parsed.moduleName + '.' + parsed.ext,
|
||||
parsed.strip, content, onLoad);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
write: function (pluginName, moduleName, write, config) {
|
||||
if (buildMap.hasOwnProperty(moduleName)) {
|
||||
var content = text.jsEscape(buildMap[moduleName]);
|
||||
write.asModule(pluginName + "!" + moduleName,
|
||||
"namespaced_define(function () { return '" +
|
||||
content +
|
||||
"';});\n");
|
||||
}
|
||||
},
|
||||
|
||||
writeFile: function (pluginName, moduleName, req, write, config) {
|
||||
var parsed = text.parseName(moduleName),
|
||||
extPart = parsed.ext ? '.' + parsed.ext : '',
|
||||
nonStripName = parsed.moduleName + extPart,
|
||||
//Use a '.js' file name so that it indicates it is a
|
||||
//script that can be loaded across domains.
|
||||
fileName = req.toUrl(parsed.moduleName + extPart) + '.js';
|
||||
|
||||
//Leverage own load() method to load plugin value, but only
|
||||
//write out values that do not have the strip argument,
|
||||
//to avoid any potential issues with ! in file names.
|
||||
text.load(nonStripName, req, function (value) {
|
||||
//Use own write() method to construct full module value.
|
||||
//But need to create shell that translates writeFile's
|
||||
//write() to the right interface.
|
||||
var textWrite = function (contents) {
|
||||
return write(fileName, contents);
|
||||
};
|
||||
textWrite.asModule = function (moduleName, contents) {
|
||||
return write.asModule(moduleName, fileName, contents);
|
||||
};
|
||||
|
||||
text.write(pluginName, nonStripName, textWrite, config);
|
||||
}, config);
|
||||
}
|
||||
};
|
||||
|
||||
if (masterConfig.env === 'node' || (!masterConfig.env &&
|
||||
typeof process !== "undefined" &&
|
||||
process.versions &&
|
||||
!!process.versions.node &&
|
||||
!process.versions['node-webkit'] &&
|
||||
!process.versions['atom-shell'])) {
|
||||
//Using special require.nodeRequire, something added by r.js.
|
||||
fs = require.nodeRequire('fs');
|
||||
|
||||
text.get = function (url, callback, errback) {
|
||||
try {
|
||||
var file = fs.readFileSync(url, 'utf8');
|
||||
//Remove BOM (Byte Mark Order) from utf8 files if it is there.
|
||||
if (file[0] === '\uFEFF') {
|
||||
file = file.substring(1);
|
||||
}
|
||||
callback(file);
|
||||
} catch (e) {
|
||||
if (errback) {
|
||||
errback(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
} else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
|
||||
text.createXhr())) {
|
||||
text.get = function (url, callback, errback, headers) {
|
||||
var xhr = text.createXhr(), header;
|
||||
xhr.open('GET', url, true);
|
||||
|
||||
//Allow plugins direct access to xhr headers
|
||||
if (headers) {
|
||||
for (header in headers) {
|
||||
if (headers.hasOwnProperty(header)) {
|
||||
xhr.setRequestHeader(header.toLowerCase(), headers[header]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Allow overrides specified in config
|
||||
if (masterConfig.onXhr) {
|
||||
masterConfig.onXhr(xhr, url);
|
||||
}
|
||||
|
||||
xhr.onreadystatechange = function (evt) {
|
||||
var status, err;
|
||||
//Do not explicitly handle errors, those should be
|
||||
//visible via console output in the browser.
|
||||
if (xhr.readyState === 4) {
|
||||
status = xhr.status || 0;
|
||||
if (status > 399 && status < 600) {
|
||||
//An http 4xx or 5xx error. Signal an error.
|
||||
err = new Error(url + ' HTTP status: ' + status);
|
||||
err.xhr = xhr;
|
||||
if (errback) {
|
||||
errback(err);
|
||||
}
|
||||
} else {
|
||||
callback(xhr.responseText);
|
||||
}
|
||||
|
||||
if (masterConfig.onXhrComplete) {
|
||||
masterConfig.onXhrComplete(xhr, url);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
};
|
||||
} else if (masterConfig.env === 'rhino' || (!masterConfig.env &&
|
||||
typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
|
||||
//Why Java, why is this so awkward?
|
||||
text.get = function (url, callback) {
|
||||
var stringBuffer, line,
|
||||
encoding = "utf-8",
|
||||
file = new java.io.File(url),
|
||||
lineSeparator = java.lang.System.getProperty("line.separator"),
|
||||
input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)),
|
||||
content = '';
|
||||
try {
|
||||
stringBuffer = new java.lang.StringBuffer();
|
||||
line = input.readLine();
|
||||
|
||||
// Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
|
||||
// http://www.unicode.org/faq/utf_bom.html
|
||||
|
||||
// Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK:
|
||||
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
|
||||
if (line && line.length() && line.charAt(0) === 0xfeff) {
|
||||
// Eat the BOM, since we've already found the encoding on this file,
|
||||
// and we plan to concatenating this buffer with others; the BOM should
|
||||
// only appear at the top of a file.
|
||||
line = line.substring(1);
|
||||
}
|
||||
|
||||
if (line !== null) {
|
||||
stringBuffer.append(line);
|
||||
}
|
||||
|
||||
while ((line = input.readLine()) !== null) {
|
||||
stringBuffer.append(lineSeparator);
|
||||
stringBuffer.append(line);
|
||||
}
|
||||
//Make sure we return a JavaScript string and not a Java string.
|
||||
content = String(stringBuffer.toString()); //String
|
||||
} finally {
|
||||
input.close();
|
||||
}
|
||||
callback(content);
|
||||
};
|
||||
} else if (masterConfig.env === 'xpconnect' || (!masterConfig.env &&
|
||||
typeof Components !== 'undefined' && Components.classes &&
|
||||
Components.interfaces)) {
|
||||
//Avert your gaze!
|
||||
Cc = Components.classes;
|
||||
Ci = Components.interfaces;
|
||||
Components.utils['import']('resource://gre/modules/FileUtils.jsm');
|
||||
xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc);
|
||||
|
||||
text.get = function (url, callback) {
|
||||
var inStream, convertStream, fileObj,
|
||||
readData = {};
|
||||
|
||||
if (xpcIsWindows) {
|
||||
url = url.replace(/\//g, '\\');
|
||||
}
|
||||
|
||||
fileObj = new FileUtils.File(url);
|
||||
|
||||
//XPCOM, you so crazy
|
||||
try {
|
||||
inStream = Cc['@mozilla.org/network/file-input-stream;1']
|
||||
.createInstance(Ci.nsIFileInputStream);
|
||||
inStream.init(fileObj, 1, 0, false);
|
||||
|
||||
convertStream = Cc['@mozilla.org/intl/converter-input-stream;1']
|
||||
.createInstance(Ci.nsIConverterInputStream);
|
||||
convertStream.init(inStream, "utf-8", inStream.available(),
|
||||
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
||||
|
||||
convertStream.readString(inStream.available(), readData);
|
||||
convertStream.close();
|
||||
inStream.close();
|
||||
callback(readData.value);
|
||||
} catch (e) {
|
||||
throw new Error((fileObj && fileObj.path || '') + ': ' + e);
|
||||
}
|
||||
};
|
||||
}
|
||||
return text;
|
||||
});
|
||||
21
common/test/acceptance/pages/lms/teams.py
Normal file
21
common/test/acceptance/pages/lms/teams.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Teams page.
|
||||
"""
|
||||
|
||||
from .course_page import CoursePage
|
||||
|
||||
|
||||
class TeamsPage(CoursePage):
|
||||
"""
|
||||
Teams page/tab.
|
||||
"""
|
||||
url_path = "teams"
|
||||
|
||||
def is_browser_on_page(self):
|
||||
""" Checks if teams page is being viewed """
|
||||
return self.q(css='body.view-teams').present
|
||||
|
||||
def get_body_text(self):
|
||||
""" Returns the current dummy text. This will be changed once there is more content on the page. """
|
||||
return self.q(css='.teams-text').text[0]
|
||||
108
common/test/acceptance/tests/lms/test_teams.py
Normal file
108
common/test/acceptance/tests/lms/test_teams.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Acceptance tests for the teams feature.
|
||||
"""
|
||||
from ..helpers import UniqueCourseTest
|
||||
from ...pages.lms.teams import TeamsPage
|
||||
from nose.plugins.attrib import attr
|
||||
from ...fixtures.course import CourseFixture
|
||||
from ...pages.lms.tab_nav import TabNavPage
|
||||
from ...pages.lms.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.course_info import CourseInfoPage
|
||||
|
||||
|
||||
@attr('shard_5')
|
||||
class TeamsTabTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests verifying when the Teams tab is present.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TeamsTabTest, self).setUp()
|
||||
self.tab_nav = TabNavPage(self.browser)
|
||||
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
|
||||
self.teams_page = TeamsPage(self.browser, self.course_id)
|
||||
self.test_topic = {u"name": u"a topic", u"description": u"test topic", u"id": 0}
|
||||
|
||||
def set_team_configuration(self, configuration, enroll_in_course=True, global_staff=False):
|
||||
"""
|
||||
Sets team configuration on the course and calls auto-auth on the user.
|
||||
"""
|
||||
#pylint: disable=attribute-defined-outside-init
|
||||
self.course_fixture = CourseFixture(**self.course_info)
|
||||
if configuration:
|
||||
self.course_fixture.add_advanced_settings(
|
||||
{u"teams_configuration": {u"value": configuration}}
|
||||
)
|
||||
self.course_fixture.install()
|
||||
|
||||
enroll_course_id = self.course_id if enroll_in_course else None
|
||||
AutoAuthPage(self.browser, course_id=enroll_course_id, staff=global_staff).visit()
|
||||
self.course_info_page.visit()
|
||||
|
||||
def verify_teams_present(self, present):
|
||||
"""
|
||||
Verifies whether or not the teams tab is present. If it should be present, also
|
||||
checks the text on the page (to ensure view is working).
|
||||
"""
|
||||
if present:
|
||||
self.assertIn("Teams", self.tab_nav.tab_names)
|
||||
self.teams_page.visit()
|
||||
self.assertEqual("This is the new Teams tab.", self.teams_page.get_body_text())
|
||||
else:
|
||||
self.assertNotIn("Teams", self.tab_nav.tab_names)
|
||||
|
||||
def test_teams_not_enabled(self):
|
||||
"""
|
||||
Scenario: teams tab should not be present if no team configuration is set
|
||||
Given I am enrolled in a course without team configuration
|
||||
When I view the course info page
|
||||
Then I should not see the Teams tab
|
||||
"""
|
||||
self.set_team_configuration(None)
|
||||
self.verify_teams_present(False)
|
||||
|
||||
def test_teams_not_enabled_no_topics(self):
|
||||
"""
|
||||
Scenario: teams tab should not be present if team configuration does not specify topics
|
||||
Given I am enrolled in a course with no topics in the team configuration
|
||||
When I view the course info page
|
||||
Then I should not see the Teams tab
|
||||
"""
|
||||
self.set_team_configuration({u"max_team_size": 10, u"topics": []})
|
||||
self.verify_teams_present(False)
|
||||
|
||||
def test_teams_not_enabled_not_enrolled(self):
|
||||
"""
|
||||
Scenario: teams tab should not be present if student is not enrolled in the course
|
||||
Given there is a course with team configuration and topics
|
||||
And I am not enrolled in that course, and am not global staff
|
||||
When I view the course info page
|
||||
Then I should not see the Teams tab
|
||||
"""
|
||||
self.set_team_configuration({u"max_team_size": 10, u"topics": [self.test_topic]}, enroll_in_course=False)
|
||||
self.verify_teams_present(False)
|
||||
|
||||
def test_teams_enabled(self):
|
||||
"""
|
||||
Scenario: teams tab should be present if user is enrolled in the course and it has team configuration
|
||||
Given I am enrolled in a course with team configuration and topics
|
||||
When I view the course info page
|
||||
Then I should see the Teams tab
|
||||
And the correct content should be on the page
|
||||
"""
|
||||
self.set_team_configuration({u"max_team_size": 10, u"topics": [self.test_topic]})
|
||||
self.verify_teams_present(True)
|
||||
|
||||
def test_teams_enabled_global_staff(self):
|
||||
"""
|
||||
Scenario: teams tab should be present if user is not enrolled in the course, but is global staff
|
||||
Given there is a course with team configuration
|
||||
And I am not enrolled in that course, but am global staff
|
||||
When I view the course info page
|
||||
Then I should see the Teams tab
|
||||
And the correct content should be on the page
|
||||
"""
|
||||
self.set_team_configuration(
|
||||
{u"max_team_size": 10, u"topics": [self.test_topic]}, enroll_in_course=False, global_staff=True
|
||||
)
|
||||
self.verify_teams_present(True)
|
||||
0
lms/djangoapps/teams/__init__.py
Normal file
0
lms/djangoapps/teams/__init__.py
Normal file
30
lms/djangoapps/teams/plugins.py
Normal file
30
lms/djangoapps/teams/plugins.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Definition of the course team feature.
|
||||
"""
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from courseware.tabs import EnrolledCourseViewType
|
||||
from .views import is_feature_enabled
|
||||
|
||||
|
||||
class TeamsCourseViewType(EnrolledCourseViewType):
|
||||
"""
|
||||
The representation of the course teams view type.
|
||||
"""
|
||||
|
||||
name = "teams"
|
||||
title = _("Teams")
|
||||
view_name = "teams_dashboard"
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None):
|
||||
"""Returns true if the teams feature is enabled in the course.
|
||||
|
||||
Args:
|
||||
course (CourseDescriptor): the course using the feature
|
||||
user (User): the user interacting with the course
|
||||
"""
|
||||
if not super(TeamsCourseViewType, cls).is_enabled(course, user=user):
|
||||
return False
|
||||
|
||||
return is_feature_enabled(course)
|
||||
@@ -0,0 +1,19 @@
|
||||
define(["jquery", "teams/js/teams_tab_factory"],
|
||||
function($, TeamsTabFactory) {
|
||||
'use strict';
|
||||
|
||||
describe("teams django app", function() {
|
||||
var teamsTab;
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures("<div class='team-tab-content'></div>");
|
||||
teamsTab = new TeamsTabFactory();
|
||||
});
|
||||
|
||||
it("can load templates", function() {
|
||||
expect($("body").text()).toContain("This is the new Teams tab");
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
13
lms/djangoapps/teams/static/teams/js/teams_tab_factory.js
Normal file
13
lms/djangoapps/teams/static/teams/js/teams_tab_factory.js
Normal file
@@ -0,0 +1,13 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
|
||||
define(['jquery', 'teams/js/views/teams_tab'],
|
||||
function ($, TeamsTabView) {
|
||||
return function () {
|
||||
var view = new TeamsTabView({
|
||||
el: $('.team-tab-content')
|
||||
});
|
||||
view.render();
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
14
lms/djangoapps/teams/static/teams/js/views/teams_tab.js
Normal file
14
lms/djangoapps/teams/static/teams/js/views/teams_tab.js
Normal file
@@ -0,0 +1,14 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
|
||||
define(['backbone', 'underscore', 'text!teams/templates/teams-tab.underscore'],
|
||||
function (Backbone, _, teamsTabTemplate) {
|
||||
var TeamTabView = Backbone.View.extend({
|
||||
render: function() {
|
||||
this.$el.html(_.template(teamsTabTemplate, {}));
|
||||
}
|
||||
});
|
||||
|
||||
return TeamTabView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1 @@
|
||||
<p class="teams-text">This is the new Teams tab.</p>
|
||||
25
lms/djangoapps/teams/templates/teams/teams.html
Normal file
25
lms/djangoapps/teams/templates/teams/teams.html
Normal file
@@ -0,0 +1,25 @@
|
||||
## mako
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
<%inherit file="/main.html" />
|
||||
|
||||
<%block name="bodyclass">view-teams is-in-course course</%block>
|
||||
<%block name="pagetitle">${_("Teams")}</%block>
|
||||
<%block name="headextra">
|
||||
<%static:css group='style-course'/>
|
||||
</%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='teams'" />
|
||||
|
||||
<div class="team-tab-content"></div>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
(function (require) {
|
||||
require(['teams/js/teams_tab_factory'], function (TeamsTabFactory) {
|
||||
var pageView = new TeamsTabFactory({
|
||||
});
|
||||
});
|
||||
}).call(this, require || RequireJS.require);
|
||||
</script>
|
||||
</%block>
|
||||
85
lms/djangoapps/teams/tests/test_views.py
Normal file
85
lms/djangoapps/teams/tests/test_views.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Tests for views.py
|
||||
"""
|
||||
from nose.plugins.attrib import attr
|
||||
from student.tests.factories import (
|
||||
CourseEnrollmentFactory,
|
||||
UserFactory,
|
||||
)
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from django.http import Http404
|
||||
from django.core.urlresolvers import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class TestDashboard(ModuleStoreTestCase):
|
||||
test_password = "test"
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up tests
|
||||
"""
|
||||
super(TestDashboard, self).setUp()
|
||||
self.course = CourseFactory.create(
|
||||
teams_configuration={"max_team_size": 10, "topics": [{"name": "foo", "id": 0, "description": "test topic"}]}
|
||||
)
|
||||
# will be assigned to self.client by default
|
||||
self.user = UserFactory.create(password=self.test_password)
|
||||
self.teams_url = reverse('teams_dashboard', args=[self.course.id])
|
||||
|
||||
def test_anonymous(self):
|
||||
""" Verifies that an anonymous client cannot access the team dashboard. """
|
||||
anonymous_client = APIClient()
|
||||
response = anonymous_client.get(self.teams_url)
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
def test_not_enrolled_not_staff(self):
|
||||
""" Verifies that a student who is not enrolled cannot access the team dashboard. """
|
||||
response = self.client.get(self.teams_url)
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
def test_not_enrolled_staff(self):
|
||||
"""
|
||||
Verifies that a user with global access who is not enrolled in the course can access the team dashboard.
|
||||
"""
|
||||
staff_user = UserFactory(is_staff=True, password=self.test_password)
|
||||
staff_client = APIClient()
|
||||
staff_client.login(username=staff_user.username, password=self.test_password)
|
||||
response = staff_client.get(self.teams_url)
|
||||
self.assertContains(response, "TeamsTabFactory", status_code=200)
|
||||
|
||||
def test_enrolled_not_staff(self):
|
||||
"""
|
||||
Verifies that a user without global access who is enrolled in the course can access the team dashboard.
|
||||
"""
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
response = self.client.get(self.teams_url)
|
||||
self.assertContains(response, "TeamsTabFactory", status_code=200)
|
||||
|
||||
def test_enrolled_teams_not_enabled(self):
|
||||
"""
|
||||
Verifies that a user without global access who is enrolled in the course cannot access the team dashboard
|
||||
if the teams feature is not enabled.
|
||||
"""
|
||||
course = CourseFactory.create()
|
||||
teams_url = reverse('teams_dashboard', args=[course.id])
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=course.id)
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
response = self.client.get(teams_url)
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
def test_bad_course_id(self):
|
||||
"""
|
||||
Verifies expected behavior when course_id does not reference an existing course or is invalid.
|
||||
"""
|
||||
bad_org = "badorgxxx"
|
||||
bad_team_url = self.teams_url.replace(self.course.id.org, bad_org)
|
||||
response = self.client.get(bad_team_url)
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
bad_team_url = bad_team_url.replace(bad_org, "invalid/course/id")
|
||||
response = self.client.get(bad_team_url)
|
||||
self.assertEqual(404, response.status_code)
|
||||
11
lms/djangoapps/teams/urls.py
Normal file
11
lms/djangoapps/teams/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
URLs for teams.
|
||||
"""
|
||||
from django.conf.urls import patterns, url
|
||||
from teams.views import TeamsDashboardView
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
"teams.views",
|
||||
url(r"^/$", TeamsDashboardView.as_view(), name="teams_dashboard"),
|
||||
)
|
||||
44
lms/djangoapps/teams/views.py
Normal file
44
lms/djangoapps/teams/views.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
View methods for the course team feature.
|
||||
"""
|
||||
|
||||
from django.shortcuts import render_to_response
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from courseware.courses import get_course_with_access, has_access
|
||||
from django.http import Http404
|
||||
from django.conf import settings
|
||||
from django.views.generic.base import View
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
|
||||
class TeamsDashboardView(View):
|
||||
"""
|
||||
View methods related to the teams dashboard.
|
||||
"""
|
||||
|
||||
def get(self, request, course_id):
|
||||
"""
|
||||
Renders the teams dashboard, which is shown on the "Teams" tab.
|
||||
|
||||
Raises a 404 if the course specified by course_id does not exist, the
|
||||
user is not registered for the course, or the teams feature is not enabled.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, "load", course_key)
|
||||
|
||||
if not is_feature_enabled(course):
|
||||
raise Http404
|
||||
|
||||
if not CourseEnrollment.is_enrolled(request.user, course.id) and \
|
||||
not has_access(request.user, 'staff', course, course.id):
|
||||
raise Http404
|
||||
|
||||
context = {"course": course}
|
||||
return render_to_response("teams/teams.html", context)
|
||||
|
||||
|
||||
def is_feature_enabled(course):
|
||||
"""
|
||||
Returns True if the teams feature is enabled.
|
||||
"""
|
||||
return settings.FEATURES.get('ENABLE_TEAMS', False) and course.teams_enabled
|
||||
@@ -1854,6 +1854,9 @@ INSTALLED_APPS = (
|
||||
|
||||
# Credit courses
|
||||
'openedx.core.djangoapps.credit',
|
||||
|
||||
# Course teams
|
||||
'teams',
|
||||
)
|
||||
|
||||
######################### CSRF #########################################
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
'jquery.url': 'xmodule_js/common_static/js/vendor/url.min',
|
||||
'datepair': 'xmodule_js/common_static/js/vendor/timepicker/datepair',
|
||||
'date': 'xmodule_js/common_static/js/vendor/date',
|
||||
'text': 'xmodule_js/common_static/js/vendor/requirejs/text',
|
||||
'underscore': 'xmodule_js/common_static/js/vendor/underscore-min',
|
||||
'underscore.string': 'xmodule_js/common_static/js/vendor/underscore.string.min',
|
||||
'backbone': 'xmodule_js/common_static/js/vendor/backbone-min',
|
||||
@@ -576,6 +577,7 @@
|
||||
// TODO: why do these need 'lms/include' at the front but the CMS equivalent logic doesn't?
|
||||
define([
|
||||
// Run the LMS tests
|
||||
'lms/include/teams/js/spec/teams_factory_spec.js',
|
||||
'lms/include/js/spec/photocapture_spec.js',
|
||||
'lms/include/js/spec/staff_debug_actions_spec.js',
|
||||
'lms/include/js/spec/views/notification_spec.js',
|
||||
|
||||
@@ -35,6 +35,7 @@ lib_paths:
|
||||
- xmodule_js/common_static/js/vendor/jasmine-imagediff.js
|
||||
- xmodule_js/common_static/js/vendor/require.js
|
||||
- js/RequireJS-namespace-undefine.js
|
||||
- xmodule_js/common_static/js/vendor/requirejs/text.js
|
||||
- xmodule_js/common_static/js/vendor/jquery.min.js
|
||||
- xmodule_js/common_static/js/vendor/jquery-ui.min.js
|
||||
- xmodule_js/common_static/js/vendor/jquery.cookie.js
|
||||
@@ -64,10 +65,12 @@ lib_paths:
|
||||
src_paths:
|
||||
- js
|
||||
- js/common_helpers
|
||||
- teams/js
|
||||
|
||||
# Paths to spec (test) JavaScript files
|
||||
spec_paths:
|
||||
- js/spec
|
||||
- teams/js/spec
|
||||
|
||||
# Paths to fixture files (optional)
|
||||
# The fixture path will be set automatically when using jasmine-jquery.
|
||||
@@ -91,6 +94,7 @@ fixture_paths:
|
||||
- js/fixtures/edxnotes
|
||||
- js/fixtures/search
|
||||
- templates/search
|
||||
- teams/templates
|
||||
- templates/discovery
|
||||
|
||||
requirejs:
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
paths: {
|
||||
"annotator_1.2.9": "js/vendor/edxnotes/annotator-full.min",
|
||||
"date": "js/vendor/date",
|
||||
"text": 'js/vendor/requirejs/text',
|
||||
"backbone": "js/vendor/backbone-min",
|
||||
"backbone-super": "js/vendor/backbone-super",
|
||||
"underscore.string": "js/vendor/underscore.string.min",
|
||||
@@ -67,7 +68,7 @@
|
||||
"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',
|
||||
"handlebars": 'js/vendor/ova/catch/js/handlebars-1.1.2'
|
||||
// end of files needed by OVA
|
||||
},
|
||||
shim: {
|
||||
@@ -89,7 +90,7 @@
|
||||
exports: "Backbone"
|
||||
},
|
||||
"backbone-super": {
|
||||
deps: ["backbone"],
|
||||
deps: ["backbone"]
|
||||
},
|
||||
"logger": {
|
||||
exports: "Logger"
|
||||
@@ -147,7 +148,7 @@
|
||||
"grouping-annotator", "diacritic-annotator", "openseadragon", "jquery-Watch", "catch", "handlebars",
|
||||
"URI"
|
||||
]
|
||||
},
|
||||
}
|
||||
// End of needed by OVA
|
||||
}
|
||||
};
|
||||
|
||||
1
lms/static/teams
Symbolic link
1
lms/static/teams
Symbolic link
@@ -0,0 +1 @@
|
||||
../djangoapps/teams/static/teams
|
||||
@@ -428,6 +428,12 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^api/branding/v1/', include('branding.api_urls')),
|
||||
)
|
||||
|
||||
if settings.FEATURES["ENABLE_TEAMS"]:
|
||||
# Teams endpoints
|
||||
urlpatterns += (
|
||||
url(r'^courses/{}/teams'.format(settings.COURSE_ID_PATTERN), include('teams.urls'), name="teams_endpoints"),
|
||||
)
|
||||
|
||||
# allow course staff to change to student view of courseware
|
||||
if settings.FEATURES.get('ENABLE_MASQUERADE'):
|
||||
urlpatterns += (
|
||||
|
||||
1
setup.py
1
setup.py
@@ -33,6 +33,7 @@ setup(
|
||||
"progress = lms.djangoapps.courseware.tabs:ProgressCourseViewType",
|
||||
"static_tab = lms.djangoapps.courseware.tabs:StaticCourseViewType",
|
||||
"syllabus = lms.djangoapps.courseware.tabs:SyllabusCourseViewType",
|
||||
"teams = lms.djangoapps.teams.plugins:TeamsCourseViewType",
|
||||
"textbooks = lms.djangoapps.courseware.tabs:TextbookCourseViews",
|
||||
"wiki = lms.djangoapps.course_wiki.tab:WikiCourseViewType",
|
||||
|
||||
|
||||
Reference in New Issue
Block a user