Commit 69450fce by muhammad-ammar

create a new team

TNL-1899
parent 56da2852
......@@ -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
......@@ -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()
......@@ -6,7 +6,7 @@
define(['backbone'], function (Backbone) {
var Team = Backbone.Model.extend({
defaults: {
id: '',
id: null,
name: '',
is_active: null,
course_id: '',
......
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('<div class="teams-content"></div>');
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');
});
});
});
define([
'jquery',
'backbone',
'teams/js/views/team_actions'
], function ($, Backbone, TeamActionsView) {
'use strict';
describe('TeamActions', function () {
var teamActionsView;
beforeEach(function () {
setFixtures('<div class="teams-content"></div>');
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');
});
});
});
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();
});
......
;(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);
;(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': '<a class="browse-teams" href="">',
'search_span_start': '<a class="search-team-descriptions" href="">',
'create_span_start': '<a class="create-team" href="">',
'span_end': '</a>'
}
);
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);
;(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(
$('<button class="action action-primary">' + gettext('Create new team') + '</button>')
);
var teamActionsView = new TeamActionsView({
teamParams: this.teamParams
});
this.$el.append(teamActionsView.$el);
teamActionsView.render();
return this;
}
});
......
......@@ -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);
});
......@@ -97,6 +102,35 @@
},
/**
* 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.
*/
browseTopic: function (topicID) {
......@@ -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
})
});
......
<div class="create-team wrapper-msg is-incontext urgency-low warning is-hidden" tabindex="-1">
<div class="msg">
<div class="msg-content">
<h3 class="title"><%- gettext("Your team could not be created!") %></h3>
<span class="screen-reader-message sr"></span>
<div class="copy">
<p></p>
</div>
</div>
</div>
</div>
<div class="form-instructions create-team-instructions">
<p class="copy">
<%- gettext("Enter information to describe your team. You cannot change these details after you create the team.") %></p>
</div>
<div class="team-edit-fields">
<div class="team-required-fields">
</div>
<div class="team-optional-fields">
</div>
</div>
<div class="create-team form-actions">
<button class="action action-primary">
<%=
interpolate_text(
_.escape(gettext("{primaryButtonTitle} {span_start}a new team{span_end}")),
{
'primaryButtonTitle': primaryButtonTitle, 'span_start': '<span class="sr">', 'span_end': '</span>'
}
)
%>
</button>
<button class="action action-cancel">
<%=
interpolate_text(
_.escape(gettext("Cancel {span_start}a new team{span_end}")),
{
'span_start': '<span class="sr">', 'span_end': '</span>'
}
)
%>
</button>
</div>
<div class="team-actions">
<h3 class="title"><%- gettext("Are you having trouble finding a team to join?") %></h3>
<p class="copy"><%= message %></p>
</div>
\ No newline at end of file
......@@ -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) }
});
</%static:require_module>
</%block>
......@@ -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)
......
......@@ -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',
......
......@@ -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]}]});
......
......@@ -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'
})
];
......
......@@ -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
};
});
......@@ -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);
}
});
});
});
......@@ -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
})
}
]
......
......@@ -99,12 +99,13 @@
},
saveValue: function () {
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({
......
......@@ -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'
})
];
......
;(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,6 +173,7 @@
},
saveAttributes: function (attributes, options) {
if (this.persistChanges === true) {
var view = this;
var defaultOptions = {
contentType: 'application/merge-patch+json',
......@@ -172,6 +188,7 @@
};
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"),
......
......@@ -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 {
......
......@@ -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
......
......@@ -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;
}
<div class="wrapper-u-field">
<div class="u-field-header">
<label class="u-field-title" for="u-field-textarea-<%- id %>" id="u-field-title-<%- id %>" aria-describedby="u-field-message-help-<%- id %>"></label>
<% if (messagePosition === 'header') { %>
<span class="u-field-message" id="u-field-message-<%- id %>">
<span class="u-field-message-notification" aria-live="polite"></span>
<span class="u-field-message-help" id="u-field-message-help-<%- id %>"> <%- message %></span>
</span>
<% }%>
</div>
<div class="u-field-value" id="u-field-value-<%- id %>" aria-labelledby="u-field-title-<%- id %>"><%
if (mode === 'edit') {
%><textarea id="u-field-textarea-<%- id %>" rows="4" aria-describedby="u-field-placeholder-value-<%- id %>"><%- value %></textarea><%
} else {
var textareaDescribedBy = (message ? 'u-field-message-help-' : 'u-field-placeholder-value-') + id;
if (mode === 'edit') {%>
<textarea id="u-field-textarea-<%- id %>" rows="4" aria-describedby="<%- textareaDescribedBy %>"><%- value %></textarea>
<% } else {
%><a href="#"><span class="sr"><%- screenReaderTitle %></span><span class="u-field-value-readonly" aria-hidden="false" aria-describedby="u-field-placeholder-value-<%- id %>"><%- value %></span><span class="sr"><%- gettext('Click to edit') %></span></a><%
}
%><span class="sr" id="u-field-placeholder-value-<%- id %>"><%- placeholderValue %></span>
</div>
<div class="u-field-footer">
<% if (messagePosition === 'footer') { %>
<span class="u-field-message" id="u-field-message-<%- id %>">
<span class="u-field-message-notification" aria-live="polite"></span>
<span class="u-field-message-help" id="u-field-message-help-<%- id %>"> <%- message %></span>
</span>
<% } %>
</div>
</div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment