From 69450fce479e06d8d88db6930e45cfbef0e4c174 Mon Sep 17 00:00:00 2001 From: muhammad-ammar Date: Thu, 16 Jul 2015 05:02:02 +0500 Subject: [PATCH] create a new team TNL-1899 --- common/test/acceptance/pages/lms/fields.py | 16 ++ common/test/acceptance/pages/lms/teams.py | 77 +++++++ .../test/acceptance/tests/lms/test_teams.py | 196 +++++++++++++++++- .../teams/static/teams/js/models/team.js | 2 +- .../static/teams/js/spec/edit_team_spec.js | 170 +++++++++++++++ .../static/teams/js/spec/team_actions_spec.js | 40 ++++ .../teams/static/teams/js/spec/teams_spec.js | 7 +- .../teams/static/teams/js/views/edit_team.js | 192 +++++++++++++++++ .../static/teams/js/views/team_actions.js | 52 +++++ .../teams/static/teams/js/views/teams.js | 19 +- .../teams/static/teams/js/views/teams_tab.js | 50 ++++- .../teams/templates/edit-team.underscore | 47 +++++ .../teams/templates/team-actions.underscore | 4 + .../teams/templates/teams/teams.html | 4 +- lms/djangoapps/teams/views.py | 5 +- lms/static/js/spec/main.js | 2 + .../account_settings_fields_spec.js | 6 +- .../learner_profile_view_spec.js | 3 +- lms/static/js/spec/views/fields_helpers.js | 34 ++- lms/static/js/spec/views/fields_spec.js | 52 ++++- .../views/account_settings_factory.js | 24 ++- .../views/account_settings_fields.js | 11 +- .../views/learner_profile_factory.js | 13 +- lms/static/js/views/fields.js | 74 +++++-- lms/static/sass/_developer.scss | 14 ++ .../sass/course/instructor/_instructor_2.scss | 13 -- lms/static/sass/views/_teams.scss | 156 ++++++++++++++ .../fields/field_textarea.underscore | 26 ++- 28 files changed, 1216 insertions(+), 93 deletions(-) create mode 100644 lms/djangoapps/teams/static/teams/js/spec/edit_team_spec.js create mode 100644 lms/djangoapps/teams/static/teams/js/spec/team_actions_spec.js create mode 100644 lms/djangoapps/teams/static/teams/js/views/edit_team.js create mode 100644 lms/djangoapps/teams/static/teams/js/views/team_actions.js create mode 100644 lms/djangoapps/teams/static/teams/templates/edit-team.underscore create mode 100644 lms/djangoapps/teams/static/teams/templates/team-actions.underscore diff --git a/common/test/acceptance/pages/lms/fields.py b/common/test/acceptance/pages/lms/fields.py index 014aa11bdb..754b692955 100644 --- a/common/test/acceptance/pages/lms/fields.py +++ b/common/test/acceptance/pages/lms/fields.py @@ -80,6 +80,15 @@ class FieldsMixin(object): query = self.q(css='.u-field-{} .u-field-message'.format(field_id)) return query.text[0] if query.present else None + def message_for_textarea_field(self, field_id): + """ + Return the current message for textarea field. + """ + self.wait_for_field(field_id) + + query = self.q(css='.u-field-{} .u-field-message-help'.format(field_id)) + return query.text[0] if query.present else None + def wait_for_message(self, field_id, message): """ Wait for a message to appear in a field. @@ -229,3 +238,10 @@ class FieldsMixin(object): query = self.q(css='.u-field-{} a'.format(field_id)) if query.present: query.first.click() + + def error_for_field(self, field_id): + """ + Returns bool based on the highlighted border for field. + """ + query = self.q(css='.u-field-{}.error'.format(field_id)) + return True if query.present else False diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py index ce224ebb9c..09f6f8d698 100644 --- a/common/test/acceptance/pages/lms/teams.py +++ b/common/test/acceptance/pages/lms/teams.py @@ -6,11 +6,14 @@ Teams pages. from .course_page import CoursePage from ..common.paging import PaginatedUIMixin +from .fields import FieldsMixin + TOPIC_CARD_CSS = 'div.wrapper-card-core' BROWSE_BUTTON_CSS = 'a.nav-item[data-index="1"]' TEAMS_LINK_CSS = '.action-view' TEAMS_HEADER_CSS = '.teams-header' +CREATE_TEAM_LINK_CSS = '.create-team' class TeamsPage(CoursePage): @@ -84,6 +87,7 @@ class BrowseTeamsPage(CoursePage, PaginatedUIMixin): def is_browser_on_page(self): """Check if we're on the teams list page for a particular topic.""" + self.wait_for_element_presence('.team-actions', 'Wait for the bottom links to be present') has_correct_url = self.url.endswith(self.url_path) teams_list_view_present = self.q(css='.teams-main').present return has_correct_url and teams_list_view_present @@ -102,3 +106,76 @@ class BrowseTeamsPage(CoursePage, PaginatedUIMixin): def team_cards(self): """Get all the team cards on the page.""" return self.q(css='.team-card') + + def click_create_team_link(self): + """ Click on create team link.""" + query = self.q(css=CREATE_TEAM_LINK_CSS) + if query.present: + query.first.click() + self.wait_for_ajax() + + def click_search_team_link(self): + """ Click on create team link.""" + query = self.q(css='.search-team-descriptions') + if query.present: + query.first.click() + self.wait_for_ajax() + + def click_browse_all_teams_link(self): + """ Click on browse team link.""" + query = self.q(css='.browse-teams') + if query.present: + query.first.click() + self.wait_for_ajax() + + +class CreateTeamPage(CoursePage, FieldsMixin): + """ + Create team page. + """ + def __init__(self, browser, course_id, topic): + """ + Set up `self.url_path` on instantiation, since it dynamically + reflects the current topic. Note that `topic` is a dict + representation of a topic following the same convention as a + course module's topic. + """ + super(CreateTeamPage, self).__init__(browser, course_id) + self.topic = topic + self.url_path = "teams/#topics/{topic_id}/create-team".format(topic_id=self.topic['id']) + + def is_browser_on_page(self): + """Check if we're on the create team page for a particular topic.""" + has_correct_url = self.url.endswith(self.url_path) + teams_create_view_present = self.q(css='.team-edit-fields').present + return has_correct_url and teams_create_view_present + + @property + def header_page_name(self): + """Get the page name displayed by the page header""" + return self.q(css='.page-header .page-title')[0].text + + @property + def header_page_description(self): + """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""" + return self.q(css='.create-team.wrapper-msg .copy')[0].text + + def submit_form(self): + """Click on create team button""" + self.q(css='.create-team .action-primary').first.click() + self.wait_for_ajax() + + def cancel_team(self): + """Click on cancel team button""" + self.q(css='.create-team .action-cancel').first.click() + self.wait_for_ajax() diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index ae1f9917cf..7e070693a8 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -6,7 +6,7 @@ import json from nose.plugins.attrib import attr from ..helpers import UniqueCourseTest -from ...pages.lms.teams import TeamsPage, BrowseTopicsPage, BrowseTeamsPage +from ...pages.lms.teams import TeamsPage, BrowseTopicsPage, BrowseTeamsPage, CreateTeamPage from ...fixtures import LMS_BASE_URL from ...fixtures.course import CourseFixture from ...pages.lms.tab_nav import TabNavPage @@ -270,6 +270,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"} self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}) self.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic) + self.topics_page = BrowseTopicsPage(self.browser, self.course_id) def create_teams(self, num_teams): """Create `num_teams` teams belonging to `self.topic`.""" @@ -455,3 +456,196 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): self.browse_teams_page.team_cards[0].find_element_by_css_selector('.member-count').text, '1 / 10 Members' ) + + def test_navigation_links(self): + """ + Scenario: User should be able to navigate to "browse all teams" and "search team description" links. + Given I am enrolled in a course with a team configuration and a topic + containing one team + When I visit the Teams page for that topic + Then I should see the correct page header + And I should see the link to "browse all team" + And I should navigate to that link + And I see the relevant page loaded + And I should see the link to "search teams" + And I should navigate to that link + And I see the relevant page loaded + """ + self.browse_teams_page.visit() + self.verify_page_header() + + self.browse_teams_page.click_browse_all_teams_link() + self.assertTrue(self.topics_page.is_browser_on_page()) + + self.browse_teams_page.visit() + self.verify_page_header() + self.browse_teams_page.click_search_team_link() + # TODO Add search page expectation once that implemented. + + +@attr('shard_5') +class CreateTeamTest(TeamsTabBase): + """ + Tests for creating a new Team within a Topic on the Teams page. + """ + + def setUp(self): + super(CreateTeamTest, self).setUp() + self.topic = {'name': 'Example Topic', 'id': 'example_topic', 'description': 'Description'} + self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}) + self.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic) + self.browse_teams_page.visit() + self.create_team_page = CreateTeamPage(self.browser, self.course_id, self.topic) + self.team_name = 'Avengers' + + def verify_page_header(self): + """ + Verify that the page header correctly reflects the + create team header, description and breadcrumb. + """ + self.assertEqual(self.create_team_page.header_page_name, 'Create a New Team') + self.assertEqual( + self.create_team_page.header_page_description, + 'Create a new team if you can\'t find existing teams to join, ' + 'or if you would like to learn with friends you know.' + ) + self.assertEqual(self.create_team_page.header_page_breadcrumbs, self.topic['name']) + + def verify_and_navigate_to_create_team_page(self): + """Navigates to the create team page and verifies.""" + self.browse_teams_page.click_create_team_link() + self.verify_page_header() + + def fill_create_form(self): + """Fill the create team form fields with appropriate values.""" + self.create_team_page.value_for_text_field(field_id='name', value=self.team_name) + self.create_team_page.value_for_textarea_field( + field_id='description', + value='The Avengers are a fictional team of superheroes.' + ) + self.create_team_page.value_for_dropdown_field(field_id='language', value='English') + self.create_team_page.value_for_dropdown_field(field_id='country', value='Pakistan') + + def test_user_can_see_create_team_page(self): + """ + Scenario: The user should be able to see the create team page via teams list page. + Given I am enrolled in a course with a team configuration and a topic + When I visit the Teams page for that topic + Then I should see the Create Team page link on bottom + And When I click create team link + Then I should see the create team page. + And I should see the create team header + And I should also see the help messages for fields. + """ + self.verify_and_navigate_to_create_team_page() + self.assertEqual( + self.create_team_page.message_for_field('name'), + 'A name that identifies your team (maximum 255 characters).' + ) + self.assertEqual( + self.create_team_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_team_page.message_for_field('country'), + 'The country that team members primarily identify with.' + ) + self.assertEqual( + self.create_team_page.message_for_field('language'), + 'The language that team members primarily use to communicate with each other.' + ) + + def test_user_can_see_error_message_for_missing_data(self): + """ + Scenario: The user should be able to see error message in case of missing required field. + Given I am enrolled in a course with a team configuration and a topic + When I visit the Create Team page for that topic + Then I should see the Create Team header and form + And When I click create team button without filling required fields + Then I should see the error message and highlighted fields. + """ + self.verify_and_navigate_to_create_team_page() + self.create_team_page.submit_form() + + self.assertEqual( + self.create_team_page.validation_message_text, + 'Check the highlighted fields below and try again.' + ) + self.assertTrue(self.create_team_page.error_for_field(field_id='name')) + self.assertTrue(self.create_team_page.error_for_field(field_id='description')) + + def test_user_can_see_error_message_for_incorrect_data(self): + """ + Scenario: The user should be able to see error message in case of increasing length for required fields. + Given I am enrolled in a course with a team configuration and a topic + When I visit the Create Team page for that topic + Then I should see the Create Team header and form + When I add text > than 255 characters for name field + And I click Create button + Then I should see the error message for exceeding length. + """ + self.verify_and_navigate_to_create_team_page() + + # Fill the name field with >255 characters to see validation message. + self.create_team_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 ' + 'audience, some at no charge. It also conducts research into learning based on how ' + 'people use its platform. EdX was created for students and institutions that seek to' + 'transform themselves through cutting-edge technologies, innovative pedagogy, and ' + 'rigorous courses. More than 70 schools, nonprofits, corporations, and international' + 'organizations offer or plan to offer courses on the edX website. As of 22 October 2014,' + 'edX has more than 4 million users taking more than 500 courses online.' + ) + self.create_team_page.submit_form() + + self.assertEqual( + self.create_team_page.validation_message_text, + 'Check the highlighted fields below and try again.' + ) + self.assertTrue(self.create_team_page.error_for_field(field_id='name')) + + def test_user_can_create_new_team_successfully(self): + """ + Scenario: The user should be able to create new team. + Given I am enrolled in a course with a team configuration and a topic + When I visit the Create Team page for that topic + Then I should see the Create Team header and form + When I fill all the fields present with appropriate data + And I click Create button + Then I should see teams list page with newly created team. + """ + self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total') + self.verify_and_navigate_to_create_team_page() + + self.fill_create_form() + self.create_team_page.submit_form() + + self.assertTrue(self.browse_teams_page.is_browser_on_page()) + self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 1 out of 1 total') + # Verify the newly created team content. + team_card = self.browse_teams_page.team_cards.results[0] + self.assertEqual(team_card.find_element_by_css_selector('.card-title').text, self.team_name) + self.assertEqual( + team_card.find_element_by_css_selector('.card-description').text, + 'The Avengers are a fictional team of superheroes.' + ) + + def test_user_can_cancel_the_team_creation(self): + """ + Scenario: The user should be able to cancel the creation of new team. + Given I am enrolled in a course with a team configuration and a topic + When I visit the Create Team page for that topic + Then I should see the Create Team header and form + When I click Cancel button + Then I should see teams list page without any new team. + """ + self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total') + + self.verify_and_navigate_to_create_team_page() + self.create_team_page.cancel_team() + + self.assertTrue(self.browse_teams_page.is_browser_on_page()) + self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total') diff --git a/lms/djangoapps/teams/static/teams/js/models/team.js b/lms/djangoapps/teams/static/teams/js/models/team.js index 3387008975..32a4531beb 100644 --- a/lms/djangoapps/teams/static/teams/js/models/team.js +++ b/lms/djangoapps/teams/static/teams/js/models/team.js @@ -6,7 +6,7 @@ define(['backbone'], function (Backbone) { var Team = Backbone.Model.extend({ defaults: { - id: '', + id: null, name: '', is_active: null, course_id: '', diff --git a/lms/djangoapps/teams/static/teams/js/spec/edit_team_spec.js b/lms/djangoapps/teams/static/teams/js/spec/edit_team_spec.js new file mode 100644 index 0000000000..553389c778 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/spec/edit_team_spec.js @@ -0,0 +1,170 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common/js/spec_helpers/ajax_helpers', + 'teams/js/views/edit_team' +], function ($, _, Backbone, AjaxHelpers, TeamEditView) { + 'use strict'; + + describe('EditTeam', function () { + var teamEditView, + teamsUrl = '/api/team/v0/teams/', + teamsData = { + id: null, + name: "TeamName", + is_active: null, + course_id: "a/b/c", + topic_id: "awesomeness", + date_created: "", + description: "TeamDescription", + country: "c", + language: "a", + membership: [] + }, + verifyValidation = function (requests, fieldsData) { + _.each(fieldsData, function (fieldData) { + teamEditView.$(fieldData[0]).val(fieldData[1]); + }); + + teamEditView.$('.create-team.form-actions .action-primary').click(); + + var message = teamEditView.$('.wrapper-msg'); + expect(message.hasClass('is-hidden')).toBeFalsy(); + expect(message.find('.title').text().trim()).toBe("Your team could not be created!"); + expect(message.find('.copy').text().trim()).toBe( + "Check the highlighted fields below and try again." + ); + + _.each(fieldsData, function (fieldData) { + if(fieldData[2] === 'error') { + expect(teamEditView.$(fieldData[0].split(" ")[0] + '.error').length).toBe(1); + } else if(fieldData[2] === 'success') { + expect(teamEditView.$(fieldData[0].split(" ")[0] + '.error').length).toBe(0); + } + }); + + expect(requests.length).toBe(0); + }, + expectContent = function (selector, text) { + expect(teamEditView.$(selector).text().trim()).toBe(text); + }, + verifyDropdownData = function (selector, expectedItems) { + var options = teamEditView.$(selector)[0].options; + var renderedItems = $.map(options, function( elem ) { + return [[elem.value, elem.text]]; + }); + for (var i = 0; i < expectedItems.length; i++) { + expect(renderedItems).toContain(expectedItems[i]); + } + }; + + beforeEach(function () { + setFixtures('
'); + spyOn(Backbone.history, 'navigate'); + teamEditView = new TeamEditView({ + el: $('.teams-content'), + teamParams: { + teamsUrl: teamsUrl, + courseId: "a/b/c", + topicId: 'awesomeness', + topicName: 'Awesomeness', + languages: [['a', 'aaa'], ['b', 'bbb']], + countries: [['c', 'ccc'], ['d', 'ddd']] + } + }).render(); + }); + + it('can render itself correctly', function () { + var fieldClasses = [ + '.u-field-name', + '.u-field-description', + '.u-field-optional_description', + '.u-field-language', + '.u-field-country' + ]; + + _.each(fieldClasses, function (fieldClass) { + expect(teamEditView.$el.find(fieldClass).length).toBe(1); + }); + + expect(teamEditView.$('.create-team.form-actions .action-primary').length).toBe(1); + expect(teamEditView.$('.create-team.form-actions .action-cancel').length).toBe(1); + }); + + it('can create a team', function () { + var requests = AjaxHelpers.requests(this); + + teamEditView.$('.u-field-name input').val(teamsData.name); + teamEditView.$('.u-field-textarea textarea').val(teamsData.description); + teamEditView.$('.u-field-language select').val('a').attr("selected", "selected"); + teamEditView.$('.u-field-country select').val('c').attr("selected", "selected"); + + teamEditView.$('.create-team.form-actions .action-primary').click(); + AjaxHelpers.expectJsonRequest(requests, 'POST', teamsUrl, teamsData); + AjaxHelpers.respondWithJson(requests, teamsData); + + expect(teamEditView.$('.create-team.wrapper-msg .copy').text().trim().length).toBe(0); + expect(Backbone.history.navigate.calls[0].args).toContain('topics/awesomeness'); + }); + + it('shows validation error message when field is empty', function () { + var requests = AjaxHelpers.requests(this); + verifyValidation(requests, [ + ['.u-field-name input', 'Name', 'success'], + ['.u-field-textarea textarea', '', 'error'] + ]); + teamEditView.render(); + verifyValidation(requests, [ + ['.u-field-name input', '', 'error'], + ['.u-field-textarea textarea', 'description', 'success'] + ]); + teamEditView.render(); + verifyValidation(requests, [ + ['.u-field-name input', '', 'error'], + ['.u-field-textarea textarea', '', 'error'] + ]); + }); + + it('shows validation error message when field value length exceeded the limit', function () { + var requests = AjaxHelpers.requests(this); + var teamName = new Array(500 + 1).join( '$' ); + var teamDescription = new Array(500 + 1).join( '$' ); + + verifyValidation(requests, [ + ['.u-field-name input', teamName, 'error'], + ['.u-field-textarea textarea', 'description', 'success'] + ]); + teamEditView.render(); + verifyValidation(requests, [ + ['.u-field-name input', 'name', 'success'], + ['.u-field-textarea textarea', teamDescription, 'error'] + ]); + teamEditView.render(); + verifyValidation(requests, [ + ['.u-field-name input', teamName, 'error'], + ['.u-field-textarea textarea', teamDescription, 'error'] + ]); + }); + + it("shows an error message for HTTP 500", function () { + var requests = AjaxHelpers.requests(this); + + teamEditView.$('.u-field-name input').val(teamsData.name); + teamEditView.$('.u-field-textarea textarea').val(teamsData.description); + + teamEditView.$('.create-team.form-actions .action-primary').click(); + teamsData.country = ''; + teamsData.language = ''; + AjaxHelpers.expectJsonRequest(requests, 'POST', teamsUrl, teamsData); + AjaxHelpers.respondWithError(requests); + + expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe("An error occurred. Please try again."); + }); + + it("changes route on cancel click", function () { + teamEditView.$('.create-team.form-actions .action-cancel').click(); + expect(Backbone.history.navigate.calls[0].args).toContain('topics/awesomeness'); + }); + }); +}); diff --git a/lms/djangoapps/teams/static/teams/js/spec/team_actions_spec.js b/lms/djangoapps/teams/static/teams/js/spec/team_actions_spec.js new file mode 100644 index 0000000000..7f32b4dfac --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/spec/team_actions_spec.js @@ -0,0 +1,40 @@ +define([ + 'jquery', + 'backbone', + 'teams/js/views/team_actions' +], function ($, Backbone, TeamActionsView) { + 'use strict'; + + describe('TeamActions', function () { + var teamActionsView; + + beforeEach(function () { + setFixtures('
'); + spyOn(Backbone.history, 'navigate'); + teamActionsView = new TeamActionsView({ + el: $('.teams-content'), + teamParams: {topicId: 'awesomeness'} + }).render(); + }); + + it('can render itself correctly', function () { + expect(teamActionsView.$('.title').text()).toBe('Are you having trouble finding a team to join?'); + expect(teamActionsView.$('.copy').text()).toBe( + "Try browsing all teams or searching team descriptions. If you " + + "still can't find a team to join, create a new team in this topic." + ); + }); + + it('can navigate to correct routes', function () { + teamActionsView.$('a.browse-teams').click(); + expect(Backbone.history.navigate.calls[0].args).toContain('browse'); + + teamActionsView.$('a.search-team-descriptions').click(); + // TODO! Should be updated once team description search feature is available + expect(Backbone.history.navigate.calls[1].args).toContain('browse'); + + teamActionsView.$('a.create-team').click(); + expect(Backbone.history.navigate.calls[2].args).toContain('topics/awesomeness/create-team'); + }); + }); +}); diff --git a/lms/djangoapps/teams/static/teams/js/spec/teams_spec.js b/lms/djangoapps/teams/static/teams/js/spec/teams_spec.js index 355de9074a..8bea272a86 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/teams_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/teams_spec.js @@ -1,6 +1,6 @@ define([ - 'teams/js/collections/team', 'teams/js/views/teams' -], function (TeamCollection, TeamsView) { + 'backbone', 'teams/js/collections/team', 'teams/js/views/teams' +], function (Backbone, TeamCollection, TeamsView) { 'use strict'; describe('TeamsView', function () { var teamsView, teamCollection, initialTeams, @@ -32,7 +32,8 @@ define([ ); teamsView = new TeamsView({ el: '.teams-container', - collection: teamCollection + collection: teamCollection, + teamParams: {} }).render(); }); diff --git a/lms/djangoapps/teams/static/teams/js/views/edit_team.js b/lms/djangoapps/teams/static/teams/js/views/edit_team.js new file mode 100644 index 0000000000..24019390fe --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/views/edit_team.js @@ -0,0 +1,192 @@ +;(function (define) { +'use strict'; + +define(['backbone', + 'underscore', + 'gettext', + 'js/views/fields', + 'teams/js/models/team', + 'text!teams/templates/edit-team.underscore'], + function (Backbone, _, gettext, FieldViews, TeamModel, edit_team_template) { + return Backbone.View.extend({ + + maxTeamNameLength: 255, + maxTeamDescriptionLength: 300, + + events: { + "click .action-primary": "createTeam", + "click .action-cancel": "goBackToTopic" + }, + + initialize: function(options) { + this.courseId = options.teamParams.courseId; + this.teamsUrl = options.teamParams.teamsUrl; + this.topicId = options.teamParams.topicId; + this.languages = options.teamParams.languages; + this.countries = options.teamParams.countries; + this.primaryButtonTitle = options.primaryButtonTitle || 'Submit'; + + _.bindAll(this, "goBackToTopic", "createTeam"); + + this.teamModel = new TeamModel({}); + this.teamModel.url = this.teamsUrl; + + this.teamNameField = new FieldViews.TextFieldView({ + model: this.teamModel, + title: gettext("Team Name (Required) *"), + valueAttribute: 'name', + helpMessage: gettext("A name that identifies your team (maximum 255 characters).") + }); + + this.teamDescriptionField = new FieldViews.TextareaFieldView({ + model: this.teamModel, + title: gettext("Team Description (Required) *"), + valueAttribute: 'description', + editable: 'always', + showMessages: false, + helpMessage: gettext("A short description of the team to help other learners understand the goals or direction of the team (maximum 300 characters).") + }); + + this.optionalDescriptionField = new FieldViews.ReadonlyFieldView({ + model: this.teamModel, + title: gettext("Optional Characteristics"), + valueAttribute: 'optional_description', + helpMessage: gettext("Help other learners decide whether to join your team by specifying some characteristics for your team. Choose carefully, because fewer people might be interested in joining your team if it seems too restrictive.") + }); + + this.teamLanguageField = new FieldViews.DropdownFieldView({ + model: this.teamModel, + title: gettext("Language"), + valueAttribute: 'language', + required: false, + showMessages: false, + titleIconName: 'fa-comment-o', + options: this.languages, + helpMessage: gettext("The language that team members primarily use to communicate with each other.") + }); + + this.teamCountryField = new FieldViews.DropdownFieldView({ + model: this.teamModel, + title: gettext('Country'), + valueAttribute: 'country', + required: false, + showMessages: false, + titleIconName: 'fa-globe', + options: this.countries, + helpMessage: gettext("The country that team members primarily identify with.") + }); + }, + + render: function() { + this.$el.html(_.template(edit_team_template)({primaryButtonTitle: this.primaryButtonTitle})); + this.set(this.teamNameField, '.team-required-fields'); + this.set(this.teamDescriptionField, '.team-required-fields'); + this.set(this.optionalDescriptionField, '.team-optional-fields'); + this.set(this.teamLanguageField, '.team-optional-fields'); + this.set(this.teamCountryField, '.team-optional-fields'); + return this; + }, + + set: function(view, selector) { + var viewEl = view.$el; + if (this.$(selector).has(viewEl).length) { + view.render().setElement(viewEl); + } else { + this.$(selector).append(view.render().$el); + } + }, + + createTeam: function () { + var teamName = this.teamNameField.fieldValue(); + var teamDescription = this.teamDescriptionField.fieldValue(); + var teamLanguage = this.teamLanguageField.fieldValue(); + var teamCountry = this.teamCountryField.fieldValue(); + + var data = { + course_id: this.courseId, + topic_id: this.topicId, + name: teamName, + description: teamDescription, + language: _.isNull(teamLanguage) ? '' : teamLanguage, + country: _.isNull(teamCountry) ? '' : teamCountry + }; + + var validationResult = this.validateTeamData(data); + if (validationResult.status === false) { + this.showMessage(validationResult.message, validationResult.srMessage); + return; + } + + var view = this; + var options = { + wait: true, + success: function () { + view.goBackToTopic(); + }, + error: function () { + var message = gettext('An error occurred. Please try again.'); + view.showMessage(message, message); + } + }; + this.teamModel.save(data, options); + }, + + validateTeamData: function (data) { + var status = true, + message = gettext("Check the highlighted fields below and try again."); + var srMessages = []; + + this.teamNameField.unhighlightField(); + this.teamDescriptionField.unhighlightField(); + + if (_.isEmpty(data.name.trim()) ) { + status = false; + this.teamNameField.highlightFieldOnError(); + srMessages.push( + gettext("Enter team name.") + ); + } else if (data.name.length > this.maxTeamNameLength) { + status = false; + this.teamNameField.highlightFieldOnError(); + srMessages.push( + gettext("Team name cannot have more than 255 characters.") + ); + } + + if (_.isEmpty(data.description.trim()) ) { + status = false; + this.teamDescriptionField.highlightFieldOnError(); + srMessages.push( + gettext("Enter team description.") + ); + } else if (data.description.length > this.maxTeamDescriptionLength) { + status = false; + this.teamDescriptionField.highlightFieldOnError(); + srMessages.push( + gettext("Team description cannot have more than 300 characters.") + ); + } + + return { + status: status, + message: message, + srMessage: srMessages.join(" ") + }; + }, + + showMessage: function (message, screenReaderMessage) { + this.$('.wrapper-msg').removeClass('is-hidden'); + this.$('.msg-content .copy p').text(message); + this.$('.wrapper-msg').focus(); + + if (screenReaderMessage) { + this.$('.screen-reader-message').text(screenReaderMessage); + } + }, + + goBackToTopic: function () { + Backbone.history.navigate("topics/" + this.topicId, {trigger: true}); + } + }); + }); +}).call(this, define || RequireJS.define); diff --git a/lms/djangoapps/teams/static/teams/js/views/team_actions.js b/lms/djangoapps/teams/static/teams/js/views/team_actions.js new file mode 100644 index 0000000000..01927c1404 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/js/views/team_actions.js @@ -0,0 +1,52 @@ +;(function (define) { + 'use strict'; + define([ + 'gettext', + 'underscore', + 'backbone', + 'text!teams/templates/team-actions.underscore' + ], function (gettext, _, Backbone, team_actions_template) { + return Backbone.View.extend({ + events: { + 'click a.browse-teams': 'browseTeams', + 'click a.search-team-descriptions': 'searchTeamDescriptions', + 'click a.create-team': 'showCreateTeamForm' + }, + + initialize: function (options) { + this.template = _.template(team_actions_template); + this.teamParams = options.teamParams; + }, + + render: function () { + var message = interpolate_text( + _.escape(gettext("Try {browse_span_start}browsing all teams{span_end} or {search_span_start}searching team descriptions{span_end}. If you still can't find a team to join, {create_span_start}create a new team in this topic{span_end}.")), + { + 'browse_span_start': '', + 'search_span_start': '', + 'create_span_start': '', + 'span_end': '' + } + ); + this.$el.html(this.template({message: message})); + return this; + }, + + browseTeams: function (event) { + event.preventDefault(); + Backbone.history.navigate('browse', {trigger: true}); + }, + + searchTeamDescriptions: function (event) { + event.preventDefault(); + // TODO! Will navigate to correct place once required functionality is available + Backbone.history.navigate('browse', {trigger: true}); + }, + + showCreateTeamForm: function (event) { + event.preventDefault(); + Backbone.history.navigate('topics/' + this.teamParams.topicId + '/create-team', {trigger: true}); + } + }); + }); +}).call(this, define || RequireJS.define); diff --git a/lms/djangoapps/teams/static/teams/js/views/teams.js b/lms/djangoapps/teams/static/teams/js/views/teams.js index 7d0d5a9670..c509da128a 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams.js @@ -1,30 +1,31 @@ ;(function (define) { 'use strict'; define([ + 'backbone', 'teams/js/views/team_card', - 'common/js/components/views/paginated_view' - ], function (TeamCardView, PaginatedView) { + 'common/js/components/views/paginated_view', + 'teams/js/views/team_actions' + ], function (Backbone, TeamCardView, PaginatedView, TeamActionsView) { var TeamsView = PaginatedView.extend({ type: 'teams', - events: { - 'click button.action': '' // entry point for team creation - }, - initialize: function (options) { this.itemViewClass = TeamCardView.extend({ router: options.router, maxTeamSize: options.maxTeamSize }); PaginatedView.prototype.initialize.call(this); + this.teamParams = options.teamParams; }, render: function () { PaginatedView.prototype.render.call(this); - this.$el.append( - $('') - ); + var teamActionsView = new TeamActionsView({ + teamParams: this.teamParams + }); + this.$el.append(teamActionsView.$el); + teamActionsView.render(); return this; } }); diff --git a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js index ce7d312493..fd5bbe9e66 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -12,9 +12,11 @@ 'teams/js/collections/topic', 'teams/js/views/teams', 'teams/js/collections/team', + 'teams/js/views/edit_team', 'text!teams/templates/teams_tab.underscore'], function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView, - TopicsView, TopicModel, TopicCollection, TeamsView, TeamCollection, teamsTemplate) { + TopicsView, TopicModel, TopicCollection, TeamsView, TeamCollection, + TeamEditView, teamsTemplate) { var ViewWithHeader = Backbone.View.extend({ initialize: function (options) { this.header = options.header; @@ -38,6 +40,8 @@ this.topic_url = options.topic_url; this.teams_url = options.teams_url; this.maxTeamSize = options.maxTeamSize; + this.languages = options.languages; + this.countries = options.countries; // This slightly tedious approach is necessary // to use regular expressions within Backbone // routes, allowing us to capture which tab @@ -45,9 +49,10 @@ router = this.router = new Backbone.Router(); _.each([ [':default', _.bind(this.routeNotFound, this)], - ['topics/:topic_id', _.bind(this.browseTopic, this)], - [new RegExp('^(browse)$'), _.bind(this.goToTab, this)], - [new RegExp('^(teams)$'), _.bind(this.goToTab, this)] + ['topics/:topic_id(/)', _.bind(this.browseTopic, this)], + ['topics/:topic_id/create-team(/)', _.bind(this.newTeam, this)], + [new RegExp('^(browse)\/?$'), _.bind(this.goToTab, this)], + [new RegExp('^(teams)\/?$'), _.bind(this.goToTab, this)] ], function (route) { router.route.apply(router, route); }); @@ -96,6 +101,35 @@ return this; }, + /** + * Render the create new team form. + */ + newTeam: function (topicId) { + var self = this; + this.getTeamsView(topicId).done(function (teamsView) { + self.mainView = new ViewWithHeader({ + header: new HeaderView({ + model: new HeaderModel({ + description: gettext("Create a new team if you can't find existing teams to join, or if you would like to learn with friends you know."), + title: gettext("Create a New Team"), + breadcrumbs: [ + { + title: teamsView.main.teamParams.topicName, + url: '#topics/' + teamsView.main.teamParams.topicId + } + ] + }) + }), + main: new TeamEditView({ + tagName: 'create-new-team', + teamParams: teamsView.main.teamParams, + primaryButtonTitle: 'Create' + }) + }); + self.render(); + }); + }, + /** * Render the list of teams for the given topic ID. */ @@ -172,6 +206,14 @@ per_page: 10, parse: true }), + teamParams: { + courseId: this.course_id, + teamsUrl: this.teams_url, + topicId: topic.get('id'), + topicName: topic.get('name'), + languages: self.languages, + countries: self.countries + }, maxTeamSize: this.maxTeamSize }) }); diff --git a/lms/djangoapps/teams/static/teams/templates/edit-team.underscore b/lms/djangoapps/teams/static/teams/templates/edit-team.underscore new file mode 100644 index 0000000000..59384f86d1 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/templates/edit-team.underscore @@ -0,0 +1,47 @@ + + +
+

+ <%- gettext("Enter information to describe your team. You cannot change these details after you create the team.") %>

+
+ +
+
+
+ +
+
+
+ +
+ + +
diff --git a/lms/djangoapps/teams/static/teams/templates/team-actions.underscore b/lms/djangoapps/teams/static/teams/templates/team-actions.underscore new file mode 100644 index 0000000000..b748b60c35 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/templates/team-actions.underscore @@ -0,0 +1,4 @@ +
+

<%- gettext("Are you having trouble finding a team to join?") %>

+

<%= message %>

+
\ No newline at end of file diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html index b53ce4a1dd..eb23a11d24 100644 --- a/lms/djangoapps/teams/templates/teams/teams.html +++ b/lms/djangoapps/teams/templates/teams/teams.html @@ -28,7 +28,9 @@ topics_url: '${ topics_url }', teams_url: '${ teams_url }', maxTeamSize: ${ course.teams_max_size }, - course_id: '${ unicode(course.id) }' + course_id: '${ unicode(course.id) }', + languages: ${ json.dumps(languages, cls=EscapedEdxJSONEncoder) }, + countries: ${ json.dumps(countries, cls=EscapedEdxJSONEncoder) } }); diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py index 2090608eee..313b433c9f 100644 --- a/lms/djangoapps/teams/views.py +++ b/lms/djangoapps/teams/views.py @@ -21,6 +21,7 @@ from rest_framework import permissions from django.db.models import Count from django.contrib.auth.models import User +from django_countries import countries from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_noop @@ -96,7 +97,9 @@ class TeamsDashboardView(View): 'topics_detail', kwargs={'topic_id': 'topic_id', 'course_id': str(course_id)}, request=request ), "topics_url": reverse('topics_list', request=request), - "teams_url": reverse('teams_list', request=request) + "teams_url": reverse('teams_list', request=request), + "languages": settings.ALL_LANGUAGES, + "countries": list(countries), } return render_to_response("teams/teams.html", context) diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index 5e922cb6fe..7f688c473e 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -533,6 +533,8 @@ 'lms/include/teams/js/spec/topics_spec.js', 'lms/include/teams/js/spec/teams_spec.js', 'lms/include/teams/js/spec/teams_tab_spec.js', + 'lms/include/teams/js/spec/team_actions_spec.js', + 'lms/include/teams/js/spec/edit_team_spec.js', 'lms/include/js/spec/components/header/header_spec.js', 'lms/include/js/spec/components/tabbed/tabbed_view_spec.js', 'lms/include/js/spec/components/card/card_spec.js', diff --git a/lms/static/js/spec/student_account/account_settings_fields_spec.js b/lms/static/js/spec/student_account/account_settings_fields_spec.js index 1a0eb0704b..dd31bfad54 100644 --- a/lms/static/js/spec/student_account/account_settings_fields_spec.js +++ b/lms/static/js/spec/student_account/account_settings_fields_spec.js @@ -49,7 +49,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers var selector = '.u-field-value > select'; var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, { valueAttribute: 'language', - options: FieldViewsSpecHelpers.SELECT_OPTIONS + options: FieldViewsSpecHelpers.SELECT_OPTIONS, + persistChanges: true }); var view = new AccountSettingsFieldViews.LanguagePreferenceFieldView(fieldData).render(); @@ -92,7 +93,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers var selector = '.u-field-value > select'; var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, { valueAttribute: 'language_proficiencies', - options: FieldViewsSpecHelpers.SELECT_OPTIONS + options: FieldViewsSpecHelpers.SELECT_OPTIONS, + persistChanges: true }); fieldData.model.set({'language_proficiencies': [{'code': FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]}]}); diff --git a/lms/static/js/spec/student_profile/learner_profile_view_spec.js b/lms/static/js/spec/student_profile/learner_profile_view_spec.js index 6b3fa8a758..520ce2cb8f 100644 --- a/lms/static/js/spec/student_profile/learner_profile_view_spec.js +++ b/lms/static/js/spec/student_profile/learner_profile_view_spec.js @@ -101,7 +101,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers placeholderValue: "Tell other edX learners a little about yourself: where you live, " + "what your interests are, why you're taking courses on edX, or what you hope to learn.", valueAttribute: "bio", - helpMessage: '' + helpMessage: '', + messagePosition: 'header' }) ]; diff --git a/lms/static/js/spec/views/fields_helpers.js b/lms/static/js/spec/views/fields_helpers.js index acfb5e0b1c..5d322d2c40 100644 --- a/lms/static/js/spec/views/fields_helpers.js +++ b/lms/static/js/spec/views/fields_helpers.js @@ -126,6 +126,37 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers expectMessageContains(view, "Do not reset this!"); }; + var verifyPersistence = function (fieldClass, requests) { + var fieldData = createFieldData(fieldClass, { + title: 'Username', + valueAttribute: 'username', + helpMessage: 'The username that you use to sign in to edX.', + validValue: 'My Name', + persistChanges: false, + messagePosition: 'header' + }); + var view = new fieldClass(fieldData).render(); + var valueInputSelector; + + switch (fieldClass) { + case FieldViews.TextFieldView: + valueInputSelector = '.u-field-value > input'; + break; + case FieldViews.DropdownFieldView: + valueInputSelector = '.u-field-value > select'; + _.extend(fieldData, {validValue: SELECT_OPTIONS[0][0]}); + break; + case FieldViews.TextareaFieldView: + valueInputSelector = '.u-field-value > textarea'; + break; + } + + view.$(valueInputSelector).val(fieldData.validValue).change(); + expect(view.fieldValue()).toBe(fieldData.validValue); + expectMessageContains(view, view.helpMessage); + expect(requests.length).toBe(0); + }; + var verifyEditableField = function (view, data, requests) { var request_data = {}; var url = view.model.url; @@ -230,6 +261,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers verifySuccessMessageReset: verifySuccessMessageReset, verifyEditableField: verifyEditableField, verifyTextField: verifyTextField, - verifyDropDownField: verifyDropDownField + verifyDropDownField: verifyDropDownField, + verifyPersistence: verifyPersistence }; }); diff --git a/lms/static/js/spec/views/fields_spec.js b/lms/static/js/spec/views/fields_spec.js index f23cd755d2..01e79b9afa 100644 --- a/lms/static/js/spec/views/fields_spec.js +++ b/lms/static/js/spec/views/fields_spec.js @@ -72,7 +72,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers var fieldData = FieldViewsSpecHelpers.createFieldData(fieldViewClass, { title: 'Preferred Language', valueAttribute: 'language', - helpMessage: 'Your preferred language.' + helpMessage: 'Your preferred language.', + persistChanges: true }); var view = new fieldViewClass(fieldData); @@ -110,7 +111,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.TextFieldView, { title: 'Full Name', valueAttribute: 'name', - helpMessage: 'How are you?' + helpMessage: 'How are you?', + persistChanges: true }); var view = new FieldViews.TextFieldView(fieldData).render(); @@ -134,7 +136,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers title: 'Full Name', valueAttribute: 'name', helpMessage: 'edX full name', - editable: 'never' + editable: 'never', + persistChanges: true }); var view = new FieldViews.DropdownFieldView(fieldData).render(); @@ -154,7 +157,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.DropdownFieldView, { title: 'Full Name', valueAttribute: 'name', - helpMessage: 'edX full name' + helpMessage: 'edX full name', + persistChanges: true }); var view = new FieldViews.DropdownFieldView(fieldData).render(); @@ -178,7 +182,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers title: 'Full Name', valueAttribute: 'name', helpMessage: 'edX full name', - editable: 'toggle' + editable: 'toggle', + persistChanges: true }); var view = new FieldViews.DropdownFieldView(fieldData).render(); @@ -205,7 +210,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers valueAttribute: 'drop-down', helpMessage: 'edX drop down', editable: editable, - required:true + required:true, + persistChanges: true }); var view = new FieldViews.DropdownFieldView(fieldData).render(); @@ -230,7 +236,9 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers helpMessage: 'Wicked is good', placeholderValue: "Tell other edX learners a little about yourself: where you live, " + "what your interests are, why you’re taking courses on edX, or what you hope to learn.", - editable: 'never' + editable: 'never', + persistChanges: true, + messagePosition: 'header' }); // set bio to empty to see the placeholder. @@ -259,8 +267,9 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers helpMessage: 'Wicked is good', placeholderValue: "Tell other edX learners a little about yourself: where you live, " + "what your interests are, why you’re taking courses on edX, or what you hope to learn.", - editable: 'toggle' - + editable: 'toggle', + persistChanges: true, + messagePosition: 'header' }); fieldData.model.set({'bio': ''}); @@ -300,5 +309,30 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers FieldViewsSpecHelpers.expectTitleAndMessageToContain(view, fieldData.title, fieldData.helpMessage, false); expect(view.$('.u-field-value > a .u-field-link-title-' + view.options.valueAttribute).text().trim()).toBe(fieldData.linkTitle); }); + + it("correctly renders LinkFieldView", function() { + var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.LinkFieldView, { + title: 'Title', + linkTitle: 'Link title', + helpMessage: 'Click the link.', + valueAttribute: 'password-reset' + }); + var view = new FieldViews.LinkFieldView(fieldData).render(); + + FieldViewsSpecHelpers.expectTitleAndMessageToContain(view, fieldData.title, fieldData.helpMessage, false); + expect(view.$('.u-field-value > a .u-field-link-title-' + view.options.valueAttribute).text().trim()).toBe(fieldData.linkTitle); + }); + + it("can't persist changes if persistChanges is off", function() { + requests = AjaxHelpers.requests(this); + var fieldClasses = [ + FieldViews.TextFieldView, + FieldViews.DropdownFieldView, + FieldViews.TextareaFieldView + ]; + for (var i = 0; i < fieldClasses.length; i++) { + FieldViewsSpecHelpers.verifyPersistence(fieldClasses[i], requests); + } + }); }); }); diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index 1daf631906..706f3a0efb 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -39,7 +39,8 @@ model: userAccountModel, title: gettext('Full Name'), valueAttribute: 'name', - helpMessage: gettext('The name that appears on your certificates. Other learners never see your full name.') + helpMessage: gettext('The name that appears on your certificates. Other learners never see your full name.'), + persistChanges: true }) }, { @@ -49,7 +50,8 @@ valueAttribute: 'email', helpMessage: interpolate_text( gettext('The email address you use to sign in. Communications from {platform_name} and your courses are sent to this address.'), {platform_name: platformName} - ) + ), + persistChanges: true }) }, { @@ -74,7 +76,8 @@ helpMessage: interpolate_text( gettext('The language used throughout this site. This site is currently available in a limited number of languages.'), {platform_name: platformName} ), - options: fieldsData.language.options + options: fieldsData.language.options, + persistChanges: true }) }, { @@ -83,7 +86,8 @@ required: true, title: gettext('Country or Region'), valueAttribute: 'country', - options: fieldsData['country']['options'] + options: fieldsData['country']['options'], + persistChanges: true }) } ] @@ -96,7 +100,8 @@ model: userAccountModel, title: gettext('Education Completed'), valueAttribute: 'level_of_education', - options: fieldsData.level_of_education.options + options: fieldsData.level_of_education.options, + persistChanges: true }) }, { @@ -104,7 +109,8 @@ model: userAccountModel, title: gettext('Gender'), valueAttribute: 'gender', - options: fieldsData.gender.options + options: fieldsData.gender.options, + persistChanges: true }) }, { @@ -112,7 +118,8 @@ model: userAccountModel, title: gettext('Year of Birth'), valueAttribute: 'year_of_birth', - options: fieldsData['year_of_birth']['options'] + options: fieldsData['year_of_birth']['options'], + persistChanges: true }) }, { @@ -120,7 +127,8 @@ model: userAccountModel, title: gettext('Preferred Language'), valueAttribute: 'language_proficiencies', - options: fieldsData.preferred_language.options + options: fieldsData.preferred_language.options, + persistChanges: true }) } ] diff --git a/lms/static/js/student_account/views/account_settings_fields.js b/lms/static/js/student_account/views/account_settings_fields.js index 0dcbe6e4ba..f20f7bff4a 100644 --- a/lms/static/js/student_account/views/account_settings_fields.js +++ b/lms/static/js/student_account/views/account_settings_fields.js @@ -99,12 +99,13 @@ }, saveValue: function () { - var attributes = {}, - value = this.fieldValue() ? [{'code': this.fieldValue()}] : []; - attributes[this.options.valueAttribute] = value; - this.saveAttributes(attributes); + if (this.persistChanges === true) { + var attributes = {}, + value = this.fieldValue() ? [{'code': this.fieldValue()}] : []; + attributes[this.options.valueAttribute] = value; + this.saveAttributes(attributes); + } } - }); AccountSettingsFieldViews.AuthFieldView = FieldViews.LinkFieldView.extend({ diff --git a/lms/static/js/student_profile/views/learner_profile_factory.js b/lms/static/js/student_profile/views/learner_profile_factory.js index 54439ffef0..a727532ce0 100644 --- a/lms/static/js/student_profile/views/learner_profile_factory.js +++ b/lms/static/js/student_profile/views/learner_profile_factory.js @@ -54,7 +54,8 @@ ['all_users', gettext('Full Profile')] ], helpMessage: '', - accountSettingsPageUrl: options.account_settings_page_url + accountSettingsPageUrl: options.account_settings_page_url, + persistChanges: true }); var profileImageFieldView = new LearnerProfileFieldsView.ProfileImageFieldView({ @@ -87,7 +88,8 @@ placeholderValue: gettext('Add Country'), valueAttribute: "country", options: options.country_options, - helpMessage: '' + helpMessage: '', + persistChanges: true }), new AccountSettingsFieldViews.LanguageProficienciesFieldView({ model: accountSettingsModel, @@ -100,7 +102,8 @@ placeholderValue: gettext('Add language'), valueAttribute: "language_proficiencies", options: options.language_options, - helpMessage: '' + helpMessage: '', + persistChanges: true }) ]; @@ -112,7 +115,9 @@ title: gettext('About me'), placeholderValue: gettext("Tell other learners a little about yourself: where you live, what your interests are, why you're taking courses, or what you hope to learn."), valueAttribute: "bio", - helpMessage: '' + helpMessage: '', + persistChanges: true, + messagePosition: 'header' }) ]; diff --git a/lms/static/js/views/fields.js b/lms/static/js/views/fields.js index 224c83f9ab..0407637ecc 100644 --- a/lms/static/js/views/fields.js +++ b/lms/static/js/views/fields.js @@ -1,8 +1,22 @@ ;(function (define, undefined) { 'use strict'; define([ - 'gettext', 'jquery', 'underscore', 'backbone', 'backbone-super', 'jquery.fileupload' - ], function (gettext, $, _, Backbone) { + 'gettext', 'jquery', 'underscore', 'backbone', + 'text!templates/fields/field_readonly.underscore', + 'text!templates/fields/field_dropdown.underscore', + 'text!templates/fields/field_link.underscore', + 'text!templates/fields/field_text.underscore', + 'text!templates/fields/field_textarea.underscore', + 'text!templates/fields/field_image.underscore', + 'backbone-super', 'jquery.fileupload' + ], function (gettext, $, _, Backbone, + field_readonly_template, + field_dropdown_template, + field_link_template, + field_text_template, + field_textarea_template, + field_image_template + ) { var messageRevertDelay = 6000; var FieldViews = {}; @@ -36,7 +50,7 @@ initialize: function () { - this.template = _.template($(this.templateSelector).text()); + this.template = _.template(this.fieldTemplate || ''); this.helpMessage = this.options.helpMessage || ''; this.showMessages = _.isUndefined(this.options.showMessages) ? true : this.options.showMessages; @@ -142,6 +156,7 @@ FieldViews.EditableFieldView = FieldViews.FieldView.extend({ initialize: function (options) { + this.persistChanges = _.isUndefined(options.persistChanges) ? false : options.persistChanges; _.bindAll(this, 'saveAttributes', 'saveSucceeded', 'showDisplayMode', 'showEditMode', 'startEditing', 'finishEditing' ); @@ -158,20 +173,22 @@ }, saveAttributes: function (attributes, options) { - var view = this; - var defaultOptions = { - contentType: 'application/merge-patch+json', - patch: true, - wait: true, - success: function () { - view.saveSucceeded(); - }, - error: function (model, xhr) { - view.showErrorMessage(xhr); - } - }; - this.showInProgressMessage(); - this.model.save(attributes, _.extend(defaultOptions, options)); + if (this.persistChanges === true) { + var view = this; + var defaultOptions = { + contentType: 'application/merge-patch+json', + patch: true, + wait: true, + success: function () { + view.saveSucceeded(); + }, + error: function (model, xhr) { + view.showErrorMessage(xhr); + } + }; + this.showInProgressMessage(); + this.model.save(attributes, _.extend(defaultOptions, options)); + } }, saveSucceeded: function () { @@ -210,6 +227,7 @@ }, finishEditing: function() { + if (this.persistChanges === false) {return;} if (this.fieldValue() !== this.modelValue()) { this.saveValue(); } else { @@ -219,6 +237,14 @@ this.showDisplayMode(true); } } + }, + + highlightFieldOnError: function () { + this.$el.addClass('error'); + }, + + unhighlightField: function () { + this.$el.removeClass('error'); } }); @@ -226,7 +252,7 @@ fieldType: 'readonly', - templateSelector: '#field_readonly-tpl', + fieldTemplate: field_readonly_template, initialize: function (options) { this._super(options); @@ -259,7 +285,7 @@ fieldType: 'text', - templateSelector: '#field_text-tpl', + fieldTemplate: field_text_template, events: { 'change input': 'saveValue' @@ -302,7 +328,7 @@ fieldType: 'dropdown', - templateSelector: '#field_dropdown-tpl', + fieldTemplate: field_dropdown_template, events: { 'click': 'startEditing', @@ -421,7 +447,7 @@ fieldType: 'textarea', - templateSelector: '#field_textarea-tpl', + fieldTemplate: field_textarea_template, events: { 'click .wrapper-u-field': 'startEditing', @@ -451,6 +477,7 @@ mode: this.mode, value: value, message: this.helpMessage, + messagePosition: this.options.messagePosition || 'footer', placeholderValue: this.options.placeholderValue })); this.delegateEvents(); @@ -472,6 +499,7 @@ }, adjustTextareaHeight: function() { + if (this.persistChanges === false) {return;} var textarea = this.$('textarea'); textarea.css('height', 'auto').css('height', textarea.prop('scrollHeight') + 10); }, @@ -520,7 +548,7 @@ fieldType: 'link', - templateSelector: '#field_link-tpl', + fieldTemplate: field_link_template, events: { 'click a': 'linkClicked' @@ -553,7 +581,7 @@ fieldType: 'image', - templateSelector: '#field_image-tpl', + fieldTemplate: field_image_template, uploadButtonSelector: '.upload-button-input', titleAdd: gettext("Upload an image"), diff --git a/lms/static/sass/_developer.scss b/lms/static/sass/_developer.scss index 55191cd0fa..ded26c5fd7 100644 --- a/lms/static/sass/_developer.scss +++ b/lms/static/sass/_developer.scss @@ -51,6 +51,20 @@ text-align: center; } +// Below divider rules are moved here from _instructor_2.scss +// UI: visual dividers +.divider-lv0 { + border-top: ($baseline/5) solid $gray-l4; +} + +.divider-lv1 { + border-top: ($baseline/10) solid $gray-l4; +} + +.divider-lv2 { + border-top: ($baseline/20) solid $gray-l4; +} + // for verify_student/make_payment_step.underscore .payment-buttons { diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 099d36fe31..34e783c2b3 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -120,19 +120,6 @@ } } } - - // UI: visual dividers - .divider-lv0 { - border-top: ($baseline/5) solid $gray-l4; - } - - .divider-lv1 { - border-top: ($baseline/10) solid $gray-l4; - } - - .divider-lv2 { - border-top: ($baseline/20) solid $gray-l4; - } } // instructor dashboard 2 diff --git a/lms/static/sass/views/_teams.scss b/lms/static/sass/views/_teams.scss index 10d3be9c3e..1091dabfdb 100644 --- a/lms/static/sass/views/_teams.scss +++ b/lms/static/sass/views/_teams.scss @@ -472,4 +472,160 @@ padding-left: 2%; } } + + .team-actions { + @extend %ui-well; + margin: 20px 1.2%; + color: $gray; + text-align: center; + + .title { + @extend %t-title6; + @extend %t-strong; + margin-bottom: ($baseline/2); + text-align: inherit; + color: inherit; + } + + .copy { + text-align: inherit; + color: inherit; + } + } +} + +.teams-content { + + .teams-main { + + .team-edit-fields { + @include clearfix(); + + .team-required-fields { + @include float(left); + width: 55%; + border-right: 2px solid $gray-l4;; + + .u-field.u-field-name { + padding-bottom: $baseline; + + .u-field-value { + display: block; + width: 90%; + + input { + border-radius: ($baseline/5); + height: ($baseline*2); + } + } + + .u-field-message { + @include padding-left(0); + padding-top: ($baseline/4); + width: 100%; + } + } + + .u-field.u-field-description { + + .u-field-value { + display: block; + width: 100%; + + textarea { + height: ($baseline*5); + width: 90%; + border-radius: ($baseline/5) + } + } + + .u-field-message { + display: block; + @extend %t-copy-sub1; + @include padding-left(0); + margin-top: ($baseline/4); + color: $gray-l1; + width: 90%; + } + } + + .u-field-title { + padding-bottom: ($baseline/4); + color: $base-font-color; + width: 40%; + } + } + + .team-optional-fields { + @include float(left); + @include margin-left($baseline); + width: 40%; + + .u-field.u-field-optional_description { + margin-bottom: ($baseline/2); + + .u-field-title { + color: $base-font-color; + font-weight: $font-semibold; + margin-bottom: ($baseline/5); + width: 100%; + } + + .u-field-value { + display: none; + } + } + + .u-field.u-field-language { + margin-bottom: ($baseline/5); + } + + .u-field-value-display { + display: none; + } + + .u-field-value { + width: 90%; + } + + .u-field-title { + display: block; + color: $base-font-color; + width: 35%; + } + + .u-field-message { + @include padding-left(0); + width: 95%; + } + } + + } + + .u-field { + padding: 0; + } + + .u-field.error { + input, textarea { + border-color: $error-red; + } + + .u-field-message-help, .u-field-description-message { + color: $error-red !important; + } + } + + .create-team.wrapper-msg { + margin: 0 0 $baseline 0; + } + } +} + +.form-instructions { + margin: ($baseline/2) 0 $baseline 0; +} + +.create-team.form-actions { + margin-top: $baseline; } diff --git a/lms/templates/fields/field_textarea.underscore b/lms/templates/fields/field_textarea.underscore index 7b11065709..aa0d7489f2 100644 --- a/lms/templates/fields/field_textarea.underscore +++ b/lms/templates/fields/field_textarea.underscore @@ -1,18 +1,30 @@
- - - <%- message %> - + <% if (messagePosition === 'header') { %> + + + <%- message %> + + <% }%>
<% - if (mode === 'edit') { - %><% - } else { + var textareaDescribedBy = (message ? 'u-field-message-help-' : 'u-field-placeholder-value-') + id; + if (mode === 'edit') {%> + + <% } else { %><%- screenReaderTitle %><%- value %><%- gettext('Click to edit') %><% } %><%- placeholderValue %>
+ +