Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
0c3be8e1
Commit
0c3be8e1
authored
Aug 31, 2015
by
Peter Fogg
Committed by
Eric Fischer
Sep 10, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Delete Team functionality and tests
TNL-3164 TNL-3163
parent
8942b31d
Hide whitespace changes
Inline
Side-by-side
Showing
28 changed files
with
545 additions
and
155 deletions
+545
-155
common/test/acceptance/pages/common/utils.py
+62
-0
common/test/acceptance/pages/lms/teams.py
+35
-18
common/test/acceptance/pages/studio/component_editor.py
+1
-1
common/test/acceptance/pages/studio/container.py
+3
-1
common/test/acceptance/pages/studio/import_export.py
+2
-1
common/test/acceptance/pages/studio/library.py
+3
-1
common/test/acceptance/pages/studio/overview.py
+3
-1
common/test/acceptance/pages/studio/settings_group_configurations.py
+1
-1
common/test/acceptance/pages/studio/textbooks.py
+1
-1
common/test/acceptance/pages/studio/utils.py
+1
-56
common/test/acceptance/pages/studio/video/video.py
+1
-1
common/test/acceptance/tests/lms/test_lms_courseware_search.py
+2
-1
common/test/acceptance/tests/lms/test_lms_dashboard_search.py
+2
-1
common/test/acceptance/tests/lms/test_teams.py
+136
-31
lms/djangoapps/teams/search_indexes.py
+36
-5
lms/djangoapps/teams/static/teams/js/collections/topic.js
+1
-1
lms/djangoapps/teams/static/teams/js/spec/collections/topic_collection_spec.js
+9
-0
lms/djangoapps/teams/static/teams/js/spec/views/instructor_tools_spec.js
+84
-0
lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js
+1
-1
lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js
+9
-2
lms/djangoapps/teams/static/teams/js/views/instructor_tools.js
+38
-3
lms/djangoapps/teams/static/teams/js/views/team_utils.js
+15
-6
lms/djangoapps/teams/static/teams/js/views/teams_tab.js
+9
-16
lms/djangoapps/teams/static/teams/js/views/topics.js
+3
-1
lms/djangoapps/teams/static/teams/templates/teams_tab.underscore
+1
-1
lms/djangoapps/teams/tests/test_views.py
+54
-2
lms/djangoapps/teams/views.py
+31
-2
lms/static/js/spec/main.js
+1
-0
No files found.
common/test/acceptance/pages/common/utils.py
0 → 100644
View file @
0c3be8e1
"""
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
)
common/test/acceptance/pages/lms/teams.py
View file @
0c3be8e1
...
...
@@ -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
,
Field
sMixin
):
class
TeamManagementPage
(
CoursePage
,
FieldsMixin
,
Breadcrumb
sMixin
):
"""
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
(
CreateOrEditTeam
Page
,
self
)
.
__init__
(
browser
,
course_id
)
super
(
TeamManagement
Page
,
self
)
.
__init__
(
browser
,
course_id
)
self
.
topic
=
topic
self
.
url_path
=
"teams/#topics/{topic_id}/create-team"
.
format
(
topic_id
=
self
.
topic
[
'id'
])
...
...
@@ -310,11 +332,6 @@ class CreateOrEditTeamPage(CoursePage, FieldsMixin):
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"""
return
self
.
q
(
css
=
'.create-team.wrapper-msg .copy'
)[
0
]
.
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 """
...
...
common/test/acceptance/pages/studio/component_editor.py
View file @
0c3be8e1
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
...
...
common/test/acceptance/pages/studio/container.py
View file @
0c3be8e1
...
...
@@ -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
):
...
...
common/test/acceptance/pages/studio/import_export.py
View file @
0c3be8e1
...
...
@@ -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
...
...
common/test/acceptance/pages/studio/library.py
View file @
0c3be8e1
...
...
@@ -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
...
...
common/test/acceptance/pages/studio/overview.py
View file @
0c3be8e1
...
...
@@ -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
):
...
...
common/test/acceptance/pages/studio/settings_group_configurations.py
View file @
0c3be8e1
...
...
@@ -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
):
...
...
common/test/acceptance/pages/studio/textbooks.py
View file @
0c3be8e1
...
...
@@ -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
):
...
...
common/test/acceptance/pages/studio/utils.py
View file @
0c3be8e1
...
...
@@ -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.
...
...
common/test/acceptance/pages/studio/video/video.py
View file @
0c3be8e1
...
...
@@ -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
=
{
...
...
common/test/acceptance/tests/lms/test_lms_courseware_search.py
View file @
0c3be8e1
...
...
@@ -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
...
...
common/test/acceptance/tests/lms/test_lms_dashboard_search.py
View file @
0c3be8e1
...
...
@@ -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
...
...
common/test/acceptance/tests/lms/test_teams.py
View file @
0c3be8e1
...
...
@@ -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
=
CreateOrEditTeam
Page
(
self
.
browser
,
self
.
course_id
,
topic
)
create_team_page
=
TeamManagement
Page
(
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'
))
...
...
@@ -1102,6 +1118,96 @@ class CreateTeamTest(TeamFormActions):
@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
):
"""
Tests for editing the team.
...
...
@@ -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
()
...
...
lms/djangoapps/teams/search_indexes.py
View file @
0c3be8e1
...
...
@@ -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'
])
lms/djangoapps/teams/static/teams/js/collections/topic.js
View file @
0c3be8e1
...
...
@@ -25,7 +25,7 @@
},
onUpdate
:
function
(
event
)
{
if
(
event
.
action
===
'create'
)
{
if
(
_
.
contains
([
'create'
,
'delete'
],
event
.
action
)
)
{
this
.
isStale
=
true
;
}
},
...
...
lms/djangoapps/teams/static/teams/js/spec/collections/topic_collection_spec.js
View file @
0c3be8e1
...
...
@@ -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
);
});
});
});
lms/djangoapps/teams/static/teams/js/spec/views/instructor_tools_spec.js
0 → 100644
View file @
0c3be8e1
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
);
});
});
});
lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js
View file @
0c3be8e1
...
...
@@ -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
.
$
(
'.w
arnin
g'
)).
toHaveClass
(
'is-hidden'
);
expect
(
teamsTabView
.
$
(
'.w
rapper-ms
g'
)).
toHaveClass
(
'is-hidden'
);
});
it
(
'displays and focuses an error message when trying to navigate to a nonexistent page'
,
function
()
{
...
...
lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js
View file @
0c3be8e1
...
...
@@ -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
};
});
lms/djangoapps/teams/static/teams/js/views/instructor_tools.js
View file @
0c3be8e1
...
...
@@ -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
();
}
});
}
});
});
...
...
lms/djangoapps/teams/static/teams/js/views/team_utils.js
View file @
0c3be8e1
...
...
@@ -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
);
}
}
};
...
...
lms/djangoapps/teams/static/teams/js/views/teams_tab.js
View file @
0c3be8e1
...
...
@@ -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
);
},
/**
...
...
lms/djangoapps/teams/static/teams/js/views/topics.js
View file @
0c3be8e1
...
...
@@ -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
;
}
...
...
lms/djangoapps/teams/static/teams/templates/teams_tab.underscore
View file @
0c3be8e1
<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">
...
...
lms/djangoapps/teams/tests/test_views.py
View file @
0c3be8e1
...
...
@@ -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
):
...
...
@@ -777,6 +803,32 @@ class TestDetailTeamAPI(TeamAPITestCase):
@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."""
...
...
lms/djangoapps/teams/views.py
View file @
0c3be8e1
"""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 upda
te a course team's information. Updates are supported
Get
, update, or dele
te 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
):
"""
...
...
lms/static/js/spec/main.js
View file @
0c3be8e1
...
...
@@ -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'
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment