Skip to content
This repository was archived by the owner on Mar 4, 2025. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 9e916e2

Browse files
author
vikasrohit
committedDec 16, 2015
SUP-2728, Add iOS community to Onboarding page (Skill picker)
-- Added communities section -- Communities section will be visible only if there exists at least one community for which the member is not registered yet -- Added unit tests
1 parent bc99f00 commit 9e916e2

File tree

6 files changed

+475
-51
lines changed

6 files changed

+475
-51
lines changed
 

‎app/skill-picker/skill-picker.controller.js

Lines changed: 143 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,123 @@
33

44
angular.module('tc.skill-picker').controller('SkillPickerController', SkillPickerController);
55

6-
SkillPickerController.$inject = ['CONSTANTS', 'ProfileService', '$state', 'userProfile', 'featuredSkills', '$log', 'toaster'];
6+
SkillPickerController.$inject = ['$scope', 'CONSTANTS', 'ProfileService', '$state', 'userProfile', 'featuredSkills', '$log', 'toaster', 'MemberCertService', '$q'];
77

8-
function SkillPickerController(CONSTANTS, ProfileService, $state, userProfile, featuredSkills, $log, toaster) {
8+
function SkillPickerController($scope, CONSTANTS, ProfileService, $state, userProfile, featuredSkills, $log, toaster, MemberCertService, $q) {
99
var vm = this;
1010
$log = $log.getInstance("SkillPickerController");
1111
vm.ASSET_PREFIX = CONSTANTS.ASSET_PREFIX;
12+
vm.IOS_PROGRAM_ID = CONSTANTS.SWIFT_PROGRAM_ID;
1213
vm.submitSkills = submitSkills;
1314
vm.featuredSkills = featuredSkills;
15+
vm.userId = userProfile.userId;
1416
vm.username = userProfile.handle;
1517
vm.toggleSkill = toggleSkill;
1618
vm.tracks = {};
1719
vm.mySkills = [];
1820
vm.disableDoneButton = false;
21+
vm.showCommunity = false;
22+
vm.loadingCommunities = false;
23+
vm.communities = {};
24+
vm.isPageDirty = isPageDirty;
1925
///////
2026
activate();
2127

28+
/**
29+
* Activates the controller.
30+
*/
2231
function activate() {
23-
$log.debug("init")
32+
$log.debug("init");
33+
initCommunities();
34+
checkCommunityStatus();
2435
}
2536

37+
/**
38+
* Verfies if the page state has been modified by the user in any way.
39+
*/
40+
function isPageDirty() {
41+
return isTracksDirty() || isCommunitiesDirty();
42+
}
43+
44+
/**
45+
* Verfies if the tracks section state has been modified by the user in any way.
46+
*/
47+
function isTracksDirty() {
48+
return vm.tracks.DESIGN || vm.tracks.DEVELOP || vm.tracks.DATA_SCIENCE;
49+
}
50+
51+
/**
52+
* Verfies if the communities section state has been modified by the user in any way.
53+
*/
54+
function isCommunitiesDirty() {
55+
var community = _.findWhere(vm.communities, {dirty: true});
56+
return !!community;
57+
}
58+
59+
/**
60+
* Initializes the communities to show in the communities section.
61+
*/
62+
function initCommunities() {
63+
vm.communities['ios'] = { displayName: 'iOS', programId: vm.IOS_PROGRAM_ID, status: false, dirty: false, display: true};
64+
_addWatchToCommunity(vm.communities['ios']);
65+
}
66+
67+
/**
68+
* Helper method to add watch to given object.
69+
*/
70+
function _addWatchToCommunity(community) {
71+
community.unregister = $scope.$watch(
72+
function() { return community; },
73+
function(newValue, oldValue) {
74+
if (oldValue && newValue.status !== oldValue.status) {
75+
newValue.dirty = oldValue.dirty ? false : true;
76+
}
77+
},
78+
true
79+
);
80+
}
81+
82+
/**
83+
* Checks registration status of each community and updates the state of each community.
84+
*/
85+
function checkCommunityStatus() {
86+
var promises = [];
87+
for (var name in vm.communities) {
88+
var community = vm.communities[name];
89+
promises.push(MemberCertService.getMemberRegistration(vm.userId, community.programId));
90+
}
91+
vm.loadingCommunities = true;
92+
$q.all(promises).then(function(responses) {
93+
vm.loadingCommunities = false;
94+
responses.forEach(function(program) {
95+
if (program) {
96+
var community = _.findWhere(vm.communities, {programId: program.eventId});
97+
if (community) {
98+
// set display false to avoid showing already enabled/registered program
99+
// we expect display property to be modified after first load of the page
100+
community.display = false;
101+
community.status = true;
102+
if (community.unregister){
103+
community.unregister();
104+
_addWatchToCommunity(community);
105+
}
106+
}
107+
}
108+
});
109+
// if there exists at least 1 community which can be displayed, set showCommunity flag to true
110+
var community = _.findWhere(vm.communities, {display: true});
111+
if (community) {
112+
vm.showCommunity = true;
113+
}
114+
})
115+
.catch(function(error) {
116+
vm.loadingCommunities = false;
117+
});
118+
}
119+
120+
/**
121+
* Toggles the given skill for the user. If it is not added, adds it and if already added, removes it.
122+
*/
26123
function toggleSkill(tagId) {
27124
var _idx = vm.mySkills.indexOf(tagId.toString());
28125
if (_idx > -1) {
@@ -34,50 +131,56 @@
34131
}
35132
}
36133

134+
/**
135+
* Persists the user's altered information.
136+
*/
37137
function submitSkills() {
38138

39139
vm.saving = true;
40-
// save tracks
41-
userProfile.tracks = _.reduce(vm.tracks, function(result, isInterested, trackName) {
42-
if (isInterested) {
43-
result.push(trackName);
44-
}
45-
return result;
46-
}, []);
47140

48-
userProfile.save().then(function(data) {
49-
if (vm.mySkills.length > 0) {
50-
// save skills
51-
var data = {};
52-
for (var i = 0; i < vm.mySkills.length; i++) {
53-
data[vm.mySkills[i]] = {
54-
hidden: false
55-
};
141+
var promises = [];
142+
if (isTracksDirty()) {
143+
// save tracks
144+
userProfile.tracks = _.reduce(vm.tracks, function(result, isInterested, trackName) {
145+
if (isInterested) {
146+
result.push(trackName);
147+
}
148+
return result;
149+
}, []);
150+
promises.push(userProfile.save());
151+
}
152+
if (vm.mySkills.length > 0) {
153+
// save skills
154+
var data = {};
155+
for (var i = 0; i < vm.mySkills.length; i++) {
156+
data[vm.mySkills[i]] = {
157+
hidden: false
158+
};
159+
}
160+
promises.push(ProfileService.updateUserSkills(vm.username, data));
161+
}
162+
$log.debug('isCommunitiesDirty: ' + isCommunitiesDirty());
163+
if (isCommunitiesDirty()) {
164+
for(var communityName in vm.communities) {
165+
var community = vm.communities[communityName];
166+
if (community.dirty === true) {
167+
if (community.status === true) {
168+
promises.push(MemberCertService.registerMember(vm.userId, community.programId));
56169
}
57-
ProfileService.updateUserSkills(vm.username, data)
58-
.then(function(resp) {
59-
vm.saving = false;
60-
toaster.pop('success', "Success!", "Your skills have been updated.");
61-
vm.disableDoneButton = true;
62-
$state.go('dashboard');
63-
})
64-
.catch(function(data) {
65-
vm.saving = false;
66-
toaster.pop('error', "Whoops", "Something went wrong. Please try again later.");
67-
})
68-
} else {
69-
vm.saving = false;
70-
toaster.pop('success', "Success!", "Your skills have been updated.");
71-
vm.disableDoneButton = true;
72-
$state.go('dashboard');
73170
}
171+
}
172+
}
74173

75-
})
76-
.catch(function(resp) {
77-
vm.saving = false;
78-
toaster.pop('error', "Whoops", "Something went wrong. Please try again later.");
79-
})
80-
174+
$q.all(promises).then(function(responses) {
175+
vm.saving = false;
176+
toaster.pop('success', "Success!", "Your skills have been updated.");
177+
vm.disableDoneButton = true;
178+
//$state.go('dashboard');
179+
})
180+
.catch(function(resp) {
181+
vm.saving = false;
182+
toaster.pop('error', "Whoops!", "Something went wrong. Please try again later.");
183+
});
81184
}
82185
}
83186
})();

‎app/skill-picker/skill-picker.jade

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@
33

44
p.instruction Hi {{vm.username}}! Your account is now active. To help other members get to know you, select the tracks in which you're interested, and specify some of your skills. You can edit this information later on your Profile.
55

6+
.communities(ng-show="!vm.loadingCommunities && vm.showCommunity")
7+
.communities-title Communities
8+
.communities-description Topcoder regularly establishes exclusive communities to help members develop expertise and earn money in particular technologies. Join a featured community to be notified of events and challenges.
9+
.communities-list
10+
.community(ng-repeat="(communityKey, community) in vm.communities", ng-class="{'disabled': !community.status}", ng-if="community.display")
11+
.community-details
12+
.community-icon(ng-class="{'disabled': !community.status}")
13+
img(ng-if="communityKey == 'ios' && community.status", src="/images/ico-ios-community.svg")
14+
img(ng-if="communityKey == 'ios' && !community.status", src="/images/ico-ios-community-grey.svg")
15+
16+
.community-text
17+
span.community-title(class="{{!community.status && 'disabled'}}") {{community.displayName}}
18+
.community-description
19+
span(ng-if="communityKey == 'ios'") Mobile app design and development for iOS, with Swift emphasis
20+
21+
onoff-switch(model="community.status", unique-id="'community-' + communityKey")
22+
23+
24+
625
.tracks-container
726
.title tracks
827
.description Topcoder's three categories of challenges… please pick at least one based on your skills and interests.
@@ -51,4 +70,4 @@
5170
type="button",
5271
tc-busy-button, tc-busy-when="vm.saving",
5372
ng-click="vm.submitSkills()",
54-
ng-disabled="vm.disableDoneButton || (!vm.tracks.DESIGN && !vm.tracks.DEVELOP && !vm.tracks.DATA_SCIENCE)") Done
73+
ng-disabled="vm.disableDoneButton || !vm.isPageDirty()") Done

‎app/skill-picker/skill-picker.spec.js

Lines changed: 215 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,224 @@
11
/* jshint -W117, -W030 */
22
describe('Skill Picker Controller', function() {
33
var vm;
4+
var toasterSvc, memberCertService, profileService;
5+
var mockProfile = mockData.getMockProfile();
46

5-
// beforeEach(function() {
6-
// bard.appModule('tc.skill-picker');
7-
// bard.inject(this, '$controller', '$rootScope', '$q', 'userProfile');
7+
beforeEach(function() {
8+
bard.appModule('tc.skill-picker');
9+
bard.inject(this, '$controller', '$rootScope', '$q', 'MemberCertService', 'ProfileService', 'toaster', 'CONSTANTS');
810

9-
// vm = $controller('SkillPickerController', {
10-
// });
11-
// });
11+
memberCertService = MemberCertService;
12+
profileService = ProfileService;
13+
var swiftProgramId = CONSTANTS.SWIFT_PROGRAM_ID;
1214

13-
// bard.verifyNoOutstandingHttpRequests();
15+
// mock member cert api
16+
sinon.stub(memberCertService, 'getMemberRegistration', function(userId, programId) {
17+
var deferred = $q.defer();
18+
if (programId === swiftProgramId) {
19+
var resp = {eventId: swiftProgramId, userId: 12345};
20+
deferred.resolve(resp);
21+
} else {
22+
deferred.resolve();
23+
}
24+
return deferred.promise;
25+
});
26+
27+
sinon.stub(memberCertService, 'registerMember', function(userId, programId) {
28+
var deferred = $q.defer();
29+
var resp = {eventId: programId, userId: 12345};
30+
if (programId === 100) {
31+
deferred.reject();
32+
} else {
33+
deferred.resolve(resp);
34+
}
35+
return deferred.promise;
36+
});
1437

15-
// it('should be created successfully', function() {
16-
// expect(vm).to.exist;
17-
// });
38+
// adds spy method to mocked profile object for saving the profile
39+
mockProfile.save = sinon.spy();
40+
41+
// mock profile service
42+
sinon.stub(profileService, 'updateUserSkills', function(username, skills) {
43+
var deferred = $q.defer();
44+
var resp = {};
45+
if (skills[412]) {
46+
deferred.reject();
47+
} else {
48+
deferred.resolve(resp);
49+
}
50+
return deferred.promise;
51+
});
52+
53+
// mocks the toaster service
54+
toasterSvc = toaster;
55+
bard.mockService(toaster, {
56+
pop: $q.when(true),
57+
default: $q.when(true)
58+
});
59+
60+
var scope = $rootScope.$new();
61+
vm = $controller('SkillPickerController', {
62+
$scope: scope,
63+
userProfile: mockProfile,
64+
featuredSkills: []
65+
});
66+
$rootScope.$digest();
67+
});
68+
69+
bard.verifyNoOutstandingHttpRequests();
70+
71+
it('should be created successfully', function() {
72+
expect(vm).to.exist;
73+
expect(vm.showCommunity).to.exist.to.false;
74+
});
75+
76+
it('should have empty tracks object ', function() {
77+
expect(vm.tracks).to.exist;
78+
expect(vm.tracks.DESIGN).not.to.exist;
79+
expect(vm.tracks.DEVELOP).not.to.exist;
80+
expect(vm.tracks.DATA_SCIENCE).not.to.exist;
81+
});
82+
83+
it('should have correct userIdentity ', function() {
84+
expect(vm.userId).to.exist.to.equal(mockProfile.userId);
85+
expect(vm.username).to.exist.to.equal(mockProfile.handle);
86+
});
87+
88+
it('should not have page dirty ', function() {
89+
var dirty = vm.isPageDirty();
90+
expect(dirty).to.equal(false);
91+
});
92+
93+
it('should be created successfully with showCommunity being true', function() {
94+
// backup original swift program id, to restore after test execution
95+
var origSwiftProgId = CONSTANTS.SWIFT_PROGRAM_ID;
96+
// update swift program id to something which says, member is not registered
97+
CONSTANTS.SWIFT_PROGRAM_ID = 3446;
98+
vm = $controller('SkillPickerController', {
99+
$scope: $rootScope.$new(),
100+
userProfile: mockProfile,
101+
featuredSkills: []
102+
});
103+
$rootScope.$digest();
104+
expect(vm).to.exist;
105+
// showCommunity flag should be set to true because we have at least one unregistered community
106+
expect(vm.showCommunity).to.exist.to.true;
107+
// restores the original swift program id
108+
CONSTANTS.SWIFT_PROGRAM_ID = origSwiftProgId;
109+
});
110+
111+
it('should add skill ', function() {
112+
vm.toggleSkill(409);
113+
expect(vm.mySkills).to.exist.have.length(1);
114+
});
115+
116+
it('should remove added skill ', function() {
117+
vm.toggleSkill(409);
118+
// should add the skill
119+
expect(vm.mySkills).to.exist.have.length(1);
120+
// next call to toggleSkill with same argument, should remove that skill
121+
vm.toggleSkill(409);
122+
// should remove the skill
123+
expect(vm.mySkills).to.exist.have.length(0);
124+
});
125+
126+
it('should not make any update call without any change ', function() {
127+
vm.submitSkills();
128+
$rootScope.$digest();
129+
expect(mockProfile.save).not.to.be.called;
130+
expect(profileService.updateUserSkills).not.to.be.called;
131+
expect(memberCertService.registerMember).not.to.be.called;
132+
});
133+
134+
it('should update tracks for the member ', function() {
135+
vm.tracks.DEVELOP = true;
136+
vm.tracks.DESIGN = false;
137+
vm.submitSkills();
138+
$rootScope.$digest();
139+
expect(mockProfile.save).to.be.calledOnce;
140+
expect(profileService.updateUserSkills).not.to.be.called;
141+
expect(memberCertService.registerMember).not.to.be.called;
142+
});
143+
144+
it('should show error popup for updating tracks ', function() {
145+
vm.tracks.DEVELOP = true;
146+
// stubs the save method to reject the promise
147+
mockProfile.save = function() {};
148+
sinon.stub(mockProfile, 'save', function() {
149+
var deferred = $q.defer();
150+
deferred.reject();
151+
return deferred.promise;
152+
});
153+
vm.submitSkills();
154+
$rootScope.$digest();
155+
expect(mockProfile.save).to.be.calledOnce;
156+
expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('wrong')).calledOnce;
157+
expect(profileService.updateUserSkills).not.to.be.called;
158+
expect(memberCertService.registerMember).not.to.be.called;
159+
});
160+
161+
it('should update skills for the member ', function() {
162+
vm.mySkills = [409, 410];
163+
vm.submitSkills();
164+
$rootScope.$digest();
165+
expect(mockProfile.save).not.to.be.called;
166+
expect(profileService.updateUserSkills).to.be.calledOnce;
167+
expect(memberCertService.registerMember).not.to.be.called;
168+
});
169+
170+
it('should show error popup for error in updating skills ', function() {
171+
vm.mySkills = [409, 410, 412];
172+
vm.submitSkills();
173+
$rootScope.$digest();
174+
expect(mockProfile.save).not.to.be.called;
175+
expect(profileService.updateUserSkills).to.be.calledOnce;
176+
expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('wrong')).calledOnce;
177+
expect(memberCertService.registerMember).not.to.be.called;
178+
});
179+
180+
it('should update communities for the member ', function() {
181+
vm.communities['ios'].status = true;
182+
vm.communities['ios'].dirty = true;
183+
vm.submitSkills();
184+
$rootScope.$digest();
185+
expect(mockProfile.save).not.to.be.called;
186+
expect(profileService.updateUserSkills).not.to.be.called;
187+
expect(memberCertService.registerMember).to.be.calledOnce;
188+
});
189+
190+
// we may need to update this test case when we want to call unregister endpoint
191+
it('should NOT update communities for the member for disabled community ', function() {
192+
vm.communities['ios'].status = false;
193+
vm.communities['ios'].dirty = true;
194+
vm.submitSkills();
195+
$rootScope.$digest();
196+
expect(mockProfile.save).not.to.be.called;
197+
expect(profileService.updateUserSkills).not.to.be.called;
198+
expect(memberCertService.registerMember).not.to.be.called;
199+
});
200+
201+
it('should NOT update communities for the member for enabled but non dirty community ', function() {
202+
// TODO need one more community, with dirty true, to test out the non dirty community update
203+
vm.communities['ios'].status = true;
204+
vm.communities['ios'].dirty = false;
205+
vm.submitSkills();
206+
$rootScope.$digest();
207+
expect(mockProfile.save).not.to.be.called;
208+
expect(profileService.updateUserSkills).not.to.be.called;
209+
expect(memberCertService.registerMember).not.to.be.called;
210+
});
211+
212+
it('should show error popup for error in updating communities ', function() {
213+
vm.communities['ios'].programId = 100;// to cause rejction of the promise
214+
vm.communities['ios'].status = true;
215+
vm.communities['ios'].dirty = true;
216+
vm.submitSkills();
217+
$rootScope.$digest();
218+
expect(mockProfile.save).not.to.be.called;
219+
expect(profileService.updateUserSkills).not.to.be.called;
220+
expect(memberCertService.registerMember).to.be.calledOnce;
221+
expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('wrong')).calledOnce;
222+
});
18223

19224
});

‎assets/css/skill-picker/skill-picker.scss

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,67 @@
3030
color: $accent-gray;
3131
}
3232

33+
.communities {
34+
width: 100%;
35+
margin-bottom: 30px;
36+
.communities-title {
37+
@include sofia-pro-regular;
38+
text-transform: uppercase;
39+
font-size: 18px;
40+
padding-bottom: 10px;
41+
}
42+
.communities-description {
43+
@include font-with-weight('Merriweather Sans');
44+
color: #A3A3AE;
45+
font-size: 13px;
46+
line-height: 18px;
47+
}
48+
.communities-list {
49+
margin-top: 10px;
50+
.community {
51+
display: flex;
52+
flex-direction: row;
53+
justify-content: space-between;
54+
align-items: center;
55+
padding: 15px 0;
56+
border-bottom: 1px solid $gray-light;
57+
58+
&.disabled {
59+
background-color: $gray-lightest;
60+
}
61+
62+
.community-details {
63+
display: flex;
64+
flex-direction: row;
65+
align-items: center;
66+
padding-left: 10px;
67+
}
68+
69+
.community-text {
70+
margin-left: 10px;
71+
}
72+
73+
.community-title {
74+
font-size: 16px;
75+
line-height: 28px;
76+
@include font-with-weight('Sofia Pro', 500);
77+
transition: .1s color;
78+
79+
&.disabled {
80+
color: $gray-dark;
81+
}
82+
}
83+
84+
.community-description {
85+
@include font-with-weight('Merriweather Sans', 400);
86+
font-size: 13px;
87+
margin-top: 4px;
88+
color: $accent-gray;
89+
}
90+
}
91+
}
92+
}
93+
3394
.tracks-container {
3495
width: 100%;
3596
>.title {
Lines changed: 18 additions & 0 deletions
Loading

‎assets/images/ico-ios-community.svg

Lines changed: 18 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)
This repository has been archived.