diff --git a/common/static/js/vendor/requirejs/text.js b/common/static/js/vendor/requirejs/text.js new file mode 100644 index 0000000000..1e5cf41f4f --- /dev/null +++ b/common/static/js/vendor/requirejs/text.js @@ -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 = /
]*>\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 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 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; +}); diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py new file mode 100644 index 0000000000..486dce4e38 --- /dev/null +++ b/common/test/acceptance/pages/lms/teams.py @@ -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] diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py new file mode 100644 index 0000000000..bc9f4c8bd4 --- /dev/null +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -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) diff --git a/lms/djangoapps/teams/__init__.py b/lms/djangoapps/teams/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/teams/plugins.py b/lms/djangoapps/teams/plugins.py new file mode 100644 index 0000000000..f010e97ecb --- /dev/null +++ b/lms/djangoapps/teams/plugins.py @@ -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) diff --git a/lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js b/lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js new file mode 100644 index 0000000000..628c00ba58 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/spec/teams_factory_spec.js @@ -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(""); + teamsTab = new TeamsTabFactory(); + }); + + it("can load templates", function() { + expect($("body").text()).toContain("This is the new Teams tab"); + }); + + }); + } +); diff --git a/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js b/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js new file mode 100644 index 0000000000..9b735c651d --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js @@ -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); diff --git a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js new file mode 100644 index 0000000000..5e60e84ecd --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -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); diff --git a/lms/djangoapps/teams/static/teams/templates/teams-tab.underscore b/lms/djangoapps/teams/static/teams/templates/teams-tab.underscore new file mode 100644 index 0000000000..e9411c5ab0 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/templates/teams-tab.underscore @@ -0,0 +1 @@ +This is the new Teams tab.
diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html new file mode 100644 index 0000000000..e001a377fe --- /dev/null +++ b/lms/djangoapps/teams/templates/teams/teams.html @@ -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'" /> + + + +<%block name="js_extra"> + +%block> diff --git a/lms/djangoapps/teams/tests/test_views.py b/lms/djangoapps/teams/tests/test_views.py new file mode 100644 index 0000000000..4c75e69384 --- /dev/null +++ b/lms/djangoapps/teams/tests/test_views.py @@ -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) diff --git a/lms/djangoapps/teams/urls.py b/lms/djangoapps/teams/urls.py new file mode 100644 index 0000000000..f6dbcea634 --- /dev/null +++ b/lms/djangoapps/teams/urls.py @@ -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"), +) diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py new file mode 100644 index 0000000000..9a856a50b8 --- /dev/null +++ b/lms/djangoapps/teams/views.py @@ -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 diff --git a/lms/envs/common.py b/lms/envs/common.py index 9da5231771..4e5b1e6914 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1854,6 +1854,9 @@ INSTALLED_APPS = ( # Credit courses 'openedx.core.djangoapps.credit', + + # Course teams + 'teams', ) ######################### CSRF ######################################### diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index 7d72799ce0..945a8f1d34 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -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', diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml index 466c107411..7b7d8eddc6 100644 --- a/lms/static/js_test.yml +++ b/lms/static/js_test.yml @@ -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: diff --git a/lms/static/require-config-lms.js b/lms/static/require-config-lms.js index 0e4b9ae82c..039f288bbb 100644 --- a/lms/static/require-config-lms.js +++ b/lms/static/require-config-lms.js @@ -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 } }; diff --git a/lms/static/teams b/lms/static/teams new file mode 120000 index 0000000000..817a55ea8d --- /dev/null +++ b/lms/static/teams @@ -0,0 +1 @@ +../djangoapps/teams/static/teams \ No newline at end of file diff --git a/lms/urls.py b/lms/urls.py index ad6dbb3140..f06f358b7c 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -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 += ( diff --git a/setup.py b/setup.py index 1a4c167f6f..9a075ec92a 100644 --- a/setup.py +++ b/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",