Delete Team functionality and tests
TNL-3164 TNL-3163
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
|
||||
|
||||
@@ -39,7 +39,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.
|
||||
"""
|
||||
@@ -88,7 +105,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"
|
||||
@@ -169,7 +186,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.
|
||||
@@ -207,6 +224,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)
|
||||
@@ -278,9 +300,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):
|
||||
"""
|
||||
@@ -289,7 +311,7 @@ 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'])
|
||||
|
||||
@@ -309,11 +331,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"""
|
||||
@@ -329,8 +346,13 @@ 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):
|
||||
|
||||
class TeamPage(CoursePage, PaginatedUIMixin, BreadcrumbsMixin):
|
||||
"""
|
||||
The page for a specific Team within the Teams tab
|
||||
"""
|
||||
@@ -479,11 +501,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,15 @@ 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,
|
||||
TeamPage
|
||||
)
|
||||
from ...pages.common.utils import confirm_prompt
|
||||
|
||||
|
||||
TOPICS_PER_PAGE = 12
|
||||
@@ -384,7 +392,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 +401,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 +843,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 +881,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 +904,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 +949,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 +976,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 +998,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 +1010,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 +1056,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 +1088,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 +1117,96 @@ 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)
|
||||
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`."""
|
||||
self.team_page.click_edit_team_button()
|
||||
self.team_management_page.wait_for_page()
|
||||
self.team_management_page.delete_team_button.click()
|
||||
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 +1220,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 +1309,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 +1342,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 +1394,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()
|
||||
|
||||
|
||||
@@ -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,84 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 () {
|
||||
|
||||
@@ -29,13 +29,15 @@ 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: '',
|
||||
url: 'api/team/v0/teams/' + id
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -124,6 +126,10 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
var triggerTeamEvent = function (action) {
|
||||
teamEvents.trigger('teams:update', {action: action});
|
||||
};
|
||||
|
||||
createMockPostResponse = function(options) {
|
||||
return _.extend(
|
||||
{
|
||||
@@ -327,6 +333,7 @@ define([
|
||||
createMockThreadResponse: createMockThreadResponse,
|
||||
createMockTopicData: createMockTopicData,
|
||||
createMockTopicCollection: createMockTopicCollection,
|
||||
triggerTeamEvent: triggerTeamEvent,
|
||||
verifyCards: verifyCards
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
'underscore',
|
||||
'gettext',
|
||||
'teams/js/views/team_utils',
|
||||
'common/js/components/utils/view_utils',
|
||||
'text!teams/templates/instructor-tools.underscore'],
|
||||
function (Backbone, _, gettext, TeamUtils, instructorToolbarTemplate) {
|
||||
function (Backbone, _, gettext, TeamUtils, ViewUtils, instructorToolbarTemplate) {
|
||||
return Backbone.View.extend({
|
||||
|
||||
events: {
|
||||
@@ -16,6 +17,8 @@
|
||||
|
||||
initialize: function(options) {
|
||||
this.template = _.template(instructorToolbarTemplate);
|
||||
this.team = options.team;
|
||||
this.teamEvents = options.teamEvents;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
@@ -25,14 +28,46 @@
|
||||
|
||||
deleteTeam: function (event) {
|
||||
event.preventDefault();
|
||||
alert("You clicked the button!");
|
||||
//placeholder; will route to delete team page
|
||||
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();
|
||||
alert("You clicked the button!");
|
||||
//placeholder; will route to remove team member page
|
||||
},
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
'teams/js/views/topic_teams',
|
||||
'teams/js/views/edit_team',
|
||||
'teams/js/views/team_profile_header_actions',
|
||||
'teams/js/views/team_utils',
|
||||
'teams/js/views/instructor_tools',
|
||||
'teams/js/views/team_utils',
|
||||
'text!teams/templates/teams_tab.underscore'],
|
||||
function (Backbone, _, gettext, SearchFieldView, HeaderView, HeaderModel,
|
||||
TopicModel, TopicCollection, TeamModel, TeamCollection, TeamMembershipCollection, TeamAnalytics,
|
||||
@@ -45,8 +45,9 @@
|
||||
this.$el.html(_.template(teamsTemplate));
|
||||
this.$('p.error').hide();
|
||||
this.header.setElement(this.$('.teams-header')).render();
|
||||
if (this.instructorTools)
|
||||
if (this.instructorTools) {
|
||||
this.instructorTools.setElement(this.$('.teams-instructor-tools-bar')).render();
|
||||
}
|
||||
this.main.setElement(this.$('.page-content')).render();
|
||||
return this;
|
||||
}
|
||||
@@ -173,7 +174,7 @@
|
||||
|
||||
render: function() {
|
||||
this.mainView.setElement(this.$el).render();
|
||||
this.hideWarning();
|
||||
TeamUtils.hideMessage();
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -253,7 +254,10 @@
|
||||
topic: topic,
|
||||
model: team
|
||||
});
|
||||
var instructorToolsView = new InstructorToolsView();
|
||||
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."),
|
||||
@@ -570,18 +574,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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(
|
||||
@@ -437,8 +442,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 +568,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 +802,32 @@ class TestDetailTeamAPI(TeamAPITestCase):
|
||||
self.verify_expanded_public_user(result['membership'][0]['user'])
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestDeleteTeamAPI(TeamAPITestCase):
|
||||
"""Test cases for the team delete endpoint."""
|
||||
|
||||
@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)
|
||||
|
||||
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.assertEqual(CourseTeamMembership.objects.filter(team=self.solar_team).count(), 0)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestUpdateTeamAPI(EventTestMixin, TeamAPITestCase):
|
||||
"""Test cases for the team update endpoint."""
|
||||
|
||||
@@ -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,15 @@ 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: also deletes all team memberships associated with this team
|
||||
team.delete()
|
||||
log.info('user %d deleted team %s', request.user.id, team_id)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class TopicListView(GenericAPIView):
|
||||
"""
|
||||
|
||||
@@ -702,6 +702,7 @@
|
||||
'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/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',
|
||||
|
||||
Reference in New Issue
Block a user