Merge pull request #9520 from edx/efischer/teams-instructor-toolbar
Teams Instructor Tools bar
This commit is contained in:
62
common/test/acceptance/pages/common/utils.py
Normal file
62
common/test/acceptance/pages/common/utils.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Utility methods common to Studio and the LMS.
|
||||
"""
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from ...tests.helpers import disable_animations
|
||||
|
||||
|
||||
def wait_for_notification(page):
|
||||
"""
|
||||
Waits for the "mini-notification" to appear and disappear on the given page (subclass of PageObject).
|
||||
"""
|
||||
def _is_saving():
|
||||
"""Whether or not the notification is currently showing."""
|
||||
return page.q(css='.wrapper-notification-mini.is-shown').present
|
||||
|
||||
def _is_saving_done():
|
||||
"""Whether or not the notification is finished showing."""
|
||||
return page.q(css='.wrapper-notification-mini.is-hiding').present
|
||||
|
||||
EmptyPromise(_is_saving, 'Notification should have been shown.', timeout=60).fulfill()
|
||||
EmptyPromise(_is_saving_done, 'Notification should have been hidden.', timeout=60).fulfill()
|
||||
|
||||
|
||||
def click_css(page, css, source_index=0, require_notification=True):
|
||||
"""
|
||||
Click the button/link with the given css and index on the specified page (subclass of PageObject).
|
||||
|
||||
Will only consider elements that are displayed and have a height and width greater than zero.
|
||||
|
||||
If require_notification is False (default value is True), the method will return immediately.
|
||||
Otherwise, it will wait for the "mini-notification" to appear and disappear.
|
||||
"""
|
||||
def _is_visible(element):
|
||||
"""Is the given element visible?"""
|
||||
# Only make the call to size once (instead of once for the height and once for the width)
|
||||
# because otherwise you will trigger a extra query on a remote element.
|
||||
return element.is_displayed() and all(size > 0 for size in element.size.itervalues())
|
||||
|
||||
# Disable all animations for faster testing with more reliable synchronization
|
||||
disable_animations(page)
|
||||
# Click on the element in the browser
|
||||
page.q(css=css).filter(_is_visible).nth(source_index).click()
|
||||
|
||||
if require_notification:
|
||||
wait_for_notification(page)
|
||||
|
||||
# Some buttons trigger ajax posts
|
||||
# (e.g. .add-missing-groups-button as configured in split_test_author_view.js)
|
||||
# so after you click anything wait for the ajax call to finish
|
||||
page.wait_for_ajax()
|
||||
|
||||
|
||||
def confirm_prompt(page, cancel=False, require_notification=None):
|
||||
"""
|
||||
Ensures that a modal prompt and confirmation button are visible, then clicks the button. The prompt is canceled iff
|
||||
cancel is True.
|
||||
"""
|
||||
page.wait_for_element_visibility('.prompt', 'Prompt is visible')
|
||||
confirmation_button_css = '.prompt .action-' + ('secondary' if cancel else 'primary')
|
||||
page.wait_for_element_visibility(confirmation_button_css, 'Confirmation button is visible')
|
||||
require_notification = (not cancel) if require_notification is None else require_notification
|
||||
click_css(page, confirmation_button_css, require_notification=require_notification)
|
||||
@@ -6,7 +6,7 @@ Teams pages.
|
||||
from .course_page import CoursePage
|
||||
from .discussion import InlineDiscussionPage
|
||||
from ..common.paging import PaginatedUIMixin
|
||||
from ...pages.studio.utils import confirm_prompt
|
||||
from ...pages.common.utils import confirm_prompt
|
||||
|
||||
from .fields import FieldsMixin
|
||||
|
||||
@@ -43,7 +43,24 @@ class TeamCardsMixin(object):
|
||||
return self.q(css='p.card-description').map(lambda e: e.text).results
|
||||
|
||||
|
||||
class TeamsPage(CoursePage):
|
||||
class BreadcrumbsMixin(object):
|
||||
"""Provides common operations on teams page breadcrumb links."""
|
||||
|
||||
@property
|
||||
def header_page_breadcrumbs(self):
|
||||
"""Get the page breadcrumb text displayed by the page header"""
|
||||
return self.q(css='.page-header .breadcrumbs')[0].text
|
||||
|
||||
def click_all_topics(self):
|
||||
""" Click on the "All Topics" breadcrumb """
|
||||
self.q(css='a.nav-item').filter(text='All Topics')[0].click()
|
||||
|
||||
def click_specific_topic(self, topic):
|
||||
""" Click on the breadcrumb for a specific topic """
|
||||
self.q(css='a.nav-item').filter(text=topic)[0].click()
|
||||
|
||||
|
||||
class TeamsPage(CoursePage, BreadcrumbsMixin):
|
||||
"""
|
||||
Teams page/tab.
|
||||
"""
|
||||
@@ -92,7 +109,7 @@ class TeamsPage(CoursePage):
|
||||
|
||||
# Click to "My Team" and verify that it contains the expected number of teams.
|
||||
self.q(css=MY_TEAMS_BUTTON_CSS).click()
|
||||
|
||||
self.wait_for_ajax()
|
||||
self.wait_for(
|
||||
lambda: len(self.q(css='.team-card')) == expected_count,
|
||||
description="Expected number of teams is wrong"
|
||||
@@ -173,7 +190,7 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin):
|
||||
self.wait_for_ajax()
|
||||
|
||||
|
||||
class BaseTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin):
|
||||
class BaseTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin, BreadcrumbsMixin):
|
||||
"""
|
||||
The paginated UI for browsing teams within a Topic on the Teams
|
||||
page.
|
||||
@@ -211,6 +228,11 @@ class BaseTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin):
|
||||
lambda e: e.is_selected()
|
||||
).results[0].text.strip()
|
||||
|
||||
@property
|
||||
def team_names(self):
|
||||
"""Get all the team names on the page."""
|
||||
return self.q(css=CARD_TITLE_CSS).map(lambda e: e.text).results
|
||||
|
||||
def click_create_team_link(self):
|
||||
""" Click on create team link."""
|
||||
query = self.q(css=CREATE_TEAM_LINK_CSS)
|
||||
@@ -282,9 +304,9 @@ class SearchTeamsPage(BaseTeamsPage):
|
||||
self.url_path = "teams/#topics/{topic_id}/search".format(topic_id=self.topic['id'])
|
||||
|
||||
|
||||
class CreateOrEditTeamPage(CoursePage, FieldsMixin):
|
||||
class TeamManagementPage(CoursePage, FieldsMixin, BreadcrumbsMixin):
|
||||
"""
|
||||
Create team page.
|
||||
Team page for creation, editing, and deletion.
|
||||
"""
|
||||
def __init__(self, browser, course_id, topic):
|
||||
"""
|
||||
@@ -293,15 +315,13 @@ class CreateOrEditTeamPage(CoursePage, FieldsMixin):
|
||||
representation of a topic following the same convention as a
|
||||
course module's topic.
|
||||
"""
|
||||
super(CreateOrEditTeamPage, self).__init__(browser, course_id)
|
||||
super(TeamManagementPage, self).__init__(browser, course_id)
|
||||
self.topic = topic
|
||||
self.url_path = "teams/#topics/{topic_id}/create-team".format(topic_id=self.topic['id'])
|
||||
|
||||
def is_browser_on_page(self):
|
||||
"""Check if we're on the create team page for a particular topic."""
|
||||
has_correct_url = self.url.endswith(self.url_path)
|
||||
teams_create_view_present = self.q(css='.team-edit-fields').present
|
||||
return has_correct_url and teams_create_view_present
|
||||
return self.q(css='.team-edit-fields').present
|
||||
|
||||
@property
|
||||
def header_page_name(self):
|
||||
@@ -313,11 +333,6 @@ class CreateOrEditTeamPage(CoursePage, FieldsMixin):
|
||||
"""Get the page description displayed by the page header"""
|
||||
return self.q(css='.page-header .page-description')[0].text
|
||||
|
||||
@property
|
||||
def header_page_breadcrumbs(self):
|
||||
"""Get the page breadcrumb text displayed by the page header"""
|
||||
return self.q(css='.page-header .breadcrumbs')[0].text
|
||||
|
||||
@property
|
||||
def validation_message_text(self):
|
||||
"""Get the error message text"""
|
||||
@@ -333,8 +348,70 @@ class CreateOrEditTeamPage(CoursePage, FieldsMixin):
|
||||
self.q(css='.create-team .action-cancel').first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
@property
|
||||
def delete_team_button(self):
|
||||
"""Returns the 'delete team' button."""
|
||||
return self.q(css='.action-delete').first
|
||||
|
||||
class TeamPage(CoursePage, PaginatedUIMixin):
|
||||
def click_membership_button(self):
|
||||
"""Clicks the 'edit membership' button"""
|
||||
self.q(css='.action-edit-members').first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
@property
|
||||
def membership_button_present(self):
|
||||
"""Checks if the edit membership button is present"""
|
||||
return self.q(css='.action-edit-members').present
|
||||
|
||||
|
||||
class EditMembershipPage(CoursePage):
|
||||
"""
|
||||
Staff or discussion-privileged user page to remove troublesome or inactive
|
||||
students from a team
|
||||
"""
|
||||
def __init__(self, browser, course_id, team):
|
||||
"""
|
||||
Set up `self.url_path` on instantiation, since it dynamically
|
||||
reflects the current team.
|
||||
"""
|
||||
super(EditMembershipPage, self).__init__(browser, course_id)
|
||||
self.team = team
|
||||
self.url_path = "teams/#teams/{topic_id}/{team_id}/edit-team/manage-members".format(
|
||||
topic_id=self.team['topic_id'], team_id=self.team['id']
|
||||
)
|
||||
|
||||
def is_browser_on_page(self):
|
||||
"""Check if we're on the team membership page for a particular team."""
|
||||
self.wait_for_ajax()
|
||||
|
||||
if self.q(css='.edit-members').present:
|
||||
return True
|
||||
empty_query = self.q(css='.teams-main>.page-content>p').first
|
||||
return (
|
||||
len(empty_query.results) > 0 and
|
||||
empty_query[0].text == "This team does not have any members."
|
||||
)
|
||||
|
||||
@property
|
||||
def team_members(self):
|
||||
"""Returns the number of team members shown on the page."""
|
||||
return len(self.q(css='.team-member'))
|
||||
|
||||
def click_first_remove(self):
|
||||
"""Clicks the remove link on the first member listed."""
|
||||
self.q(css='.action-remove-member').first.click()
|
||||
|
||||
def confirm_delete_membership_dialog(self):
|
||||
"""Click 'delete' on the warning dialog."""
|
||||
confirm_prompt(self, require_notification=False)
|
||||
self.wait_for_ajax()
|
||||
|
||||
def cancel_delete_membership_dialog(self):
|
||||
"""Click 'delete' on the warning dialog."""
|
||||
confirm_prompt(self, cancel=True)
|
||||
|
||||
|
||||
class TeamPage(CoursePage, PaginatedUIMixin, BreadcrumbsMixin):
|
||||
"""
|
||||
The page for a specific Team within the Teams tab
|
||||
"""
|
||||
@@ -483,11 +560,6 @@ class TeamPage(CoursePage, PaginatedUIMixin):
|
||||
""" Returns True if New Post button is present else False """
|
||||
return self.q(css='.discussion-module .new-post-btn').present
|
||||
|
||||
def click_all_topics_breadcrumb(self):
|
||||
"""Navigate to the 'All Topics' page."""
|
||||
self.q(css='.breadcrumbs a').results[0].click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
@property
|
||||
def edit_team_button_present(self):
|
||||
""" Returns True if Edit Team button is present else False """
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from bok_choy.page_object import PageObject
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from utils import click_css
|
||||
from ..common.utils import click_css
|
||||
from selenium.webdriver.support.ui import Select
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ from bok_choy.page_object import PageObject
|
||||
from bok_choy.promise import Promise, EmptyPromise
|
||||
from . import BASE_URL
|
||||
|
||||
from .utils import click_css, confirm_prompt, type_in_codemirror
|
||||
from ..common.utils import click_css, confirm_prompt
|
||||
|
||||
from .utils import type_in_codemirror
|
||||
|
||||
|
||||
class ContainerPage(PageObject):
|
||||
|
||||
@@ -9,7 +9,8 @@ import os
|
||||
import re
|
||||
import requests
|
||||
|
||||
from .utils import click_css
|
||||
from ..common.utils import click_css
|
||||
|
||||
from .library import LibraryPage
|
||||
from .course_page import CoursePage
|
||||
from . import BASE_URL
|
||||
|
||||
@@ -9,7 +9,9 @@ from .component_editor import ComponentEditorView
|
||||
from .container import XBlockWrapper
|
||||
from ...pages.studio.users import UsersPageMixin
|
||||
from ...pages.studio.pagination import PaginatedMixin
|
||||
from .utils import confirm_prompt, wait_for_notification
|
||||
|
||||
from ..common.utils import confirm_prompt, wait_for_notification
|
||||
|
||||
from . import BASE_URL
|
||||
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ from bok_choy.promise import EmptyPromise
|
||||
from selenium.webdriver.support.ui import Select
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from ..common.utils import click_css, confirm_prompt
|
||||
|
||||
from .course_page import CoursePage
|
||||
from .container import ContainerPage
|
||||
from .utils import set_input_value_and_save, set_input_value, click_css, confirm_prompt
|
||||
from .utils import set_input_value_and_save, set_input_value
|
||||
|
||||
|
||||
class CourseOutlineItem(object):
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
Course Group Configurations page.
|
||||
"""
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from ..common.utils import confirm_prompt
|
||||
from .course_page import CoursePage
|
||||
from .utils import confirm_prompt
|
||||
|
||||
|
||||
class GroupConfigurationsPage(CoursePage):
|
||||
|
||||
@@ -4,8 +4,8 @@ Course Textbooks page.
|
||||
|
||||
import requests
|
||||
from path import Path as path
|
||||
from ..common.utils import click_css
|
||||
from .course_page import CoursePage
|
||||
from .utils import click_css
|
||||
|
||||
|
||||
class TextbooksPage(CoursePage):
|
||||
|
||||
@@ -3,52 +3,9 @@ Utility methods useful for Studio page tests.
|
||||
"""
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from bok_choy.javascript import js_defined
|
||||
|
||||
from ...tests.helpers import disable_animations
|
||||
|
||||
|
||||
def click_css(page, css, source_index=0, require_notification=True):
|
||||
"""
|
||||
Click the button/link with the given css and index on the specified page (subclass of PageObject).
|
||||
|
||||
Will only consider elements that are displayed and have a height and width greater than zero.
|
||||
|
||||
If require_notification is False (default value is True), the method will return immediately.
|
||||
Otherwise, it will wait for the "mini-notification" to appear and disappear.
|
||||
"""
|
||||
def _is_visible(el):
|
||||
# Only make the call to size once (instead of once for the height and once for the width)
|
||||
# because otherwise you will trigger a extra query on a remote element.
|
||||
return el.is_displayed() and all(size > 0 for size in el.size.itervalues())
|
||||
|
||||
# Disable all animations for faster testing with more reliable synchronization
|
||||
disable_animations(page)
|
||||
# Click on the element in the browser
|
||||
page.q(css=css).filter(lambda el: _is_visible(el)).nth(source_index).click()
|
||||
|
||||
if require_notification:
|
||||
wait_for_notification(page)
|
||||
|
||||
# Some buttons trigger ajax posts
|
||||
# (e.g. .add-missing-groups-button as configured in split_test_author_view.js)
|
||||
# so after you click anything wait for the ajax call to finish
|
||||
page.wait_for_ajax()
|
||||
|
||||
|
||||
def wait_for_notification(page):
|
||||
"""
|
||||
Waits for the "mini-notification" to appear and disappear on the given page (subclass of PageObject).
|
||||
"""
|
||||
def _is_saving():
|
||||
return page.q(css='.wrapper-notification-mini.is-shown').present
|
||||
|
||||
def _is_saving_done():
|
||||
return page.q(css='.wrapper-notification-mini.is-hiding').present
|
||||
|
||||
EmptyPromise(_is_saving, 'Notification should have been shown.', timeout=60).fulfill()
|
||||
EmptyPromise(_is_saving_done, 'Notification should have been hidden.', timeout=60).fulfill()
|
||||
from ..common.utils import click_css, wait_for_notification
|
||||
|
||||
|
||||
@js_defined('window.jQuery')
|
||||
@@ -177,18 +134,6 @@ def get_codemirror_value(page, index=0, find_prefix="$"):
|
||||
)
|
||||
|
||||
|
||||
def confirm_prompt(page, cancel=False, require_notification=None):
|
||||
"""
|
||||
Ensures that a modal prompt and confirmation button are visible, then clicks the button. The prompt is canceled iff
|
||||
cancel is True.
|
||||
"""
|
||||
page.wait_for_element_visibility('.prompt', 'Prompt is visible')
|
||||
confirmation_button_css = '.prompt .action-' + ('secondary' if cancel else 'primary')
|
||||
page.wait_for_element_visibility(confirmation_button_css, 'Confirmation button is visible')
|
||||
require_notification = (not cancel) if require_notification is None else require_notification
|
||||
click_css(page, confirmation_button_css, require_notification=require_notification)
|
||||
|
||||
|
||||
def set_input_value(page, css, value):
|
||||
"""
|
||||
Sets the text field with the given label (display name) to the specified value.
|
||||
|
||||
@@ -8,8 +8,8 @@ from bok_choy.promise import EmptyPromise, Promise
|
||||
from bok_choy.javascript import wait_for_js, js_defined
|
||||
from ....tests.helpers import YouTubeStubConfig
|
||||
from ...lms.video.video import VideoPage
|
||||
from ...common.utils import wait_for_notification
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from ..utils import wait_for_notification
|
||||
|
||||
|
||||
CLASS_SELECTORS = {
|
||||
|
||||
@@ -7,7 +7,8 @@ from nose.plugins.attrib import attr
|
||||
|
||||
from ..helpers import UniqueCourseTest, remove_file
|
||||
from ...pages.common.logout import LogoutPage
|
||||
from ...pages.studio.utils import add_html_component, click_css, type_in_codemirror
|
||||
from ...pages.common.utils import click_css
|
||||
from ...pages.studio.utils import add_html_component, type_in_codemirror
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
from ...pages.studio.container import ContainerPage
|
||||
|
||||
@@ -7,7 +7,8 @@ import json
|
||||
from bok_choy.web_app_test import WebAppTest
|
||||
from ..helpers import generate_course_key
|
||||
from ...pages.common.logout import LogoutPage
|
||||
from ...pages.studio.utils import add_html_component, click_css, type_in_codemirror
|
||||
from ...pages.common.utils import click_css
|
||||
from ...pages.studio.utils import add_html_component, type_in_codemirror
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
from ...pages.studio.container import ContainerPage
|
||||
|
||||
@@ -22,7 +22,16 @@ from ...pages.lms.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.course_info import CourseInfoPage
|
||||
from ...pages.lms.learner_profile import LearnerProfilePage
|
||||
from ...pages.lms.tab_nav import TabNavPage
|
||||
from ...pages.lms.teams import TeamsPage, MyTeamsPage, BrowseTopicsPage, BrowseTeamsPage, CreateOrEditTeamPage, TeamPage
|
||||
from ...pages.lms.teams import (
|
||||
TeamsPage,
|
||||
MyTeamsPage,
|
||||
BrowseTopicsPage,
|
||||
BrowseTeamsPage,
|
||||
TeamManagementPage,
|
||||
EditMembershipPage,
|
||||
TeamPage
|
||||
)
|
||||
from ...pages.common.utils import confirm_prompt
|
||||
|
||||
|
||||
TOPICS_PER_PAGE = 12
|
||||
@@ -199,7 +208,7 @@ class TeamsTabTest(TeamsTabBase):
|
||||
@ddt.data(
|
||||
'topics/{topic_id}',
|
||||
'topics/{topic_id}/search',
|
||||
'topics/{topic_id}/{team_id}/edit-team',
|
||||
'teams/{topic_id}/{team_id}/edit-team',
|
||||
'teams/{topic_id}/{team_id}'
|
||||
)
|
||||
def test_unauthorized_error_message(self, route):
|
||||
@@ -209,10 +218,10 @@ class TeamsTabTest(TeamsTabBase):
|
||||
"""
|
||||
topics = self.create_topics(1)
|
||||
topic = topics[0]
|
||||
self.set_team_configuration({
|
||||
u'max_team_size': 10,
|
||||
u'topics': topics
|
||||
})
|
||||
self.set_team_configuration(
|
||||
{u'max_team_size': 10, u'topics': topics},
|
||||
global_staff=True
|
||||
)
|
||||
team = self.create_teams(topic, 1)[0]
|
||||
self.teams_page.visit()
|
||||
self.browser.delete_cookie('sessionid')
|
||||
@@ -384,7 +393,7 @@ class BrowseTopicsTest(TeamsTabBase):
|
||||
browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, topic)
|
||||
self.assertTrue(browse_teams_page.is_browser_on_page())
|
||||
browse_teams_page.click_create_team_link()
|
||||
create_team_page = CreateOrEditTeamPage(self.browser, self.course_id, topic)
|
||||
create_team_page = TeamManagementPage(self.browser, self.course_id, topic)
|
||||
create_team_page.value_for_text_field(field_id='name', value='Team Name', press_enter=False)
|
||||
create_team_page.value_for_textarea_field(
|
||||
field_id='description',
|
||||
@@ -393,8 +402,9 @@ class BrowseTopicsTest(TeamsTabBase):
|
||||
create_team_page.submit_form()
|
||||
team_page = TeamPage(self.browser, self.course_id)
|
||||
self.assertTrue(team_page.is_browser_on_page)
|
||||
team_page.click_all_topics_breadcrumb()
|
||||
team_page.click_all_topics()
|
||||
self.assertTrue(self.topics_page.is_browser_on_page())
|
||||
self.topics_page.wait_for_ajax()
|
||||
self.assertEqual(topic_name, self.topics_page.topic_names[0])
|
||||
|
||||
def test_list_topics(self):
|
||||
@@ -834,21 +844,25 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
|
||||
@attr('shard_5')
|
||||
class TeamFormActions(TeamsTabBase):
|
||||
"""
|
||||
Base class for create & edit team.
|
||||
Base class for create, edit, and delete team.
|
||||
"""
|
||||
TEAM_DESCRIPTION = 'The Avengers are a fictional team of superheroes.'
|
||||
|
||||
topic = {'name': 'Example Topic', 'id': 'example_topic', 'description': 'Description'}
|
||||
TEAMS_NAME = 'Avengers'
|
||||
|
||||
def setUp(self):
|
||||
super(TeamFormActions, self).setUp()
|
||||
self.team_management_page = TeamManagementPage(self.browser, self.course_id, self.topic)
|
||||
|
||||
def verify_page_header(self, title, description, breadcrumbs):
|
||||
"""
|
||||
Verify that the page header correctly reflects the
|
||||
create team header, description and breadcrumb.
|
||||
"""
|
||||
self.assertEqual(self.create_or_edit_team_page.header_page_name, title)
|
||||
self.assertEqual(self.create_or_edit_team_page.header_page_description, description)
|
||||
self.assertEqual(self.create_or_edit_team_page.header_page_breadcrumbs, breadcrumbs)
|
||||
self.assertEqual(self.team_management_page.header_page_name, title)
|
||||
self.assertEqual(self.team_management_page.header_page_description, description)
|
||||
self.assertEqual(self.team_management_page.header_page_breadcrumbs, breadcrumbs)
|
||||
|
||||
def verify_and_navigate_to_create_team_page(self):
|
||||
"""Navigates to the create team page and verifies."""
|
||||
@@ -868,7 +882,7 @@ class TeamFormActions(TeamsTabBase):
|
||||
|
||||
self.team_page.click_edit_team_button()
|
||||
|
||||
self.create_or_edit_team_page.wait_for_page()
|
||||
self.team_management_page.wait_for_page()
|
||||
|
||||
# Edit page header.
|
||||
self.verify_page_header(
|
||||
@@ -891,33 +905,37 @@ class TeamFormActions(TeamsTabBase):
|
||||
|
||||
def fill_create_or_edit_form(self):
|
||||
"""Fill the create/edit team form fields with appropriate values."""
|
||||
self.create_or_edit_team_page.value_for_text_field(field_id='name', value=self.TEAMS_NAME, press_enter=False)
|
||||
self.create_or_edit_team_page.value_for_textarea_field(
|
||||
self.team_management_page.value_for_text_field(
|
||||
field_id='name',
|
||||
value=self.TEAMS_NAME,
|
||||
press_enter=False
|
||||
)
|
||||
self.team_management_page.value_for_textarea_field(
|
||||
field_id='description',
|
||||
value=self.TEAM_DESCRIPTION
|
||||
)
|
||||
self.create_or_edit_team_page.value_for_dropdown_field(field_id='language', value='English')
|
||||
self.create_or_edit_team_page.value_for_dropdown_field(field_id='country', value='Pakistan')
|
||||
self.team_management_page.value_for_dropdown_field(field_id='language', value='English')
|
||||
self.team_management_page.value_for_dropdown_field(field_id='country', value='Pakistan')
|
||||
|
||||
def verify_all_fields_exist(self):
|
||||
"""
|
||||
Verify the fields for create/edit page.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.create_or_edit_team_page.message_for_field('name'),
|
||||
self.team_management_page.message_for_field('name'),
|
||||
'A name that identifies your team (maximum 255 characters).'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.create_or_edit_team_page.message_for_textarea_field('description'),
|
||||
self.team_management_page.message_for_textarea_field('description'),
|
||||
'A short description of the team to help other learners understand '
|
||||
'the goals or direction of the team (maximum 300 characters).'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.create_or_edit_team_page.message_for_field('country'),
|
||||
self.team_management_page.message_for_field('country'),
|
||||
'The country that team members primarily identify with.'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.create_or_edit_team_page.message_for_field('language'),
|
||||
self.team_management_page.message_for_field('language'),
|
||||
'The language that team members primarily use to communicate with each other.'
|
||||
)
|
||||
|
||||
@@ -932,7 +950,6 @@ class CreateTeamTest(TeamFormActions):
|
||||
super(CreateTeamTest, self).setUp()
|
||||
self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]})
|
||||
|
||||
self.create_or_edit_team_page = CreateOrEditTeamPage(self.browser, self.course_id, self.topic)
|
||||
self.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic)
|
||||
self.browse_teams_page.visit()
|
||||
|
||||
@@ -960,14 +977,14 @@ class CreateTeamTest(TeamFormActions):
|
||||
Then I should see the error message and highlighted fields.
|
||||
"""
|
||||
self.verify_and_navigate_to_create_team_page()
|
||||
self.create_or_edit_team_page.submit_form()
|
||||
self.team_management_page.submit_form()
|
||||
|
||||
self.assertEqual(
|
||||
self.create_or_edit_team_page.validation_message_text,
|
||||
self.team_management_page.validation_message_text,
|
||||
'Check the highlighted fields below and try again.'
|
||||
)
|
||||
self.assertTrue(self.create_or_edit_team_page.error_for_field(field_id='name'))
|
||||
self.assertTrue(self.create_or_edit_team_page.error_for_field(field_id='description'))
|
||||
self.assertTrue(self.team_management_page.error_for_field(field_id='name'))
|
||||
self.assertTrue(self.team_management_page.error_for_field(field_id='description'))
|
||||
|
||||
def test_user_can_see_error_message_for_incorrect_data(self):
|
||||
"""
|
||||
@@ -982,7 +999,7 @@ class CreateTeamTest(TeamFormActions):
|
||||
self.verify_and_navigate_to_create_team_page()
|
||||
|
||||
# Fill the name field with >255 characters to see validation message.
|
||||
self.create_or_edit_team_page.value_for_text_field(
|
||||
self.team_management_page.value_for_text_field(
|
||||
field_id='name',
|
||||
value='EdX is a massive open online course (MOOC) provider and online learning platform. '
|
||||
'It hosts online university-level courses in a wide range of disciplines to a worldwide '
|
||||
@@ -994,13 +1011,13 @@ class CreateTeamTest(TeamFormActions):
|
||||
'edX has more than 4 million users taking more than 500 courses online.',
|
||||
press_enter=False
|
||||
)
|
||||
self.create_or_edit_team_page.submit_form()
|
||||
self.team_management_page.submit_form()
|
||||
|
||||
self.assertEqual(
|
||||
self.create_or_edit_team_page.validation_message_text,
|
||||
self.team_management_page.validation_message_text,
|
||||
'Check the highlighted fields below and try again.'
|
||||
)
|
||||
self.assertTrue(self.create_or_edit_team_page.error_for_field(field_id='name'))
|
||||
self.assertTrue(self.team_management_page.error_for_field(field_id='name'))
|
||||
|
||||
def test_user_can_create_new_team_successfully(self):
|
||||
"""
|
||||
@@ -1040,7 +1057,7 @@ class CreateTeamTest(TeamFormActions):
|
||||
}
|
||||
]
|
||||
with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events):
|
||||
self.create_or_edit_team_page.submit_form()
|
||||
self.team_management_page.submit_form()
|
||||
|
||||
# Verify that the page is shown for the new team
|
||||
team_page = TeamPage(self.browser, self.course_id)
|
||||
@@ -1072,7 +1089,7 @@ class CreateTeamTest(TeamFormActions):
|
||||
self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total'))
|
||||
|
||||
self.verify_and_navigate_to_create_team_page()
|
||||
self.create_or_edit_team_page.cancel_team()
|
||||
self.team_management_page.cancel_team()
|
||||
|
||||
self.assertTrue(self.browse_teams_page.is_browser_on_page())
|
||||
self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total'))
|
||||
@@ -1101,6 +1118,131 @@ class CreateTeamTest(TeamFormActions):
|
||||
self.verify_and_navigate_to_create_team_page()
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class DeleteTeamTest(TeamFormActions):
|
||||
"""
|
||||
Tests for deleting teams.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(DeleteTeamTest, self).setUp()
|
||||
|
||||
self.set_team_configuration(
|
||||
{'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]},
|
||||
global_staff=True
|
||||
)
|
||||
|
||||
self.team = self.create_teams(self.topic, num_teams=1)[0]
|
||||
self.team_page = TeamPage(self.browser, self.course_id, team=self.team)
|
||||
|
||||
#need to have a membership to confirm it gets deleted as well
|
||||
self.create_membership(self.user_info['username'], self.team['id'])
|
||||
|
||||
self.team_page.visit()
|
||||
|
||||
def test_cancel_delete(self):
|
||||
"""
|
||||
Scenario: The user should be able to cancel the Delete Team dialog
|
||||
Given I am staff user for a course with a team
|
||||
When I visit the Team profile page
|
||||
Then I should see the Edit Team button
|
||||
And When I click edit team button
|
||||
Then I should see the Delete Team button
|
||||
When I click the delete team button
|
||||
And I cancel the prompt
|
||||
And I refresh the page
|
||||
Then I should still see the team
|
||||
"""
|
||||
self.delete_team(cancel=True)
|
||||
self.assertTrue(self.team_management_page.is_browser_on_page())
|
||||
self.browser.refresh()
|
||||
self.team_management_page.wait_for_page()
|
||||
self.assertEqual(
|
||||
' '.join(('All Topics', self.topic['name'], self.team['name'])),
|
||||
self.team_management_page.header_page_breadcrumbs
|
||||
)
|
||||
|
||||
@ddt.data('Moderator', 'Community TA', 'Administrator', None)
|
||||
def test_delete_team(self, role):
|
||||
"""
|
||||
Scenario: The user should be able to see and navigate to the delete team page.
|
||||
Given I am staff user for a course with a team
|
||||
When I visit the Team profile page
|
||||
Then I should see the Edit Team button
|
||||
And When I click edit team button
|
||||
Then I should see the Delete Team button
|
||||
When I click the delete team button
|
||||
And I confirm the prompt
|
||||
Then I should see the browse teams page
|
||||
And the team should not be present
|
||||
"""
|
||||
# If role is None, remain logged in as global staff
|
||||
if role is not None:
|
||||
AutoAuthPage(
|
||||
self.browser,
|
||||
course_id=self.course_id,
|
||||
staff=False,
|
||||
roles=role
|
||||
).visit()
|
||||
self.team_page.visit()
|
||||
self.delete_team(require_notification=False)
|
||||
browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic)
|
||||
self.assertTrue(browse_teams_page.is_browser_on_page())
|
||||
self.assertNotIn(self.team['name'], browse_teams_page.team_names)
|
||||
|
||||
def delete_team(self, **kwargs):
|
||||
"""
|
||||
Delete a team. Passes `kwargs` to `confirm_prompt`.
|
||||
Expects edx.team.deleted event to be emitted, with correct course_id.
|
||||
Also expects edx.team.learner_removed event to be emitted for the
|
||||
membership that is removed as a part of the delete operation.
|
||||
"""
|
||||
|
||||
self.team_page.click_edit_team_button()
|
||||
self.team_management_page.wait_for_page()
|
||||
self.team_management_page.delete_team_button.click()
|
||||
|
||||
if 'cancel' in kwargs and kwargs['cancel'] is True:
|
||||
confirm_prompt(self.team_management_page, **kwargs)
|
||||
else:
|
||||
expected_events = [
|
||||
{
|
||||
'event_type': 'edx.team.deleted',
|
||||
'event': {
|
||||
'course_id': self.course_id,
|
||||
'team_id': self.team['id']
|
||||
}
|
||||
},
|
||||
{
|
||||
'event_type': 'edx.team.learner_removed',
|
||||
'event': {
|
||||
'course_id': self.course_id,
|
||||
'team_id': self.team['id'],
|
||||
'remove_method': 'team_deleted',
|
||||
'user_id': self.user_info['user_id']
|
||||
}
|
||||
}
|
||||
]
|
||||
with self.assert_events_match_during(
|
||||
event_filter=self.only_team_events, expected_events=expected_events
|
||||
):
|
||||
confirm_prompt(self.team_management_page, **kwargs)
|
||||
|
||||
def test_delete_team_updates_topics(self):
|
||||
"""
|
||||
Scenario: Deleting a team should update the team count on the topics page
|
||||
Given I am staff user for a course with a team
|
||||
And I delete a team
|
||||
When I navigate to the browse topics page
|
||||
Then the team count for the deletd team's topic should be updated
|
||||
"""
|
||||
self.delete_team(require_notification=False)
|
||||
BrowseTeamsPage(self.browser, self.course_id, self.topic).click_all_topics()
|
||||
topics_page = BrowseTopicsPage(self.browser, self.course_id)
|
||||
self.assertTrue(topics_page.is_browser_on_page())
|
||||
self.teams_page.verify_topic_team_count(0)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class EditTeamTest(TeamFormActions):
|
||||
"""
|
||||
@@ -1114,7 +1256,6 @@ class EditTeamTest(TeamFormActions):
|
||||
{'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]},
|
||||
global_staff=True
|
||||
)
|
||||
self.create_or_edit_team_page = CreateOrEditTeamPage(self.browser, self.course_id, self.topic)
|
||||
|
||||
self.team = self.create_teams(self.topic, num_teams=1)[0]
|
||||
self.team_page = TeamPage(self.browser, self.course_id, team=self.team)
|
||||
@@ -1204,7 +1345,7 @@ class EditTeamTest(TeamFormActions):
|
||||
},
|
||||
]
|
||||
with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events):
|
||||
self.create_or_edit_team_page.submit_form()
|
||||
self.team_management_page.submit_form()
|
||||
|
||||
self.team_page.wait_for_page()
|
||||
|
||||
@@ -1237,7 +1378,7 @@ class EditTeamTest(TeamFormActions):
|
||||
self.verify_and_navigate_to_edit_team_page()
|
||||
|
||||
self.fill_create_or_edit_form()
|
||||
self.create_or_edit_team_page.cancel_team()
|
||||
self.team_management_page.cancel_team()
|
||||
|
||||
self.team_page.wait_for_page()
|
||||
|
||||
@@ -1289,7 +1430,7 @@ class EditTeamTest(TeamFormActions):
|
||||
self.verify_and_navigate_to_edit_team_page()
|
||||
|
||||
self.fill_create_or_edit_form()
|
||||
self.create_or_edit_team_page.submit_form()
|
||||
self.team_management_page.submit_form()
|
||||
|
||||
self.team_page.wait_for_page()
|
||||
|
||||
@@ -1319,6 +1460,108 @@ class EditTeamTest(TeamFormActions):
|
||||
self.verify_and_navigate_to_edit_team_page()
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class EditMembershipTest(TeamFormActions):
|
||||
"""
|
||||
Tests for administrating from the team membership page
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(EditMembershipTest, self).setUp()
|
||||
|
||||
self.set_team_configuration(
|
||||
{'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]},
|
||||
global_staff=True
|
||||
)
|
||||
self.team_management_page = TeamManagementPage(self.browser, self.course_id, self.topic)
|
||||
self.team = self.create_teams(self.topic, num_teams=1)[0]
|
||||
|
||||
#make sure a user exists on this team so we can edit the membership
|
||||
self.create_membership(self.user_info['username'], self.team['id'])
|
||||
|
||||
self.edit_membership_page = EditMembershipPage(self.browser, self.course_id, self.team)
|
||||
self.team_page = TeamPage(self.browser, self.course_id, team=self.team)
|
||||
|
||||
def edit_membership_helper(self, role, cancel=False):
|
||||
"""
|
||||
Helper for common functionality in edit membership tests.
|
||||
Checks for all relevant assertions about membership being removed,
|
||||
including verify edx.team.learner_removed events are emitted.
|
||||
"""
|
||||
if role is not None:
|
||||
AutoAuthPage(
|
||||
self.browser,
|
||||
course_id=self.course_id,
|
||||
staff=False,
|
||||
roles=role
|
||||
).visit()
|
||||
|
||||
self.team_page.visit()
|
||||
self.team_page.click_edit_team_button()
|
||||
self.team_management_page.wait_for_page()
|
||||
|
||||
self.assertTrue(
|
||||
self.team_management_page.membership_button_present
|
||||
)
|
||||
|
||||
self.team_management_page.click_membership_button()
|
||||
self.edit_membership_page.wait_for_page()
|
||||
self.edit_membership_page.click_first_remove()
|
||||
if cancel:
|
||||
self.edit_membership_page.cancel_delete_membership_dialog()
|
||||
self.assertEqual(self.edit_membership_page.team_members, 1)
|
||||
else:
|
||||
expected_events = [
|
||||
{
|
||||
'event_type': 'edx.team.learner_removed',
|
||||
'event': {
|
||||
'course_id': self.course_id,
|
||||
'team_id': self.team['id'],
|
||||
'remove_method': 'removed_by_admin',
|
||||
'user_id': self.user_info['user_id']
|
||||
}
|
||||
}
|
||||
]
|
||||
with self.assert_events_match_during(
|
||||
event_filter=self.only_team_events, expected_events=expected_events
|
||||
):
|
||||
self.edit_membership_page.confirm_delete_membership_dialog()
|
||||
self.assertEqual(self.edit_membership_page.team_members, 0)
|
||||
self.assertTrue(self.edit_membership_page.is_browser_on_page)
|
||||
|
||||
@ddt.data('Moderator', 'Community TA', 'Administrator', None)
|
||||
def test_remove_membership(self, role):
|
||||
"""
|
||||
Scenario: The user should be able to remove a membership
|
||||
Given I am staff user for a course with a team
|
||||
When I visit the Team profile page
|
||||
Then I should see the Edit Team button
|
||||
And When I click edit team button
|
||||
Then I should see the Edit Membership button
|
||||
And When I click the edit membership button
|
||||
Then I should see the edit membership page
|
||||
And When I click the remove button and confirm the dialog
|
||||
Then my membership should be removed, and I should remain on the page
|
||||
"""
|
||||
self.edit_membership_helper(role, cancel=False)
|
||||
|
||||
@ddt.data('Moderator', 'Community TA', 'Administrator', None)
|
||||
def test_cancel_remove_membership(self, role):
|
||||
"""
|
||||
Scenario: The user should be able to remove a membership
|
||||
Given I am staff user for a course with a team
|
||||
When I visit the Team profile page
|
||||
Then I should see the Edit Team button
|
||||
And When I click edit team button
|
||||
Then I should see the Edit Membership button
|
||||
And When I click the edit membership button
|
||||
Then I should see the edit membership page
|
||||
And When I click the remove button and cancel the dialog
|
||||
Then my membership should not be removed, and I should remain on the page
|
||||
"""
|
||||
self.edit_membership_helper(role, cancel=True)
|
||||
|
||||
|
||||
@attr('shard_5')
|
||||
@ddt.ddt
|
||||
class TeamPageTest(TeamsTabBase):
|
||||
|
||||
@@ -4,9 +4,10 @@ import logging
|
||||
from requests import ConnectionError
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_save
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import translation
|
||||
from functools import wraps
|
||||
|
||||
from search.search_engine_base import SearchEngine
|
||||
|
||||
@@ -14,6 +15,19 @@ from .errors import ElasticSearchConnectionError
|
||||
from .serializers import CourseTeamSerializer, CourseTeam
|
||||
|
||||
|
||||
def if_search_enabled(f):
|
||||
"""
|
||||
Only call `f` if search is enabled for the CourseTeamIndexer.
|
||||
"""
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Wraps the decorated function."""
|
||||
cls = args[0]
|
||||
if cls.search_is_enabled():
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class CourseTeamIndexer(object):
|
||||
"""
|
||||
This is the index object for searching and storing CourseTeam model instances.
|
||||
@@ -70,16 +84,25 @@ class CourseTeamIndexer(object):
|
||||
return self.course_team.language
|
||||
|
||||
@classmethod
|
||||
@if_search_enabled
|
||||
def index(cls, course_team):
|
||||
"""
|
||||
Update index with course_team object (if feature is enabled).
|
||||
"""
|
||||
if cls.search_is_enabled():
|
||||
search_engine = cls.engine()
|
||||
serialized_course_team = CourseTeamIndexer(course_team).data()
|
||||
search_engine.index(cls.DOCUMENT_TYPE_NAME, [serialized_course_team])
|
||||
search_engine = cls.engine()
|
||||
serialized_course_team = CourseTeamIndexer(course_team).data()
|
||||
search_engine.index(cls.DOCUMENT_TYPE_NAME, [serialized_course_team])
|
||||
|
||||
@classmethod
|
||||
@if_search_enabled
|
||||
def remove(cls, course_team):
|
||||
"""
|
||||
Remove course_team from the index (if feature is enabled).
|
||||
"""
|
||||
cls.engine().remove(cls.DOCUMENT_TYPE_NAME, [course_team.team_id])
|
||||
|
||||
@classmethod
|
||||
@if_search_enabled
|
||||
def engine(cls):
|
||||
"""
|
||||
Return course team search engine (if feature is enabled).
|
||||
@@ -108,3 +131,11 @@ def course_team_post_save_callback(**kwargs):
|
||||
CourseTeamIndexer.index(kwargs['instance'])
|
||||
except ElasticSearchConnectionError:
|
||||
pass
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CourseTeam, dispatch_uid='teams.signals.course_team_post_delete_callback')
|
||||
def course_team_post_delete_callback(**kwargs): # pylint: disable=invalid-name
|
||||
"""
|
||||
Reindex object after delete.
|
||||
"""
|
||||
CourseTeamIndexer.remove(kwargs['instance'])
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
|
||||
onUpdate: function(event) {
|
||||
if (event.action === 'create') {
|
||||
if (_.contains(['create', 'delete'], event.action)) {
|
||||
this.isStale = true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -35,5 +35,14 @@ define(['backbone', 'URI', 'underscore', 'common/js/spec_helpers/ajax_helpers',
|
||||
topicCollection.course_id = 'my+course+id';
|
||||
testRequestParam(this, 'course_id', 'my+course+id');
|
||||
});
|
||||
|
||||
it('sets itself to stale on receiving a teams create or delete event', function () {
|
||||
expect(topicCollection.isStale).toBe(false);
|
||||
TeamSpecHelpers.triggerTeamEvent('create');
|
||||
expect(topicCollection.isStale).toBe(true);
|
||||
topicCollection.isStale = false;
|
||||
TeamSpecHelpers.triggerTeamEvent('delete');
|
||||
expect(topicCollection.isStale).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'common/js/spec_helpers/ajax_helpers',
|
||||
'teams/js/views/edit_team_members',
|
||||
'teams/js/models/team',
|
||||
'teams/js/views/team_utils',
|
||||
'teams/js/spec_helpers/team_spec_helpers'
|
||||
], function ($, _, Backbone, AjaxHelpers, TeamEditMembershipView, TeamModel, TeamUtils, TeamSpecHelpers) {
|
||||
'use strict';
|
||||
|
||||
describe('CreateEditTeam', function() {
|
||||
var editTeamID = 'av',
|
||||
DEFAULT_MEMBERSHIP = [
|
||||
{
|
||||
'user': {
|
||||
'username': 'frodo',
|
||||
'profile_image': {
|
||||
'has_image': true,
|
||||
'image_url_medium': '/frodo-image-url'
|
||||
},
|
||||
},
|
||||
last_activity_at: "2015-08-21T18:53:01.145Z",
|
||||
date_joined: "2014-01-01T18:53:01.145Z"
|
||||
}
|
||||
],
|
||||
deleteTeamMemember = function (view, confirm) {
|
||||
view.$('.action-remove-member').click();
|
||||
// Confirm delete dialog
|
||||
if (confirm) {
|
||||
$('.action-primary').click();
|
||||
}
|
||||
else {
|
||||
$('.action-secondary').click();
|
||||
}
|
||||
},
|
||||
verifyTeamMembersView = function (view) {
|
||||
expect(view.$('.team-member').length).toEqual(1);
|
||||
expect(view.$('.member-profile').attr('href')).toEqual('/u/frodo');
|
||||
expect(view.$('img.image-url').attr('src')).toEqual('/frodo-image-url');
|
||||
expect(view.$('.member-info-container .primary').text()).toBe('frodo');
|
||||
expect(view.$el.find('#last-active abbr').attr('title')).toEqual("2015-08-21T18:53:01.145Z");
|
||||
expect(view.$el.find('#date-joined abbr').attr('title')).toEqual("2014-01-01T18:53:01.145Z");
|
||||
},
|
||||
verifyNoMembersView = function (view){
|
||||
expect(view.$el.text().trim()).toBe('This team does not have any members.');
|
||||
},
|
||||
createTeamModelData = function (membership) {
|
||||
return {
|
||||
id: editTeamID,
|
||||
name: 'Avengers',
|
||||
description: 'Team of dumbs',
|
||||
language: 'en',
|
||||
country: 'US',
|
||||
membership: membership,
|
||||
url: '/api/team/v0/teams/' + editTeamID
|
||||
};
|
||||
},
|
||||
createEditTeamMembersView = function (membership) {
|
||||
var teamModel = new TeamModel(
|
||||
createTeamModelData(membership),
|
||||
{ parse: true }
|
||||
);
|
||||
|
||||
return new TeamEditMembershipView({
|
||||
teamEvents: TeamSpecHelpers.teamEvents,
|
||||
el: $('.teams-content'),
|
||||
model: teamModel,
|
||||
context: TeamSpecHelpers.testContext
|
||||
}).render();
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
setFixtures('<div id="page-prompt"></div><div class="teams-content"></div>');
|
||||
spyOn(Backbone.history, 'navigate');
|
||||
spyOn(TeamUtils, 'showMessage');
|
||||
});
|
||||
|
||||
it('can render a message when there are no members', function () {
|
||||
var view = createEditTeamMembersView([]);
|
||||
verifyNoMembersView(view);
|
||||
});
|
||||
|
||||
it('can delete a team member and update the view', function () {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
view = createEditTeamMembersView(DEFAULT_MEMBERSHIP);
|
||||
|
||||
spyOn(view.teamEvents, 'trigger');
|
||||
verifyTeamMembersView(view);
|
||||
|
||||
deleteTeamMemember(view, true);
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests,
|
||||
'DELETE',
|
||||
'/api/team/v0/team_membership/av,frodo?admin=true'
|
||||
);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
expect(view.teamEvents.trigger).toHaveBeenCalledWith(
|
||||
'teams:update', {
|
||||
action: 'leave',
|
||||
team: view.model
|
||||
}
|
||||
);
|
||||
AjaxHelpers.expectJsonRequest(requests, 'GET', view.model.get('url'));
|
||||
AjaxHelpers.respondWithJson(requests, createTeamModelData([]));
|
||||
|
||||
verifyNoMembersView(view);
|
||||
});
|
||||
|
||||
it('can show an error message if removing the user fails', function () {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
view = createEditTeamMembersView(DEFAULT_MEMBERSHIP);
|
||||
|
||||
spyOn(view.teamEvents, 'trigger');
|
||||
verifyTeamMembersView(view);
|
||||
|
||||
deleteTeamMemember(view, true);
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests,
|
||||
'DELETE',
|
||||
'/api/team/v0/team_membership/av,frodo?admin=true'
|
||||
);
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
expect(TeamUtils.showMessage).toHaveBeenCalledWith(
|
||||
'An error occurred while removing the member from the team. Try again.',
|
||||
undefined
|
||||
);
|
||||
expect(view.teamEvents.trigger).not.toHaveBeenCalled();
|
||||
verifyTeamMembersView(view);
|
||||
});
|
||||
|
||||
it('can cancel team membership deletion', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var view = createEditTeamMembersView(DEFAULT_MEMBERSHIP);
|
||||
|
||||
spyOn(view.teamEvents, 'trigger');
|
||||
verifyTeamMembersView(view);
|
||||
|
||||
deleteTeamMemember(view, false);
|
||||
expect(requests.length).toBe(0);
|
||||
expect(view.teamEvents.trigger).not.toHaveBeenCalled();
|
||||
verifyTeamMembersView(view);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
define([
|
||||
'jquery',
|
||||
'backbone',
|
||||
'underscore',
|
||||
'teams/js/models/team',
|
||||
'teams/js/views/instructor_tools',
|
||||
'teams/js/views/team_utils',
|
||||
'teams/js/spec_helpers/team_spec_helpers',
|
||||
'common/js/spec_helpers/ajax_helpers'
|
||||
], function ($, Backbone, _, Team, InstructorToolsView, TeamUtils, TeamSpecHelpers, AjaxHelpers) {
|
||||
'use strict';
|
||||
|
||||
describe('Instructor Tools', function () {
|
||||
var view,
|
||||
createInstructorTools = function () {
|
||||
return new InstructorToolsView({
|
||||
team: new Team(TeamSpecHelpers.createMockTeamData(1, 1)[0]),
|
||||
teamEvents: TeamSpecHelpers.teamEvents
|
||||
});
|
||||
},
|
||||
deleteTeam = function (view, confirm) {
|
||||
view.$('.action-delete').click();
|
||||
// Confirm delete dialog
|
||||
if (confirm) {
|
||||
$('.action-primary').click();
|
||||
}
|
||||
else {
|
||||
$('.action-secondary').click();
|
||||
}
|
||||
},
|
||||
expectSuccessMessage = function (team) {
|
||||
expect(TeamUtils.showMessage).toHaveBeenCalledWith(
|
||||
'Team "' + team.get('name') + '" successfully deleted.',
|
||||
'success'
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
setFixtures('<div id="page-prompt"></div>');
|
||||
spyOn(Backbone.history, 'navigate');
|
||||
spyOn(TeamUtils, 'showMessage');
|
||||
view = createInstructorTools().render();
|
||||
spyOn(view.teamEvents, 'trigger');
|
||||
});
|
||||
|
||||
it('can render itself', function () {
|
||||
expect(_.strip(view.$('.action-delete').text())).toEqual('Delete Team');
|
||||
expect(_.strip(view.$('.action-edit-members').text())).toEqual('Edit Membership');
|
||||
expect(view.$el.text()).toContain('Instructor tools');
|
||||
});
|
||||
|
||||
it('can delete a team and shows a success message', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
deleteTeam(view, true);
|
||||
AjaxHelpers.expectJsonRequest(requests, 'DELETE', view.team.url, null);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
expect(Backbone.history.navigate).toHaveBeenCalledWith(
|
||||
'topics/' + view.team.get('topic_id'),
|
||||
{trigger: true}
|
||||
);
|
||||
expect(view.teamEvents.trigger).toHaveBeenCalledWith(
|
||||
'teams:update', {
|
||||
action: 'delete',
|
||||
team: view.team
|
||||
}
|
||||
);
|
||||
expectSuccessMessage(view.team);
|
||||
});
|
||||
|
||||
it('can cancel team deletion', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
deleteTeam(view, false);
|
||||
expect(requests.length).toBe(0);
|
||||
expect(Backbone.history.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a success message after receiving a 404', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
deleteTeam(view, true);
|
||||
AjaxHelpers.respondWithError(requests, 404);
|
||||
expectSuccessMessage(view.team);
|
||||
});
|
||||
|
||||
it('can trigger the edit membership view', function () {
|
||||
view.$('.action-edit-members').click();
|
||||
expect(Backbone.history.navigate).toHaveBeenCalledWith(
|
||||
'teams/' + view.team.get('topic_id') + "/" + view.team.id + "/edit-team/manage-members",
|
||||
{trigger: true}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -61,7 +61,7 @@ define([
|
||||
it('does not interfere with anchor links to #content', function () {
|
||||
var teamsTabView = createTeamsTabView();
|
||||
teamsTabView.router.navigate('#content', {trigger: true});
|
||||
expect(teamsTabView.$('.warning')).toHaveClass('is-hidden');
|
||||
expect(teamsTabView.$('.wrapper-msg')).toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
it('displays and focuses an error message when trying to navigate to a nonexistent page', function () {
|
||||
@@ -156,7 +156,7 @@ define([
|
||||
}
|
||||
],
|
||||
'fires a page view event for the edit team page': [
|
||||
'topics/' + TeamSpecHelpers.testTopicID + '/' + 'test_team_id/edit-team',
|
||||
'teams/' + TeamSpecHelpers.testTopicID + '/' + 'test_team_id/edit-team',
|
||||
{
|
||||
page_name: 'edit-team',
|
||||
topic_id: TeamSpecHelpers.testTopicID,
|
||||
@@ -165,7 +165,9 @@ define([
|
||||
]
|
||||
}, function (url, expectedEvent) {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
teamsTabView = createTeamsTabView();
|
||||
teamsTabView = createTeamsTabView({
|
||||
userInfo: TeamSpecHelpers.createMockUserInfo({ staff: true })
|
||||
});
|
||||
teamsTabView.router.navigate(url, {trigger: true});
|
||||
if (requests.length) {
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
|
||||
@@ -29,13 +29,16 @@ define([
|
||||
|
||||
var createMockTeamData = function (startIndex, stopIndex) {
|
||||
return _.map(_.range(startIndex, stopIndex + 1), function (i) {
|
||||
var id = "id" + i;
|
||||
return {
|
||||
name: "team " + i,
|
||||
id: "id " + i,
|
||||
id: id,
|
||||
language: testLanguages[i%4][0],
|
||||
country: testCountries[i%4][0],
|
||||
membership: [],
|
||||
last_activity_at: ''
|
||||
last_activity_at: '',
|
||||
topic_id: 'topic_id' + i,
|
||||
url: 'api/team/v0/teams/' + id
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -124,6 +127,10 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
var triggerTeamEvent = function (action) {
|
||||
teamEvents.trigger('teams:update', {action: action});
|
||||
};
|
||||
|
||||
createMockPostResponse = function(options) {
|
||||
return _.extend(
|
||||
{
|
||||
@@ -327,6 +334,7 @@ define([
|
||||
createMockThreadResponse: createMockThreadResponse,
|
||||
createMockTopicData: createMockTopicData,
|
||||
createMockTopicCollection: createMockTopicCollection,
|
||||
triggerTeamEvent: triggerTeamEvent,
|
||||
verifyCards: verifyCards
|
||||
};
|
||||
});
|
||||
|
||||
103
lms/djangoapps/teams/static/teams/js/views/edit_team_members.js
Normal file
103
lms/djangoapps/teams/static/teams/js/views/edit_team_members.js
Normal file
@@ -0,0 +1,103 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'teams/js/models/team',
|
||||
'teams/js/views/team_utils',
|
||||
'common/js/components/utils/view_utils',
|
||||
'text!teams/templates/edit-team-member.underscore',
|
||||
'text!teams/templates/date.underscore'
|
||||
],
|
||||
function (Backbone, $, _, gettext, TeamModel, TeamUtils, ViewUtils, editTeamMemberTemplate, dateTemplate) {
|
||||
return Backbone.View.extend({
|
||||
dateTemplate: _.template(dateTemplate),
|
||||
teamMemberTemplate: _.template(editTeamMemberTemplate),
|
||||
errorMessage: gettext("An error occurred while removing the member from the team. Try again."),
|
||||
|
||||
events: {
|
||||
'click .action-remove-member': 'removeMember'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.teamMembershipDetailUrl = options.context.teamMembershipDetailUrl;
|
||||
// The URL ends with team_id,request_username. We want to replace
|
||||
// the last occurrence of team_id with the actual team_id, and remove request_username
|
||||
// as the actual user to be removed from the team will be added on before calling DELETE.
|
||||
this.teamMembershipDetailUrl = this.teamMembershipDetailUrl.substring(
|
||||
0, this.teamMembershipDetailUrl.lastIndexOf('team_id')
|
||||
) + this.model.get('id') + ",";
|
||||
|
||||
this.teamEvents = options.teamEvents;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.model.get('membership').length === 0) {
|
||||
this.$el.html('<p>' + gettext('This team does not have any members.') + '</p>');
|
||||
}
|
||||
else {
|
||||
this.$el.html('<ul class="edit-members"></ul>');
|
||||
this.renderTeamMembers();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
renderTeamMembers: function() {
|
||||
var self = this, dateJoined, lastActivity;
|
||||
|
||||
_.each(this.model.get('membership'), function(membership) {
|
||||
dateJoined = interpolate(
|
||||
// Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: https://github.com/rmm5t/jquery-timeago)
|
||||
gettext("Joined %(date)s"),
|
||||
{date: self.dateTemplate({date: membership.date_joined})},
|
||||
true
|
||||
);
|
||||
|
||||
lastActivity = interpolate(
|
||||
// Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: https://github.com/rmm5t/jquery-timeago)
|
||||
gettext("Last Activity %(date)s"),
|
||||
{date: self.dateTemplate({date: membership.last_activity_at})},
|
||||
true
|
||||
);
|
||||
|
||||
// It is assumed that the team member array is automatically in the order of date joined.
|
||||
self.$('.edit-members').append(self.teamMemberTemplate({
|
||||
imageUrl: membership.user.profile_image.image_url_medium,
|
||||
username: membership.user.username,
|
||||
memberProfileUrl: '/u/' + membership.user.username,
|
||||
dateJoined: dateJoined,
|
||||
lastActive: lastActivity
|
||||
}));
|
||||
});
|
||||
this.$('abbr').timeago();
|
||||
},
|
||||
|
||||
removeMember: function (event) {
|
||||
var self = this, username = $(event.currentTarget).data('username');
|
||||
event.preventDefault();
|
||||
|
||||
ViewUtils.confirmThenRunOperation(
|
||||
gettext('Remove this team member?'),
|
||||
gettext('This learner will be removed from the team, allowing another learner to take the available spot.'),
|
||||
gettext('Remove'),
|
||||
function () {
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: self.teamMembershipDetailUrl.concat(username, '?admin=true')
|
||||
}).done(function () {
|
||||
self.teamEvents.trigger('teams:update', {
|
||||
action: 'leave',
|
||||
team: self.model
|
||||
});
|
||||
self.model.fetch().done(function() { self.render(); });
|
||||
}).fail(function (data) {
|
||||
TeamUtils.parseAndShowMessage(data, self.errorMessage);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,76 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
|
||||
define(['backbone',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'teams/js/views/team_utils',
|
||||
'common/js/components/utils/view_utils',
|
||||
'text!teams/templates/instructor-tools.underscore'],
|
||||
function (Backbone, _, gettext, TeamUtils, ViewUtils, instructorToolbarTemplate) {
|
||||
return Backbone.View.extend({
|
||||
|
||||
events: {
|
||||
'click .action-delete': 'deleteTeam',
|
||||
'click .action-edit-members': 'editMembership'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.template = _.template(instructorToolbarTemplate);
|
||||
this.team = options.team;
|
||||
this.teamEvents = options.teamEvents;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.template);
|
||||
return this;
|
||||
},
|
||||
|
||||
deleteTeam: function (event) {
|
||||
event.preventDefault();
|
||||
ViewUtils.confirmThenRunOperation(
|
||||
gettext('Delete this team?'),
|
||||
gettext('Deleting a team is permanent and cannot be undone. All members are removed from the team, and team discussions can no longer be accessed.'),
|
||||
gettext('Delete'),
|
||||
_.bind(this.handleDelete, this)
|
||||
);
|
||||
},
|
||||
|
||||
editMembership: function (event) {
|
||||
event.preventDefault();
|
||||
Backbone.history.navigate(
|
||||
'teams/' + this.team.get('topic_id') + '/' + this.team.id +'/edit-team/manage-members',
|
||||
{trigger: true}
|
||||
);
|
||||
},
|
||||
|
||||
handleDelete: function () {
|
||||
var self = this,
|
||||
postDelete = function () {
|
||||
self.teamEvents.trigger('teams:update', {
|
||||
action: 'delete',
|
||||
team: self.team
|
||||
});
|
||||
Backbone.history.navigate('topics/' + self.team.get('topic_id'), {trigger: true});
|
||||
TeamUtils.showMessage(
|
||||
interpolate(
|
||||
gettext('Team "%(team)s" successfully deleted.'),
|
||||
{team: self.team.get('name')},
|
||||
true
|
||||
),
|
||||
'success'
|
||||
);
|
||||
};
|
||||
this.team.destroy().then(postDelete).fail(function (response) {
|
||||
// In the 404 case, this team has already been
|
||||
// deleted by someone else. Since the team was
|
||||
// successfully deleted anyway, just show a
|
||||
// success message.
|
||||
if (response.status === 404) {
|
||||
postDelete();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -10,7 +10,7 @@
|
||||
'teams/js/views/team_utils',
|
||||
'text!teams/templates/team-membership-details.underscore',
|
||||
'text!teams/templates/team-country-language.underscore',
|
||||
'text!teams/templates/team-activity.underscore'
|
||||
'text!teams/templates/date.underscore'
|
||||
], function (
|
||||
$,
|
||||
Backbone,
|
||||
@@ -21,7 +21,7 @@
|
||||
TeamUtils,
|
||||
teamMembershipDetailsTemplate,
|
||||
teamCountryLanguageTemplate,
|
||||
teamActivityTemplate
|
||||
dateTemplate
|
||||
) {
|
||||
var TeamMembershipView, TeamCountryLanguageView, TeamActivityView, TeamCardView;
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
TeamActivityView = Backbone.View.extend({
|
||||
tagName: 'div',
|
||||
className: 'team-activity',
|
||||
template: _.template(teamActivityTemplate),
|
||||
template: _.template(dateTemplate),
|
||||
|
||||
initialize: function (options) {
|
||||
this.date = options.date;
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
'use strict';
|
||||
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'teams/js/views/team_utils',
|
||||
'text!teams/templates/team-profile-header-actions.underscore'],
|
||||
function (Backbone, _, gettext, TeamUtils, teamProfileHeaderActionsTemplate) {
|
||||
function (Backbone, $, _, gettext, TeamUtils, teamProfileHeaderActionsTemplate) {
|
||||
return Backbone.View.extend({
|
||||
|
||||
errorMessage: gettext("An error occurred. Try again."),
|
||||
@@ -56,8 +57,10 @@
|
||||
return view;
|
||||
},
|
||||
|
||||
joinTeam: function () {
|
||||
joinTeam: function (event) {
|
||||
var view = this;
|
||||
|
||||
event.preventDefault();
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: view.context.teamMembershipsUrl,
|
||||
@@ -117,7 +120,7 @@
|
||||
editTeam: function (event) {
|
||||
event.preventDefault();
|
||||
Backbone.history.navigate(
|
||||
'topics/' + this.topic.id + '/' + this.model.get('id') +'/edit-team',
|
||||
'teams/' + this.topic.id + '/' + this.model.get('id') +'/edit-team',
|
||||
{trigger: true}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* Team utility methods*/
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
define([
|
||||
], function () {
|
||||
define(["jquery", "underscore"
|
||||
], function ($, _) {
|
||||
return {
|
||||
|
||||
/**
|
||||
@@ -40,9 +40,16 @@
|
||||
);
|
||||
},
|
||||
|
||||
showMessage: function (message) {
|
||||
var messageElement = $('.teams-content .wrapper-msg');
|
||||
messageElement.removeClass('is-hidden');
|
||||
hideMessage: function () {
|
||||
$('#teams-message').addClass('.is-hidden');
|
||||
},
|
||||
|
||||
showMessage: function (message, type) {
|
||||
var messageElement = $('#teams-message');
|
||||
if (_.isUndefined(type)) {
|
||||
type = 'warning';
|
||||
}
|
||||
messageElement.removeClass('is-hidden').addClass(type);
|
||||
$('.teams-content .msg-content .copy').text(message);
|
||||
messageElement.focus();
|
||||
},
|
||||
@@ -50,12 +57,14 @@
|
||||
/**
|
||||
* Parse `data` and show user message. If parsing fails than show `genericErrorMessage`
|
||||
*/
|
||||
parseAndShowMessage: function (data, genericErrorMessage) {
|
||||
parseAndShowMessage: function (data, genericErrorMessage, type) {
|
||||
try {
|
||||
var errors = JSON.parse(data.responseText);
|
||||
this.showMessage(_.isUndefined(errors.user_message) ? genericErrorMessage : errors.user_message);
|
||||
this.showMessage(
|
||||
_.isUndefined(errors.user_message) ? genericErrorMessage : errors.user_message, type
|
||||
);
|
||||
} catch (error) {
|
||||
this.showMessage(genericErrorMessage);
|
||||
this.showMessage(genericErrorMessage, type);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
'use strict';
|
||||
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'common/js/components/views/search_field',
|
||||
@@ -19,13 +20,15 @@
|
||||
'teams/js/views/my_teams',
|
||||
'teams/js/views/topic_teams',
|
||||
'teams/js/views/edit_team',
|
||||
'teams/js/views/edit_team_members',
|
||||
'teams/js/views/team_profile_header_actions',
|
||||
'teams/js/views/team_utils',
|
||||
'teams/js/views/instructor_tools',
|
||||
'text!teams/templates/teams_tab.underscore'],
|
||||
function (Backbone, _, gettext, SearchFieldView, HeaderView, HeaderModel,
|
||||
function (Backbone, $, _, gettext, SearchFieldView, HeaderView, HeaderModel,
|
||||
TopicModel, TopicCollection, TeamModel, TeamCollection, TeamMembershipCollection, TeamAnalytics,
|
||||
TeamsTabbedView, TopicsView, TeamProfileView, MyTeamsView, TopicTeamsView, TeamEditView,
|
||||
TeamProfileHeaderActionsView, TeamUtils, teamsTemplate) {
|
||||
TeamMembersEditView, TeamProfileHeaderActionsView, TeamUtils, InstructorToolsView, teamsTemplate) {
|
||||
var TeamsHeaderModel = HeaderModel.extend({
|
||||
initialize: function () {
|
||||
_.extend(this.defaults, {nav_aria_label: gettext('teams')});
|
||||
@@ -37,12 +40,16 @@
|
||||
initialize: function (options) {
|
||||
this.header = options.header;
|
||||
this.main = options.main;
|
||||
this.instructorTools = options.instructorTools;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(_.template(teamsTemplate));
|
||||
this.$('p.error').hide();
|
||||
this.header.setElement(this.$('.teams-header')).render();
|
||||
if (this.instructorTools) {
|
||||
this.instructorTools.setElement(this.$('.teams-instructor-tools-bar')).render();
|
||||
}
|
||||
this.main.setElement(this.$('.page-content')).render();
|
||||
return this;
|
||||
}
|
||||
@@ -68,7 +75,6 @@
|
||||
['topics/:topic_id(/)', _.bind(this.browseTopic, this)],
|
||||
['topics/:topic_id/search(/)', _.bind(this.searchTeams, this)],
|
||||
['topics/:topic_id/create-team(/)', _.bind(this.newTeam, this)],
|
||||
['topics/:topic_id/:team_id/edit-team(/)', _.bind(this.editTeam, this)],
|
||||
['teams/:topic_id/:team_id(/)', _.bind(this.browseTeam, this)],
|
||||
[new RegExp('^(browse)\/?$'), _.bind(this.goToTab, this)],
|
||||
[new RegExp('^(my-teams)\/?$'), _.bind(this.goToTab, this)]
|
||||
@@ -76,6 +82,17 @@
|
||||
router.route.apply(router, route);
|
||||
});
|
||||
|
||||
if (this.canEditTeam()) {
|
||||
_.each([
|
||||
['teams/:topic_id/:team_id/edit-team(/)', _.bind(this.editTeam, this)],
|
||||
['teams/:topic_id/:team_id/edit-team/manage-members(/)',
|
||||
_.bind(this.editTeamMembers, this)
|
||||
]
|
||||
], function (route) {
|
||||
router.route.apply(router, route);
|
||||
});
|
||||
}
|
||||
|
||||
// Create an event queue to track team changes
|
||||
this.teamEvents = _.clone(Backbone.Events);
|
||||
|
||||
@@ -169,7 +186,7 @@
|
||||
|
||||
render: function() {
|
||||
this.mainView.setElement(this.$el).render();
|
||||
this.hideWarning();
|
||||
TeamUtils.hideMessage();
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -240,26 +257,54 @@
|
||||
editTeam: function (topicID, teamID) {
|
||||
var self = this,
|
||||
editViewWithHeader;
|
||||
this.getTopic(topicID).done(function (topic) {
|
||||
self.getTeam(teamID, false).done(function(team) {
|
||||
var view = new TeamEditView({
|
||||
action: 'edit',
|
||||
teamEvents: self.teamEvents,
|
||||
context: self.context,
|
||||
topic: topic,
|
||||
model: team
|
||||
});
|
||||
editViewWithHeader = self.createViewWithHeader({
|
||||
title: gettext("Edit Team"),
|
||||
description: gettext("If you make significant changes, make sure you notify members of the team before making these changes."),
|
||||
$.when(this.getTopic(topicID), this.getTeam(teamID, false)).done(function(topic, team) {
|
||||
var view = new TeamEditView({
|
||||
action: 'edit',
|
||||
teamEvents: self.teamEvents,
|
||||
context: self.context,
|
||||
topic: topic,
|
||||
model: team
|
||||
});
|
||||
var instructorToolsView = new InstructorToolsView({
|
||||
team: team,
|
||||
teamEvents: self.teamEvents
|
||||
});
|
||||
editViewWithHeader = self.createViewWithHeader({
|
||||
title: gettext("Edit Team"),
|
||||
description: gettext("If you make significant changes, make sure you notify members of the team before making these changes."),
|
||||
mainView: view,
|
||||
topic: topic,
|
||||
team: team,
|
||||
instructorTools: instructorToolsView
|
||||
});
|
||||
self.mainView = editViewWithHeader;
|
||||
self.render();
|
||||
TeamAnalytics.emitPageViewed('edit-team', topicID, teamID);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* The backbone router entry for editing team members, using topic and team IDs.
|
||||
*/
|
||||
editTeamMembers: function (topicID, teamID) {
|
||||
var self = this;
|
||||
$.when(this.getTopic(topicID), this.getTeam(teamID, true)).done(function(topic, team) {
|
||||
var view = new TeamMembersEditView({
|
||||
teamEvents: self.teamEvents,
|
||||
context: self.context,
|
||||
model: team
|
||||
});
|
||||
self.mainView = self.createViewWithHeader({
|
||||
mainView: view,
|
||||
title: gettext("Membership"),
|
||||
description: gettext("You can remove members from this team, especially if they have not participated in the team's activity."),
|
||||
topic: topic,
|
||||
team: team
|
||||
});
|
||||
self.mainView = editViewWithHeader;
|
||||
self.render();
|
||||
TeamAnalytics.emitPageViewed('edit-team', topicID, teamID);
|
||||
});
|
||||
}
|
||||
);
|
||||
self.render();
|
||||
TeamAnalytics.emitPageViewed('edit-team-members', topicID, teamID);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -365,38 +410,41 @@
|
||||
getBrowseTeamView: function (topicID, teamID) {
|
||||
var self = this,
|
||||
deferred = $.Deferred();
|
||||
self.getTopic(topicID).done(function(topic) {
|
||||
self.getTeam(teamID, true).done(function(team) {
|
||||
var view = new TeamProfileView({
|
||||
teamEvents: self.teamEvents,
|
||||
router: self.router,
|
||||
context: self.context,
|
||||
model: team,
|
||||
setFocusToHeaderFunc: self.setFocusToHeader
|
||||
});
|
||||
|
||||
var TeamProfileActionsView = new TeamProfileHeaderActionsView({
|
||||
teamEvents: self.teamEvents,
|
||||
context: self.context,
|
||||
model: team,
|
||||
topic: topic,
|
||||
showEditButton: self.context.userInfo.privileged || self.context.userInfo.staff
|
||||
});
|
||||
deferred.resolve(
|
||||
self.createViewWithHeader(
|
||||
{
|
||||
mainView: view,
|
||||
subject: team,
|
||||
topic: topic,
|
||||
headerActionsView: TeamProfileActionsView
|
||||
}
|
||||
)
|
||||
);
|
||||
$.when(this.getTopic(topicID), this.getTeam(teamID, true)).done(function(topic, team) {
|
||||
var view = new TeamProfileView({
|
||||
teamEvents: self.teamEvents,
|
||||
router: self.router,
|
||||
context: self.context,
|
||||
model: team,
|
||||
setFocusToHeaderFunc: self.setFocusToHeader
|
||||
});
|
||||
|
||||
var TeamProfileActionsView = new TeamProfileHeaderActionsView({
|
||||
teamEvents: self.teamEvents,
|
||||
context: self.context,
|
||||
model: team,
|
||||
topic: topic,
|
||||
showEditButton: self.canEditTeam()
|
||||
});
|
||||
deferred.resolve(
|
||||
self.createViewWithHeader(
|
||||
{
|
||||
mainView: view,
|
||||
subject: team,
|
||||
topic: topic,
|
||||
headerActionsView: TeamProfileActionsView
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
return deferred.promise();
|
||||
},
|
||||
|
||||
canEditTeam: function () {
|
||||
return this.context.userInfo.privileged || this.context.userInfo.staff;
|
||||
},
|
||||
|
||||
createBreadcrumbs: function(topic, team) {
|
||||
var breadcrumbs = [{
|
||||
title: gettext('All Topics'),
|
||||
@@ -446,7 +494,8 @@
|
||||
}
|
||||
}
|
||||
}),
|
||||
main: options.mainView
|
||||
main: options.mainView,
|
||||
instructorTools: options.instructorTools
|
||||
});
|
||||
},
|
||||
|
||||
@@ -566,18 +615,7 @@
|
||||
*/
|
||||
notFoundError: function (message) {
|
||||
this.router.navigate('my-teams', {trigger: true});
|
||||
this.showWarning(message);
|
||||
},
|
||||
|
||||
showWarning: function (message) {
|
||||
var warningEl = this.$('.warning');
|
||||
warningEl.find('.copy').html('<p>' + message + '</p');
|
||||
warningEl.toggleClass('is-hidden', false);
|
||||
warningEl.focus();
|
||||
},
|
||||
|
||||
hideWarning: function () {
|
||||
this.$('.warning').toggleClass('is-hidden', true);
|
||||
TeamUtils.showMessage(message);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
define([
|
||||
'gettext',
|
||||
'teams/js/views/topic_card',
|
||||
'teams/js/views/team_utils',
|
||||
'common/js/components/views/paging_header',
|
||||
'common/js/components/views/paginated_view'
|
||||
], function (gettext, TopicCardView, PagingHeader, PaginatedView) {
|
||||
], function (gettext, TopicCardView, TeamUtils, PagingHeader, PaginatedView) {
|
||||
var TopicsView = PaginatedView.extend({
|
||||
type: 'topics',
|
||||
|
||||
@@ -35,6 +36,7 @@
|
||||
this.collection.refresh()
|
||||
.done(function() {
|
||||
PaginatedView.prototype.render.call(self);
|
||||
TeamUtils.hideMessage();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<li class="team-member">
|
||||
<a class="member-profile" href="<%= memberProfileUrl %>">
|
||||
<img class="image-url" src="<%= imageUrl %>" alt="<%= username %>'s profile page" />
|
||||
</a>
|
||||
<div class="member-info-container">
|
||||
<span class="primary"><%= username %></span>
|
||||
<div class="secondary">
|
||||
<span id="date-joined"><%= dateJoined %></span>
|
||||
<span> | </span>
|
||||
<span id="last-active"><%= lastActive %></span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="action-remove-member" data-username="<%= username %>">
|
||||
<%- gettext("Remove") %><span class="sr"> <%= username %></span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="wrapper-msg">
|
||||
<h3 class="left-floater">
|
||||
<%- gettext("Instructor tools") %>
|
||||
</h3>
|
||||
<span class="right-floater">
|
||||
<button class="action-delete">
|
||||
<%- gettext("Delete Team") %>
|
||||
</button>
|
||||
<button class="action-edit-members">
|
||||
<%- gettext("Edit Membership") %>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="wrapper-msg is-incontext urgency-low warning is-hidden" tabindex="-1">
|
||||
<div id="teams-message" class="wrapper-msg is-incontext urgency-low is-hidden" tabindex="-1">
|
||||
<div class="msg">
|
||||
<div class="msg-content">
|
||||
<div class="copy">
|
||||
@@ -7,6 +7,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="teams-header"></div>
|
||||
<div class="teams-instructor-tools-bar"></div>
|
||||
<div class="teams-main">
|
||||
<div class="page-content"></div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ from util.testing import EventTestMixin
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from .factories import CourseTeamFactory, LAST_ACTIVITY_AT
|
||||
from ..models import CourseTeamMembership
|
||||
from ..search_indexes import CourseTeamIndexer, CourseTeam, course_team_post_save_callback
|
||||
|
||||
from django_comment_common.models import Role, FORUM_ROLE_COMMUNITY_TA
|
||||
@@ -339,6 +340,10 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase):
|
||||
"""Gets detailed team information for team_id. Verifies expected_status."""
|
||||
return self.make_call(reverse('teams_detail', args=[team_id]), expected_status, 'get', data, **kwargs)
|
||||
|
||||
def delete_team(self, team_id, expected_status, **kwargs):
|
||||
"""Delete the given team. Verifies expected_status."""
|
||||
return self.make_call(reverse('teams_detail', args=[team_id]), expected_status, 'delete', **kwargs)
|
||||
|
||||
def patch_team_detail(self, team_id, expected_status, data=None, **kwargs):
|
||||
"""Patches the team with team_id using data. Verifies expected_status."""
|
||||
return self.make_call(
|
||||
@@ -384,12 +389,8 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase):
|
||||
|
||||
def delete_membership(self, team_id, username, expected_status=200, **kwargs):
|
||||
"""Deletes an individual membership record. Verifies expected_status."""
|
||||
return self.make_call(
|
||||
reverse('team_membership_detail', args=[team_id, username]),
|
||||
expected_status,
|
||||
'delete',
|
||||
**kwargs
|
||||
)
|
||||
url = reverse('team_membership_detail', args=[team_id, username]) + '?admin=true'
|
||||
return self.make_call(url, expected_status, 'delete', **kwargs)
|
||||
|
||||
def verify_expanded_public_user(self, user):
|
||||
"""Verifies that fields exist on the returned user json indicating that it is expanded."""
|
||||
@@ -437,8 +438,9 @@ class TestListTeamsAPI(EventTestMixin, TeamAPITestCase):
|
||||
def verify_names(self, data, status, names=None, **kwargs):
|
||||
"""Gets a team listing with data as query params, verifies status, and then verifies team names if specified."""
|
||||
teams = self.get_teams_list(data=data, expected_status=status, **kwargs)
|
||||
if names:
|
||||
self.assertEqual(names, [team['name'] for team in teams['results']])
|
||||
if names is not None and 200 <= status < 300:
|
||||
results = teams['results']
|
||||
self.assertEqual(names, [team['name'] for team in results])
|
||||
|
||||
def test_filter_invalid_course_id(self):
|
||||
self.verify_names({'course_id': 'no_such_course'}, 400)
|
||||
@@ -562,6 +564,26 @@ class TestListTeamsAPI(EventTestMixin, TeamAPITestCase):
|
||||
user='student_enrolled_public_profile'
|
||||
)
|
||||
|
||||
def test_delete_removed_from_search(self):
|
||||
team = CourseTeamFactory.create(
|
||||
name=u'zoinks',
|
||||
course_id=self.test_course_1.id,
|
||||
topic_id='topic_0'
|
||||
)
|
||||
self.verify_names(
|
||||
{'course_id': self.test_course_1.id, 'text_search': 'zoinks'},
|
||||
200,
|
||||
[team.name],
|
||||
user='staff'
|
||||
)
|
||||
team.delete()
|
||||
self.verify_names(
|
||||
{'course_id': self.test_course_1.id, 'text_search': 'zoinks'},
|
||||
200,
|
||||
[],
|
||||
user='staff'
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCreateTeamAPI(EventTestMixin, TeamAPITestCase):
|
||||
@@ -776,6 +798,60 @@ class TestDetailTeamAPI(TeamAPITestCase):
|
||||
self.verify_expanded_public_user(result['membership'][0]['user'])
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestDeleteTeamAPI(EventTestMixin, TeamAPITestCase):
|
||||
"""Test cases for the team delete endpoint."""
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(TestDeleteTeamAPI, self).setUp('teams.views.tracker')
|
||||
|
||||
@ddt.data(
|
||||
(None, 401),
|
||||
('student_inactive', 401),
|
||||
('student_unenrolled', 403),
|
||||
('student_enrolled', 403),
|
||||
('staff', 204),
|
||||
('course_staff', 204),
|
||||
('community_ta', 204)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_access(self, user, status):
|
||||
self.delete_team(self.solar_team.team_id, status, user=user)
|
||||
if status == 204:
|
||||
self.assert_event_emitted(
|
||||
'edx.team.deleted',
|
||||
team_id=self.solar_team.team_id,
|
||||
course_id=unicode(self.test_course_1.id)
|
||||
)
|
||||
self.assert_event_emitted(
|
||||
'edx.team.learner_removed',
|
||||
team_id=self.solar_team.team_id,
|
||||
course_id=unicode(self.test_course_1.id),
|
||||
remove_method='team_deleted',
|
||||
user_id=self.users['student_enrolled'].id
|
||||
)
|
||||
|
||||
def test_does_not_exist(self):
|
||||
self.delete_team('nonexistent', 404)
|
||||
|
||||
def test_memberships_deleted(self):
|
||||
self.assertEqual(CourseTeamMembership.objects.filter(team=self.solar_team).count(), 1)
|
||||
self.delete_team(self.solar_team.team_id, 204, user='staff')
|
||||
self.assert_event_emitted(
|
||||
'edx.team.deleted',
|
||||
team_id=self.solar_team.team_id,
|
||||
course_id=unicode(self.test_course_1.id)
|
||||
)
|
||||
self.assert_event_emitted(
|
||||
'edx.team.learner_removed',
|
||||
team_id=self.solar_team.team_id,
|
||||
course_id=unicode(self.test_course_1.id),
|
||||
remove_method='team_deleted',
|
||||
user_id=self.users['student_enrolled'].id
|
||||
)
|
||||
self.assertEqual(CourseTeamMembership.objects.filter(team=self.solar_team).count(), 0)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestUpdateTeamAPI(EventTestMixin, TeamAPITestCase):
|
||||
"""Test cases for the team update endpoint."""
|
||||
@@ -1299,17 +1375,32 @@ class TestDeleteMembershipAPI(EventTestMixin, TeamAPITestCase):
|
||||
)
|
||||
|
||||
if status == 204:
|
||||
remove_method = 'self_removal' if user == 'student_enrolled' else 'removed_by_admin'
|
||||
self.assert_event_emitted(
|
||||
'edx.team.learner_removed',
|
||||
team_id=self.solar_team.team_id,
|
||||
course_id=unicode(self.solar_team.course_id),
|
||||
user_id=self.users['student_enrolled'].id,
|
||||
remove_method=remove_method
|
||||
remove_method='removed_by_admin'
|
||||
)
|
||||
else:
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
def test_leave_team(self):
|
||||
"""
|
||||
The key difference between this test and test_access above is that
|
||||
removal via "Edit Membership" and "Leave Team" emit different events
|
||||
despite hitting the same API endpoint, due to the 'admin' query string.
|
||||
"""
|
||||
url = reverse('team_membership_detail', args=[self.solar_team.team_id, self.users['student_enrolled'].username])
|
||||
self.make_call(url, 204, 'delete', user='student_enrolled')
|
||||
self.assert_event_emitted(
|
||||
'edx.team.learner_removed',
|
||||
team_id=self.solar_team.team_id,
|
||||
course_id=unicode(self.solar_team.course_id),
|
||||
user_id=self.users['student_enrolled'].id,
|
||||
remove_method='self_removal'
|
||||
)
|
||||
|
||||
def test_bad_team(self):
|
||||
self.delete_membership('no_such_team', self.users['student_enrolled'].username, 404)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""HTTP endpoints for the Teams API."""
|
||||
|
||||
from django.shortcuts import render_to_response
|
||||
import logging
|
||||
|
||||
from django.shortcuts import get_object_or_404, render_to_response
|
||||
from django.http import Http404
|
||||
from django.conf import settings
|
||||
from django.core.paginator import Paginator
|
||||
@@ -61,6 +63,8 @@ TEAM_MEMBERSHIPS_PER_PAGE = 2
|
||||
TOPICS_PER_PAGE = 12
|
||||
MAXIMUM_SEARCH_SIZE = 100000
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseTeam)
|
||||
def team_post_save_callback(sender, instance, **kwargs): # pylint: disable=unused-argument
|
||||
@@ -504,7 +508,7 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView):
|
||||
"""
|
||||
**Use Cases**
|
||||
|
||||
Get or update a course team's information. Updates are supported
|
||||
Get, update, or delete a course team's information. Updates are supported
|
||||
only through merge patch.
|
||||
|
||||
**Example Requests**:
|
||||
@@ -513,6 +517,8 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView):
|
||||
|
||||
PATCH /api/team/v0/teams/{team_id} "application/merge-patch+json"
|
||||
|
||||
DELETE /api/team/v0/teams/{team_id}
|
||||
|
||||
**Query Parameters for GET**
|
||||
|
||||
* expand: Comma separated list of types for which to return
|
||||
@@ -577,6 +583,20 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView):
|
||||
If the update could not be completed due to validation errors, this
|
||||
method returns a 400 error with all error messages in the
|
||||
"field_errors" field of the returned JSON.
|
||||
|
||||
**Response Values for DELETE**
|
||||
|
||||
Only staff can delete teams. When a team is deleted, all
|
||||
team memberships associated with that team are also
|
||||
deleted. Returns 204 on successful deletion.
|
||||
|
||||
If the user is anonymous or inactive, a 401 is returned.
|
||||
|
||||
If the user is not course or global staff and does not
|
||||
have discussion privileges, a 403 is returned.
|
||||
|
||||
If the user is logged in and the team does not exist, a 404 is returned.
|
||||
|
||||
"""
|
||||
authentication_classes = (OAuth2Authentication, SessionAuthentication)
|
||||
permission_classes = (permissions.IsAuthenticated, IsStaffOrPrivilegedOrReadOnly, IsEnrolledOrIsStaff,)
|
||||
@@ -588,6 +608,29 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView):
|
||||
"""Returns the queryset used to access the given team."""
|
||||
return CourseTeam.objects.all()
|
||||
|
||||
def delete(self, request, team_id):
|
||||
"""DELETE /api/team/v0/teams/{team_id}"""
|
||||
team = get_object_or_404(CourseTeam, team_id=team_id)
|
||||
self.check_object_permissions(request, team)
|
||||
# Note: list() forces the queryset to be evualuated before delete()
|
||||
memberships = list(CourseTeamMembership.get_memberships(team_id=team_id))
|
||||
|
||||
# Note: also deletes all team memberships associated with this team
|
||||
team.delete()
|
||||
log.info('user %d deleted team %s', request.user.id, team_id)
|
||||
tracker.emit('edx.team.deleted', {
|
||||
'team_id': team_id,
|
||||
'course_id': unicode(team.course_id),
|
||||
})
|
||||
for member in memberships:
|
||||
tracker.emit('edx.team.learner_removed', {
|
||||
'team_id': team_id,
|
||||
'course_id': unicode(team.course_id),
|
||||
'remove_method': 'team_deleted',
|
||||
'user_id': member.user_id
|
||||
})
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class TopicListView(GenericAPIView):
|
||||
"""
|
||||
@@ -1155,6 +1198,9 @@ class MembershipDetailView(ExpandableFieldViewMixin, GenericAPIView):
|
||||
team = self.get_team(team_id)
|
||||
if has_team_api_access(request.user, team.course_id, access_username=username):
|
||||
membership = self.get_membership(username, team)
|
||||
removal_method = 'self_removal'
|
||||
if 'admin' in request.QUERY_PARAMS:
|
||||
removal_method = 'removed_by_admin'
|
||||
membership.delete()
|
||||
tracker.emit(
|
||||
'edx.team.learner_removed',
|
||||
@@ -1162,7 +1208,7 @@ class MembershipDetailView(ExpandableFieldViewMixin, GenericAPIView):
|
||||
'team_id': team.team_id,
|
||||
'course_id': unicode(team.course_id),
|
||||
'user_id': membership.user.id,
|
||||
'remove_method': 'self_removal' if membership.user == request.user else 'removed_by_admin'
|
||||
'remove_method': removal_method
|
||||
}
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -704,6 +704,8 @@
|
||||
'lms/include/teams/js/spec/collections/topic_collection_spec.js',
|
||||
'lms/include/teams/js/spec/teams_tab_factory_spec.js',
|
||||
'lms/include/teams/js/spec/views/edit_team_spec.js',
|
||||
'lms/include/teams/js/spec/views/edit_team_members_spec.js',
|
||||
'lms/include/teams/js/spec/views/instructor_tools_spec.js',
|
||||
'lms/include/teams/js/spec/views/my_teams_spec.js',
|
||||
'lms/include/teams/js/spec/views/team_card_spec.js',
|
||||
'lms/include/teams/js/spec/views/team_discussion_spec.js',
|
||||
|
||||
@@ -232,3 +232,85 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//efischer; scratch space for teams instructor tools
|
||||
.view-teams {
|
||||
.wrapper-msg {
|
||||
@include clearfix();
|
||||
max-width: grid-width(12);
|
||||
margin: 0 auto;
|
||||
border-top: 3px solid $orange;
|
||||
|
||||
.left-floater {
|
||||
@include float(left);
|
||||
text-transform: uppercase;
|
||||
font-weight: $font-semibold;
|
||||
color: $white;
|
||||
line-height: $body-line-height;
|
||||
}
|
||||
|
||||
.right-floater {
|
||||
@include float(right);
|
||||
line-height: $body-line-height;
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: $white;
|
||||
box-shadow: 0 0 0 0;
|
||||
font-weight: $font-regular;
|
||||
text-shadow: 0 0;
|
||||
|
||||
&:hover {
|
||||
color: $orange;
|
||||
background: transparent;
|
||||
border: 1px solid $orange;
|
||||
box-shadow: 0 0 0 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-members {
|
||||
@extend %ui-no-list;
|
||||
|
||||
.team-member {
|
||||
line-height: $body-line-height;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.member-info-container {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
@include margin-left($baseline/2);
|
||||
|
||||
.primary {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: $lighter-base-font-color;
|
||||
font-size: 80%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.member-profile {
|
||||
img {
|
||||
border: 1px solid $gray;
|
||||
}
|
||||
}
|
||||
|
||||
.action-remove-member {
|
||||
color: $blue;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
font: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user