Skip to content
This repository was archived by the owner on Mar 4, 2025. It is now read-only.

SUP-2728, Add iOS community to Onboarding page (Skill picker) #613

Merged
merged 2 commits into from
Dec 21, 2015
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 143 additions & 40 deletions app/skill-picker/skill-picker.controller.js
Original file line number Diff line number Diff line change
@@ -3,26 +3,123 @@

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

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

function SkillPickerController(CONSTANTS, ProfileService, $state, userProfile, featuredSkills, $log, toaster) {
function SkillPickerController($scope, CONSTANTS, ProfileService, $state, userProfile, featuredSkills, $log, toaster, MemberCertService, $q) {
var vm = this;
$log = $log.getInstance("SkillPickerController");
vm.ASSET_PREFIX = CONSTANTS.ASSET_PREFIX;
vm.IOS_PROGRAM_ID = CONSTANTS.SWIFT_PROGRAM_ID;
vm.submitSkills = submitSkills;
vm.featuredSkills = featuredSkills;
vm.userId = userProfile.userId;
vm.username = userProfile.handle;
vm.toggleSkill = toggleSkill;
vm.tracks = {};
vm.mySkills = [];
vm.disableDoneButton = false;
vm.showCommunity = false;
vm.loadingCommunities = false;
vm.communities = {};
vm.isPageDirty = isPageDirty;
///////
activate();

/**
* Activates the controller.
*/
function activate() {
$log.debug("init")
$log.debug("init");
initCommunities();
checkCommunityStatus();
}

/**
* Verfies if the page state has been modified by the user in any way.
*/
function isPageDirty() {
return isTracksDirty() || isCommunitiesDirty();
}

/**
* Verfies if the tracks section state has been modified by the user in any way.
*/
function isTracksDirty() {
return vm.tracks.DESIGN || vm.tracks.DEVELOP || vm.tracks.DATA_SCIENCE;
}

/**
* Verfies if the communities section state has been modified by the user in any way.
*/
function isCommunitiesDirty() {
var community = _.findWhere(vm.communities, {dirty: true});
return !!community;
}

/**
* Initializes the communities to show in the communities section.
*/
function initCommunities() {
vm.communities['ios'] = { displayName: 'iOS', programId: vm.IOS_PROGRAM_ID, status: false, dirty: false, display: true};
_addWatchToCommunity(vm.communities['ios']);
}

/**
* Helper method to add watch to given object.
*/
function _addWatchToCommunity(community) {
community.unregister = $scope.$watch(
function() { return community; },
function(newValue, oldValue) {
if (oldValue && newValue.status !== oldValue.status) {
newValue.dirty = oldValue.dirty ? false : true;
}
},
true
);
}

/**
* Checks registration status of each community and updates the state of each community.
*/
function checkCommunityStatus() {
var promises = [];
for (var name in vm.communities) {
var community = vm.communities[name];
promises.push(MemberCertService.getMemberRegistration(vm.userId, community.programId));
}
vm.loadingCommunities = true;
$q.all(promises).then(function(responses) {
vm.loadingCommunities = false;
responses.forEach(function(program) {
if (program) {
var community = _.findWhere(vm.communities, {programId: program.eventId});
if (community) {
// set display false to avoid showing already enabled/registered program
// we expect display property to be modified after first load of the page
community.display = false;
community.status = true;
if (community.unregister){
community.unregister();
_addWatchToCommunity(community);
}
}
}
});
// if there exists at least 1 community which can be displayed, set showCommunity flag to true
var community = _.findWhere(vm.communities, {display: true});
if (community) {
vm.showCommunity = true;
}
})
.catch(function(error) {
vm.loadingCommunities = false;
});
}

/**
* Toggles the given skill for the user. If it is not added, adds it and if already added, removes it.
*/
function toggleSkill(tagId) {
var _idx = vm.mySkills.indexOf(tagId.toString());
if (_idx > -1) {
@@ -34,50 +131,56 @@
}
}

/**
* Persists the user's altered information.
*/
function submitSkills() {

vm.saving = true;
// save tracks
userProfile.tracks = _.reduce(vm.tracks, function(result, isInterested, trackName) {
if (isInterested) {
result.push(trackName);
}
return result;
}, []);

userProfile.save().then(function(data) {
if (vm.mySkills.length > 0) {
// save skills
var data = {};
for (var i = 0; i < vm.mySkills.length; i++) {
data[vm.mySkills[i]] = {
hidden: false
};
var promises = [];
if (isTracksDirty()) {
// save tracks
userProfile.tracks = _.reduce(vm.tracks, function(result, isInterested, trackName) {
if (isInterested) {
result.push(trackName);
}
return result;
}, []);
promises.push(userProfile.save());
}
if (vm.mySkills.length > 0) {
// save skills
var data = {};
for (var i = 0; i < vm.mySkills.length; i++) {
data[vm.mySkills[i]] = {
hidden: false
};
}
promises.push(ProfileService.updateUserSkills(vm.username, data));
}
$log.debug('isCommunitiesDirty: ' + isCommunitiesDirty());
if (isCommunitiesDirty()) {
for(var communityName in vm.communities) {
var community = vm.communities[communityName];
if (community.dirty === true) {
if (community.status === true) {
promises.push(MemberCertService.registerMember(vm.userId, community.programId));
}
ProfileService.updateUserSkills(vm.username, data)
.then(function(resp) {
vm.saving = false;
toaster.pop('success', "Success!", "Your skills have been updated.");
vm.disableDoneButton = true;
$state.go('dashboard');
})
.catch(function(data) {
vm.saving = false;
toaster.pop('error', "Whoops", "Something went wrong. Please try again later.");
})
} else {
vm.saving = false;
toaster.pop('success', "Success!", "Your skills have been updated.");
vm.disableDoneButton = true;
$state.go('dashboard');
}
}
}

})
.catch(function(resp) {
vm.saving = false;
toaster.pop('error', "Whoops", "Something went wrong. Please try again later.");
})

$q.all(promises).then(function(responses) {
vm.saving = false;
toaster.pop('success', "Success!", "Your skills have been updated.");
vm.disableDoneButton = true;
//$state.go('dashboard');
})
.catch(function(resp) {
vm.saving = false;
toaster.pop('error', "Whoops!", "Something went wrong. Please try again later.");
});
}
}
})();
21 changes: 20 additions & 1 deletion app/skill-picker/skill-picker.jade
Original file line number Diff line number Diff line change
@@ -3,6 +3,25 @@

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.

.communities(ng-show="!vm.loadingCommunities && vm.showCommunity")
.communities__title Communities
.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.
.communities__list
.community(ng-repeat="(communityKey, community) in vm.communities", ng-class="{'community--disabled': !community.status}", ng-if="community.display")
.community__details
.community__icon(ng-class="{'community__icon--disabled': !community.status}")
img(ng-if="communityKey == 'ios' && community.status", src="/images/ico-ios-community.svg")
img(ng-if="communityKey == 'ios' && !community.status", src="/images/ico-ios-community-grey.svg")

.community__text
span.community__title(class="{{!community.status && 'disabled'}}") {{community.displayName}}
.community__description
span(ng-if="communityKey == 'ios'") Mobile app design and development for iOS, with Swift emphasis

onoff-switch(model="community.status", unique-id="'community-' + communityKey")



.tracks-container
.title tracks
.description Topcoder's three categories of challenges… please pick at least one based on your skills and interests.
@@ -51,4 +70,4 @@
type="button",
tc-busy-button, tc-busy-when="vm.saving",
ng-click="vm.submitSkills()",
ng-disabled="vm.disableDoneButton || (!vm.tracks.DESIGN && !vm.tracks.DEVELOP && !vm.tracks.DATA_SCIENCE)") Done
ng-disabled="vm.disableDoneButton || !vm.isPageDirty()") Done
225 changes: 215 additions & 10 deletions app/skill-picker/skill-picker.spec.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,224 @@
/* jshint -W117, -W030 */
describe('Skill Picker Controller', function() {
var vm;
var toasterSvc, memberCertService, profileService;
var mockProfile = mockData.getMockProfile();

// beforeEach(function() {
// bard.appModule('tc.skill-picker');
// bard.inject(this, '$controller', '$rootScope', '$q', 'userProfile');
beforeEach(function() {
bard.appModule('tc.skill-picker');
bard.inject(this, '$controller', '$rootScope', '$q', 'MemberCertService', 'ProfileService', 'toaster', 'CONSTANTS');

// vm = $controller('SkillPickerController', {
// });
// });
memberCertService = MemberCertService;
profileService = ProfileService;
var swiftProgramId = CONSTANTS.SWIFT_PROGRAM_ID;

// bard.verifyNoOutstandingHttpRequests();
// mock member cert api
sinon.stub(memberCertService, 'getMemberRegistration', function(userId, programId) {
var deferred = $q.defer();
if (programId === swiftProgramId) {
var resp = {eventId: swiftProgramId, userId: 12345};
deferred.resolve(resp);
} else {
deferred.resolve();
}
return deferred.promise;
});

sinon.stub(memberCertService, 'registerMember', function(userId, programId) {
var deferred = $q.defer();
var resp = {eventId: programId, userId: 12345};
if (programId === 100) {
deferred.reject();
} else {
deferred.resolve(resp);
}
return deferred.promise;
});

// it('should be created successfully', function() {
// expect(vm).to.exist;
// });
// adds spy method to mocked profile object for saving the profile
mockProfile.save = sinon.spy();

// mock profile service
sinon.stub(profileService, 'updateUserSkills', function(username, skills) {
var deferred = $q.defer();
var resp = {};
if (skills[412]) {
deferred.reject();
} else {
deferred.resolve(resp);
}
return deferred.promise;
});

// mocks the toaster service
toasterSvc = toaster;
bard.mockService(toaster, {
pop: $q.when(true),
default: $q.when(true)
});

var scope = $rootScope.$new();
vm = $controller('SkillPickerController', {
$scope: scope,
userProfile: mockProfile,
featuredSkills: []
});
$rootScope.$digest();
});

bard.verifyNoOutstandingHttpRequests();

it('should be created successfully', function() {
expect(vm).to.exist;
expect(vm.showCommunity).to.exist.to.false;
});

it('should have empty tracks object ', function() {
expect(vm.tracks).to.exist;
expect(vm.tracks.DESIGN).not.to.exist;
expect(vm.tracks.DEVELOP).not.to.exist;
expect(vm.tracks.DATA_SCIENCE).not.to.exist;
});

it('should have correct userIdentity ', function() {
expect(vm.userId).to.exist.to.equal(mockProfile.userId);
expect(vm.username).to.exist.to.equal(mockProfile.handle);
});

it('should not have page dirty ', function() {
var dirty = vm.isPageDirty();
expect(dirty).to.equal(false);
});

it('should be created successfully with showCommunity being true', function() {
// backup original swift program id, to restore after test execution
var origSwiftProgId = CONSTANTS.SWIFT_PROGRAM_ID;
// update swift program id to something which says, member is not registered
CONSTANTS.SWIFT_PROGRAM_ID = 3446;
vm = $controller('SkillPickerController', {
$scope: $rootScope.$new(),
userProfile: mockProfile,
featuredSkills: []
});
$rootScope.$digest();
expect(vm).to.exist;
// showCommunity flag should be set to true because we have at least one unregistered community
expect(vm.showCommunity).to.exist.to.true;
// restores the original swift program id
CONSTANTS.SWIFT_PROGRAM_ID = origSwiftProgId;
});

it('should add skill ', function() {
vm.toggleSkill(409);
expect(vm.mySkills).to.exist.have.length(1);
});

it('should remove added skill ', function() {
vm.toggleSkill(409);
// should add the skill
expect(vm.mySkills).to.exist.have.length(1);
// next call to toggleSkill with same argument, should remove that skill
vm.toggleSkill(409);
// should remove the skill
expect(vm.mySkills).to.exist.have.length(0);
});

it('should not make any update call without any change ', function() {
vm.submitSkills();
$rootScope.$digest();
expect(mockProfile.save).not.to.be.called;
expect(profileService.updateUserSkills).not.to.be.called;
expect(memberCertService.registerMember).not.to.be.called;
});

it('should update tracks for the member ', function() {
vm.tracks.DEVELOP = true;
vm.tracks.DESIGN = false;
vm.submitSkills();
$rootScope.$digest();
expect(mockProfile.save).to.be.calledOnce;
expect(profileService.updateUserSkills).not.to.be.called;
expect(memberCertService.registerMember).not.to.be.called;
});

it('should show error popup for updating tracks ', function() {
vm.tracks.DEVELOP = true;
// stubs the save method to reject the promise
mockProfile.save = function() {};
sinon.stub(mockProfile, 'save', function() {
var deferred = $q.defer();
deferred.reject();
return deferred.promise;
});
vm.submitSkills();
$rootScope.$digest();
expect(mockProfile.save).to.be.calledOnce;
expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('wrong')).calledOnce;
expect(profileService.updateUserSkills).not.to.be.called;
expect(memberCertService.registerMember).not.to.be.called;
});

it('should update skills for the member ', function() {
vm.mySkills = [409, 410];
vm.submitSkills();
$rootScope.$digest();
expect(mockProfile.save).not.to.be.called;
expect(profileService.updateUserSkills).to.be.calledOnce;
expect(memberCertService.registerMember).not.to.be.called;
});

it('should show error popup for error in updating skills ', function() {
vm.mySkills = [409, 410, 412];
vm.submitSkills();
$rootScope.$digest();
expect(mockProfile.save).not.to.be.called;
expect(profileService.updateUserSkills).to.be.calledOnce;
expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('wrong')).calledOnce;
expect(memberCertService.registerMember).not.to.be.called;
});

it('should update communities for the member ', function() {
vm.communities['ios'].status = true;
vm.communities['ios'].dirty = true;
vm.submitSkills();
$rootScope.$digest();
expect(mockProfile.save).not.to.be.called;
expect(profileService.updateUserSkills).not.to.be.called;
expect(memberCertService.registerMember).to.be.calledOnce;
});

// we may need to update this test case when we want to call unregister endpoint
it('should NOT update communities for the member for disabled community ', function() {
vm.communities['ios'].status = false;
vm.communities['ios'].dirty = true;
vm.submitSkills();
$rootScope.$digest();
expect(mockProfile.save).not.to.be.called;
expect(profileService.updateUserSkills).not.to.be.called;
expect(memberCertService.registerMember).not.to.be.called;
});

it('should NOT update communities for the member for enabled but non dirty community ', function() {
// TODO need one more community, with dirty true, to test out the non dirty community update
vm.communities['ios'].status = true;
vm.communities['ios'].dirty = false;
vm.submitSkills();
$rootScope.$digest();
expect(mockProfile.save).not.to.be.called;
expect(profileService.updateUserSkills).not.to.be.called;
expect(memberCertService.registerMember).not.to.be.called;
});

it('should show error popup for error in updating communities ', function() {
vm.communities['ios'].programId = 100;// to cause rejction of the promise
vm.communities['ios'].status = true;
vm.communities['ios'].dirty = true;
vm.submitSkills();
$rootScope.$digest();
expect(mockProfile.save).not.to.be.called;
expect(profileService.updateUserSkills).not.to.be.called;
expect(memberCertService.registerMember).to.be.calledOnce;
expect(toasterSvc.pop).to.have.been.calledWith('error', "Whoops!", sinon.match('wrong')).calledOnce;
});

});
61 changes: 61 additions & 0 deletions assets/css/skill-picker/skill-picker.scss
Original file line number Diff line number Diff line change
@@ -30,6 +30,67 @@
color: $accent-gray;
}

.communities {
width: 100%;
margin-bottom: 30px;
.communities__title {
@include sofia-pro-regular;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll probably deprecate these mixins, so it's safer to use the @include font-with-weight mixin

text-transform: uppercase;
font-size: 18px;
padding-bottom: 10px;
}
.communities__description {
@include font-with-weight('Merriweather Sans');
color: #A3A3AE;
font-size: 13px;
line-height: 18px;
}
.communities__list {
margin-top: 10px;
.community {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid $gray-light;

&.disabled {
background-color: $gray-lightest;
}

.community__details {
display: flex;
flex-direction: row;
align-items: center;
padding-left: 10px;
}

.community__text {
margin-left: 10px;
}

.community__title {
font-size: 16px;
line-height: 28px;
@include font-with-weight('Sofia Pro', 500);
transition: .1s color;

&.disabled {
color: $gray-dark;
}
}

.community__description {
@include font-with-weight('Merriweather Sans', 400);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default font-weight is set to 400, so we don't need to explicitly set it.

font-size: 13px;
margin-top: 4px;
color: $accent-gray;
}
}
}
}

.tracks-container {
width: 100%;
>.title {
18 changes: 18 additions & 0 deletions assets/images/ico-ios-community-grey.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions assets/images/ico-ios-community.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.