From e0ddb1e0e260816dcf87cadb852cef57148ded2a Mon Sep 17 00:00:00 2001 From: Nathan Sprenkle Date: Tue, 2 Jun 2020 15:05:47 -0400 Subject: [PATCH] Team Assignments Dashboard (#24019) * Add team assignments to frontend * Limit team assignments to the given teamset * Remove deprecated django render_to_response * Move team assignments panel behind feature flag --- lms/djangoapps/teams/api.py | 17 +++ lms/djangoapps/teams/api_urls.py | 8 ++ .../teams/js/spec/views/team_profile_spec.js | 89 ++++++++++++++- .../js/spec_helpers/team_spec_helpers.js | 14 +++ .../static/teams/js/views/team_profile.js | 47 +++++++- .../templates/team-assignment.underscore | 3 + .../teams/templates/team-profile.underscore | 7 ++ .../teams/templates/teams/teams.html | 3 + lms/djangoapps/teams/tests/test_views.py | 104 +++++++++++++++++- lms/djangoapps/teams/views.py | 92 +++++++++++++++- lms/djangoapps/teams/waffle.py | 26 +++++ 11 files changed, 404 insertions(+), 6 deletions(-) create mode 100644 lms/djangoapps/teams/static/teams/templates/team-assignment.underscore create mode 100644 lms/djangoapps/teams/waffle.py diff --git a/lms/djangoapps/teams/api.py b/lms/djangoapps/teams/api.py index c2b82b84ec..6ca1149475 100644 --- a/lms/djangoapps/teams/api.py +++ b/lms/djangoapps/teams/api.py @@ -365,3 +365,20 @@ def anonymous_user_ids_for_team(user, team): anonymous_id_for_user(user=team_member, course_id=team.course_id, save=False) for team_member in team.users.all() ]) + + +def get_assignments_for_team(user, team): + """ Get openassessment XBlocks configured for the current teamset """ + # Confirm access + if not has_specific_team_access(user, team): + raise Exception("User {user} is not permitted to access team info for {team}".format( + user=user.username, + team=team.team_id + )) + + # Limit to team-enabled ORAs for the matching teamset in the course + return modulestore().get_items( + team.course_id, + qualifiers={'category': 'openassessment'}, + settings={'teams_enabled': True, 'selected_teamset_id': team.topic_id} + ) diff --git a/lms/djangoapps/teams/api_urls.py b/lms/djangoapps/teams/api_urls.py index 8c8f905d14..e36cb69a65 100644 --- a/lms/djangoapps/teams/api_urls.py +++ b/lms/djangoapps/teams/api_urls.py @@ -10,6 +10,7 @@ from .views import ( MembershipBulkManagementView, MembershipDetailView, MembershipListView, + TeamsAssignmentsView, TeamsDetailView, TeamsListView, TopicDetailView, @@ -32,6 +33,13 @@ urlpatterns = [ TeamsDetailView.as_view(), name="teams_detail" ), + url( + r'^v0/teams/{team_id_pattern}/assignments$'.format( + team_id_pattern=TEAM_ID_PATTERN, + ), + TeamsAssignmentsView.as_view(), + name="teams_assignments_list" + ), url( r'^v0/topics/$', TopicListView.as_view(), diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js index 8d8ce68a65..8a4cda671a 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js @@ -48,7 +48,7 @@ define([ el: $('.profile-view'), teamEvents: TeamSpecHelpers.teamEvents, courseID: TeamSpecHelpers.testCourseID, - context: TeamSpecHelpers.testContext, + context: options.context || TeamSpecHelpers.testContext, model: teamModel, topic: isInstructorManagedTopic ? TeamSpecHelpers.createMockInstructorManagedTopic() : @@ -72,6 +72,23 @@ define([ ) ); AjaxHelpers.respondWithJson(requests, TeamSpecHelpers.createMockDiscussionResponse()); + + // Assignments are feature-flagged + if (profileView.context.teamsAssignmentsUrl) { + AjaxHelpers.expectRequest( + requests, + 'GET', + interpolate( // eslint-disable-line no-undef + '/api/team/v0/teams/%(teamId)s/assignments', + { + teamId: teamModel.id + }, + true + ) + ); + AjaxHelpers.respondWithJson(requests, TeamSpecHelpers.createMockTeamAssignments(options.assignments)); + } + return profileView; }; @@ -101,6 +118,76 @@ define([ } }; + describe('TeamAssignmentsView', function() { + it('can render itself', function() { + // Given a member of a team with team assignments + var mockAssignments = TeamSpecHelpers.createMockTeamAssignments(), + options = { + membership: DEFAULT_MEMBERSHIP + }, + requests = AjaxHelpers.requests(this); + + // When they go to the team profile view + var view = createTeamProfileView(requests, options); + + // The Assignments section renders with their assignments + expect(view.$('.team-assignment').length).toEqual(mockAssignments.length); + }); + + it('displays a message when no assignments are found', function() { + // Given a member viewing a team with no assignments + var mockAssignments = [], + options = { + assignments: mockAssignments, + membership: DEFAULT_MEMBERSHIP + }, + requests = AjaxHelpers.requests(this); + + // When they view the team + var view = createTeamProfileView(requests, options); + + // There should be filler text that says there are no assignments + expect(view.$('#assignments').text()).toEqual('No assignments for team'); + expect(view.$('.team-assignment').length).toEqual(0); + }); + + it('does not show at all for someone who is not on the team or staff', function() { + // Given a user who is not on a team viewing a team with assignments + var mockAssignments = TeamSpecHelpers.createMockTeamAssignments(), + options = { + assignments: mockAssignments + }, + requests = AjaxHelpers.requests(this); + + // When the user goes to the team detail page + var view = createTeamProfileView(requests, options); + + // Then then assignments view does not appear on the page + expect(view.$('.team-assignments').length).toBe(0); + }); + + it('does not show at all when the feature flag is turned off', function() { + // Given the team submissions feature is turned off + // (teamAsssignmentsUrl isn't surfaced to user) + var mockAssignments = TeamSpecHelpers.createMockTeamAssignments(), + options = { + assignments: mockAssignments, + membership: DEFAULT_MEMBERSHIP, + context: Object.assign({}, TeamSpecHelpers.testContext) + }, + requests = AjaxHelpers.requests(this), + view; + + delete options.context.teamsAssignmentsUrl; + + // When the user goes to the team detail page + view = createTeamProfileView(requests, options); + + // Then then assignments view does not appear on the page + expect(view.$('.team-assignments').length).toBe(0); + }); + }); + describe('DiscussionsView', function() { it('can render itself', function() { var requests = AjaxHelpers.requests(this), diff --git a/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js b/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js index 2ecd83a27a..65914993f9 100644 --- a/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js +++ b/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js @@ -73,6 +73,18 @@ define([ ); }; + var createMockTeamAssignments = function(assignments, options) { + if (_.isUndefined(assignments)) { + assignments = [ // eslint-disable-line no-param-reassign + { + display_name: 'Send me', + location: 'your location' + } + ]; + } + return _.extend(assignments, options); + }; + var createMockTeamMembershipsData = function(startIndex, stopIndex) { var teams = createMockTeamData(startIndex, stopIndex); return _.map(_.range(startIndex, stopIndex + 1), function(i) { @@ -282,6 +294,7 @@ define([ countries: testCountries, topicUrl: '/api/team/v0/topics/topic_id,' + testCourseID, teamsUrl: '/api/team/v0/teams/', + teamsAssignmentsUrl: '/api/team/v0/teams/team_id/assignments', teamsDetailUrl: '/api/team/v0/teams/team_id', teamMembershipsUrl: '/api/team/v0/team_memberships/', teamMembershipDetailUrl: '/api/team/v0/team_membership/team_id,' + testUser, @@ -327,6 +340,7 @@ define([ createMockTeamData: createMockTeamData, createMockTeamsResponse: createMockTeamsResponse, createMockTeams: createMockTeams, + createMockTeamAssignments: createMockTeamAssignments, createMockUserInfo: createMockUserInfo, createMockContext: createMockContext, createMockTopic: createMockTopic, diff --git a/lms/djangoapps/teams/static/teams/js/views/team_profile.js b/lms/djangoapps/teams/static/teams/js/views/team_profile.js index dd5c794fb5..7267a0ef7e 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_profile.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_profile.js @@ -12,10 +12,11 @@ 'common/js/components/utils/view_utils', 'teams/js/views/team_utils', 'text!teams/templates/team-profile.underscore', - 'text!teams/templates/team-member.underscore' + 'text!teams/templates/team-member.underscore', + 'text!teams/templates/team-assignment.underscore' ], function(Backbone, _, gettext, HtmlUtils, TeamDiscussionView, ViewUtils, TeamUtils, - teamTemplate, teamMemberTemplate) { + teamTemplate, teamMemberTemplate, teamAssignmentTemplate) { var TeamProfileView = Backbone.View.extend({ errorMessage: gettext('An error occurred. Try again.'), @@ -44,6 +45,10 @@ isInstructorManagedTopic = TeamUtils.isInstructorManagedTopic(this.topic.attributes.type), maxTeamSize = this.topic.getMaxTeamSize(this.context.courseMaxTeamSize); + // Assignments URL isn't provided if team assignments shouldn't be shown + // so we can treat it like a toggle + var showAssignments = !!this.context.teamsAssignmentsUrl; + var showLeaveLink = isMember && (isAdminOrStaff || !isInstructorManagedTopic); HtmlUtils.setHtml( @@ -56,7 +61,9 @@ language: this.languages[this.model.get('language')], membershipText: TeamUtils.teamCapacityText(memberships.length, maxTeamSize), isMember: isMember, + isAdminOrStaff: isAdminOrStaff, showLeaveLink: showLeaveLink, + showAssignments: showAssignments, hasCapacity: maxTeamSize && (memberships.length < maxTeamSize), hasMembers: memberships.length >= 1 }) @@ -67,12 +74,48 @@ }); this.discussionView.render(); + if (showAssignments) { + this.getTeamAssignments(); + } + this.renderTeamMembers(); this.setFocusToHeaderFunc(); return this; }, + getTeamAssignments: function() { + var view = this; + + $.ajax({ + type: 'GET', + url: view.context.teamsAssignmentsUrl.replace('team_id', view.model.get('id')) + }).done(function(data) { + view.renderTeamAssignments(data); + }).fail(function(data) { + TeamUtils.parseAndShowMessage(data, view.errorMessage); + }); + }, + + renderTeamAssignments: function(assignments) { + var view = this; + + if (!assignments || !assignments.length) { + view.$('#assignments').text(gettext('No assignments for team')); + return; + } + + _.each(assignments, function(assignment) { + HtmlUtils.append( + view.$('#assignments'), + HtmlUtils.template(teamAssignmentTemplate)({ + displayName: assignment.display_name, + linkLocation: assignment.location + }) + ); + }); + }, + renderTeamMembers: function() { var view = this; _.each(this.model.get('membership'), function(membership) { diff --git a/lms/djangoapps/teams/static/teams/templates/team-assignment.underscore b/lms/djangoapps/teams/static/teams/templates/team-assignment.underscore new file mode 100644 index 0000000000..a20d10f2be --- /dev/null +++ b/lms/djangoapps/teams/static/teams/templates/team-assignment.underscore @@ -0,0 +1,3 @@ +
  • + <%- displayName %> +
  • diff --git a/lms/djangoapps/teams/static/teams/templates/team-profile.underscore b/lms/djangoapps/teams/static/teams/templates/team-profile.underscore index 6bb3621c03..d6df083c9d 100644 --- a/lms/djangoapps/teams/static/teams/templates/team-profile.underscore +++ b/lms/djangoapps/teams/static/teams/templates/team-profile.underscore @@ -1,5 +1,12 @@
    + <% if (showAssignments && (isMember || isAdminOrStaff)) { %> +
    +

    <%- gettext("Team Assignments") %>

    +
      +
      +
      + <% } %>