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 name="pagetitle">${_("Teams")} +<%block name="headextra"> +<%static:css group='style-course'/> + + +<%include file="/courseware/course_navigation.html" args="active_page='teams'" /> + +
+ +<%block name="js_extra"> + + 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",