Merge pull request #8513 from edx/benmcmorran/topic-card
TNL-1893 Add teams topic card
This commit is contained in:
17
lms/djangoapps/teams/static/teams/js/models/topic.js
Normal file
17
lms/djangoapps/teams/static/teams/js/models/topic.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Model for a topic.
|
||||
*/
|
||||
(function (define) {
|
||||
'use strict';
|
||||
define(['backbone'], function (Backbone) {
|
||||
var Topic = Backbone.Model.extend({
|
||||
defaults: {
|
||||
name: '',
|
||||
description: '',
|
||||
team_count: 0,
|
||||
id: ''
|
||||
}
|
||||
});
|
||||
return Topic;
|
||||
})
|
||||
}).call(this, define || RequireJS.define);
|
||||
37
lms/djangoapps/teams/static/teams/js/spec/topic_card_spec.js
Normal file
37
lms/djangoapps/teams/static/teams/js/spec/topic_card_spec.js
Normal file
@@ -0,0 +1,37 @@
|
||||
define(['jquery',
|
||||
'underscore',
|
||||
'teams/js/views/topic_card',
|
||||
'teams/js/models/topic'],
|
||||
function ($, _, TopicCardView, Topic) {
|
||||
|
||||
describe('topic card view', function () {
|
||||
var view;
|
||||
|
||||
beforeEach(function () {
|
||||
spyOn(TopicCardView.prototype, 'action');
|
||||
view = new TopicCardView({
|
||||
model: new Topic({
|
||||
'id': 'renewables',
|
||||
'name': 'Renewable Energy',
|
||||
'description': 'Explore how changes in <ⓡⓔⓝⓔⓦⓐⓑⓛⓔ> ʎƃɹǝuǝ will affect our lives.',
|
||||
'team_count': 34
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('can render itself', function () {
|
||||
expect(view.$el).toHaveClass('square-card');
|
||||
expect(view.$el.find('.card-title').text()).toContain('Renewable Energy');
|
||||
expect(view.$el.find('.card-description').text()).toContain('changes in <ⓡⓔⓝⓔⓦⓐⓑⓛⓔ> ʎƃɹǝuǝ');
|
||||
expect(view.$el.find('.card-meta-details').text()).toContain('34 Teams');
|
||||
expect(view.$el.find('.action').text()).toContain('View');
|
||||
});
|
||||
|
||||
it('navigates when action button is clicked', function () {
|
||||
view.$el.find('.action').trigger('click');
|
||||
// TODO test actual navigation once implemented
|
||||
expect(view.action).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
49
lms/djangoapps/teams/static/teams/js/views/topic_card.js
Normal file
49
lms/djangoapps/teams/static/teams/js/views/topic_card.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* View for a topic card. Displays a Topic.
|
||||
*/
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
define(['backbone', 'underscore', 'gettext', 'js/components/card/views/card'],
|
||||
function (Backbone, _, gettext, CardView) {
|
||||
var TeamCountDetailView = Backbone.View.extend({
|
||||
tagName: 'p',
|
||||
className: 'team-count',
|
||||
|
||||
initialize: function () {
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var team_count = this.model.get('team_count');
|
||||
this.$el.html(_.escape(interpolate(
|
||||
ngettext('%(team_count)s Team', '%(team_count)s Teams', team_count),
|
||||
{team_count: team_count},
|
||||
true
|
||||
)));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
var TopicCardView = CardView.extend({
|
||||
initialize: function () {
|
||||
this.detailViews = [new TeamCountDetailView({ model: this.model })];
|
||||
CardView.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
action: function (event) {
|
||||
event.preventDefault();
|
||||
// TODO implement actual navigation
|
||||
},
|
||||
|
||||
configuration: 'square_card',
|
||||
cardClass: 'topic-card',
|
||||
title: function () { return this.model.get('name'); },
|
||||
description: function () { return this.model.get('description'); },
|
||||
details: function () { return this.detailViews; },
|
||||
actionClass: 'action-view',
|
||||
actionContent: _.escape(gettext('View')) + ' <span class="icon fa-arrow-right"></span>'
|
||||
});
|
||||
|
||||
return TopicCardView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
99
lms/static/js/components/card/views/card.js
Normal file
99
lms/static/js/components/card/views/card.js
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* A generic card view class.
|
||||
*
|
||||
* Subclasses can override any of the following:
|
||||
* - configuration (string or function): Sets the display style of the card as square or list. Can take values of
|
||||
* "square_card" or "list_card". Defaults to "square_card".
|
||||
* - action (function): Action to take when the action button is clicked. Defaults to a no-op.
|
||||
* - cardClass (string or function): Class name for this card's DOM element. Defaults to the empty string.
|
||||
* - title (string or function): Title of the card. Defaults to the empty string.
|
||||
* - description (string or function): Description of the card. Defaults to the empty string.
|
||||
* - details (array or function): Array of child views to be rendered as details of this card. The class "meta-detail"
|
||||
* is automatically added to each rendered child view to ensure appropriate styling. Defaults to an empty list.
|
||||
* - actionClass (string or function): Class name for the action DOM element. Defaults to the empty string.
|
||||
* - actionUrl (string or function): URL to navigate to when the action button is clicked. Defaults to the empty string.
|
||||
* - actionContent: Content of the action button. This may include HTML. Default to the empty string.
|
||||
*/
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
define(['jquery',
|
||||
'backbone',
|
||||
'text!templates/components/card/square_card.underscore',
|
||||
'text!templates/components/card/list_card.underscore'],
|
||||
function ($, Backbone, squareCardTemplate, listCardTemplate) {
|
||||
var CardView = Backbone.View.extend({
|
||||
events: {
|
||||
'click .action' : 'action'
|
||||
},
|
||||
|
||||
/**
|
||||
* constructor is needed in addition to initialize because of Backbone's initialization order.
|
||||
* initialize seems to run last in the initialization process, after className. However, className
|
||||
* depends on this.configuration being set to pick the appropriate class. Therefore, configuration
|
||||
* is set in the constructor, but the rest of the initialization happens in initialize.
|
||||
*/
|
||||
constructor: function (options) {
|
||||
if (!this.configuration) {
|
||||
this.configuration = (options && options.configuration) ? options.configuration : 'square_card';
|
||||
}
|
||||
Backbone.View.prototype.constructor.apply(this, arguments);
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.template = this.switchOnConfiguration(
|
||||
_.template(squareCardTemplate),
|
||||
_.template(listCardTemplate)
|
||||
);
|
||||
this.render();
|
||||
},
|
||||
|
||||
switchOnConfiguration: function (square_result, list_result) {
|
||||
return this.callIfFunction(this.configuration) === 'square_card' ?
|
||||
square_result : list_result;
|
||||
},
|
||||
|
||||
callIfFunction: function (value) {
|
||||
if ($.isFunction(value)) {
|
||||
return value.call(this);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
|
||||
className: function () {
|
||||
return 'card ' +
|
||||
this.switchOnConfiguration('square-card', 'list-card') +
|
||||
' ' + this.callIfFunction(this.cardClass);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.template({
|
||||
title: this.callIfFunction(this.title),
|
||||
description: this.callIfFunction(this.description),
|
||||
action_class: this.callIfFunction(this.actionClass),
|
||||
action_url: this.callIfFunction(this.actionUrl),
|
||||
action_content: this.callIfFunction(this.actionContent)
|
||||
}));
|
||||
var detailsEl = this.$el.find('.card-meta-details');
|
||||
_.each(this.callIfFunction(this.details), function (detail) {
|
||||
// Call setElement to rebind event handlers
|
||||
detail.setElement(detail.el).render();
|
||||
detail.$el.addClass('meta-detail');
|
||||
detailsEl.append(detail.el);
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
action: function () { },
|
||||
cardClass: '',
|
||||
title: '',
|
||||
description: '',
|
||||
details: [],
|
||||
actionClass: '',
|
||||
actionUrl: '',
|
||||
actionContent: ''
|
||||
});
|
||||
|
||||
return CardView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
73
lms/static/js/spec/components/card/card_spec.js
Normal file
73
lms/static/js/spec/components/card/card_spec.js
Normal file
@@ -0,0 +1,73 @@
|
||||
(function (define) {
|
||||
'use strict';
|
||||
|
||||
define(['jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'js/components/card/views/card'
|
||||
],
|
||||
function($, _, Backbone, CardView) {
|
||||
|
||||
describe('card component view', function () {
|
||||
it('can render itself as a square card', function () {
|
||||
var view = new CardView({ configuration: 'square_card' });
|
||||
expect(view.$el).toHaveClass('square-card');
|
||||
expect(view.$el.find('.card-meta-wrapper .action').length).toBe(1);
|
||||
});
|
||||
|
||||
it('can render itself as a list card', function () {
|
||||
var view = new CardView({ configuration: 'list_card' });
|
||||
expect(view.$el).toHaveClass('list-card');
|
||||
expect(view.$el.find('.card-core-wrapper .action').length).toBe(1);
|
||||
});
|
||||
|
||||
it('can render child views', function () {
|
||||
var testChildView = new (Backbone.View.extend({ className: 'test-view' }))();
|
||||
spyOn(testChildView, 'render');
|
||||
var view = new (CardView.extend({ details: [testChildView] }))();
|
||||
expect(testChildView.render).toHaveBeenCalled();
|
||||
expect(view.$el.find('.test-view')).toHaveClass('meta-detail');
|
||||
});
|
||||
|
||||
it('calls action when clicked', function () {
|
||||
spyOn(CardView.prototype, 'action');
|
||||
var view = new CardView({ configuration: 'square_card' });
|
||||
view.$el.find('.action').trigger('click');
|
||||
expect(view.action).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
var verifyContent = function (view) {
|
||||
expect(view.$el).toHaveClass('test-card');
|
||||
expect(view.$el.find('.card-title').text()).toContain('A test title');
|
||||
expect(view.$el.find('.card-description').text()).toContain('A test description');
|
||||
expect(view.$el.find('.action')).toHaveClass('test-action');
|
||||
expect(view.$el.find('.action')).toHaveAttr('href', 'www.example.com');
|
||||
expect(view.$el.find('.action').text()).toContain('A test action');
|
||||
};
|
||||
|
||||
it('can have strings for cardClass, title, description, and action', function () {
|
||||
var view = new (CardView.extend({
|
||||
cardClass: 'test-card',
|
||||
title: 'A test title',
|
||||
description: 'A test description',
|
||||
actionClass: 'test-action',
|
||||
actionUrl: 'www.example.com',
|
||||
actionContent: 'A test action'
|
||||
}))();
|
||||
verifyContent(view);
|
||||
});
|
||||
|
||||
it('can have functions for cardClass, title, description, and action', function () {
|
||||
var view = new (CardView.extend({
|
||||
cardClass: function () { return 'test-card'; },
|
||||
title: function () { return 'A test title'; },
|
||||
description: function () { return 'A test description'; },
|
||||
actionClass: function () { return 'test-action'; },
|
||||
actionUrl: function () { return 'www.example.com'; },
|
||||
actionContent: function () { return 'A test action'; }
|
||||
}));
|
||||
verifyContent(view);
|
||||
});
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -583,8 +583,10 @@
|
||||
define([
|
||||
// Run the LMS tests
|
||||
'lms/include/teams/js/spec/teams_factory_spec.js',
|
||||
'lms/include/teams/js/spec/topic_card_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',
|
||||
'lms/include/js/spec/photocapture_spec.js',
|
||||
'lms/include/js/spec/staff_debug_actions_spec.js',
|
||||
'lms/include/js/spec/views/notification_spec.js',
|
||||
|
||||
@@ -93,11 +93,13 @@ fixture_paths:
|
||||
- templates/file-upload.underscore
|
||||
- templates/components/header
|
||||
- templates/components/tabbed
|
||||
- templates/components/card
|
||||
- js/fixtures/edxnotes
|
||||
- js/fixtures/search
|
||||
- templates/search
|
||||
- templates/discovery
|
||||
- common/templates
|
||||
- teams/templates
|
||||
|
||||
requirejs:
|
||||
paths:
|
||||
|
||||
13
lms/templates/components/card/list_card.underscore
Normal file
13
lms/templates/components/card/list_card.underscore
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="card-core-wrapper">
|
||||
<div class="card-core">
|
||||
<h3 class="card-title"><%- title %></h3>
|
||||
<p class="card-description"><%- description %></p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="action <%= action_class %>" href="<%= action_url %>"><%= action_content %></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-meta-wrapper">
|
||||
<div class="card-meta-details">
|
||||
</div>
|
||||
</div>
|
||||
13
lms/templates/components/card/square_card.underscore
Normal file
13
lms/templates/components/card/square_card.underscore
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="card-core-wrapper">
|
||||
<div class="card-core">
|
||||
<h3 class="card-title"><%- title %></h3>
|
||||
<p class="card-description"><%- description %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-meta-wrapper has-actions">
|
||||
<div class="card-meta-details">
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="action <%= action_class %>" href="<%= action_url %>"><%= action_content %></a>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user