diff --git a/app/index.jade b/app/index.jade index 4c78788d2..906c3de62 100644 --- a/app/index.jade +++ b/app/index.jade @@ -156,8 +156,6 @@ html script(src='../bower_components/react/react.js') script(src='../bower_components/react/react-dom.js') script(src='../bower_components/classnames/index.js') - script(src='../bower_components/classnames/bind.js') - script(src='../bower_components/classnames/dedupe.js') script(src='../bower_components/react-input-autosize/dist/react-input-autosize.min.js') script(src='../bower_components/react-select/dist/react-select.min.js') script(src='../bower_components/ngReact/ngReact.js') @@ -336,6 +334,7 @@ html script(src="submissions/submissions.controller.js") script(src="submissions/submissions.routes.js") script(src="submissions/submit-design-files/submit-design-files.controller.js") + script(src="submissions/submit-develop-files/submit-develop-files.controller.js") script(src="topcoder.constants.js") script(src="topcoder.controller.js") script(src="topcoder.interceptors.js") diff --git a/app/profile/subtrack/design/design-challenges.jade b/app/profile/subtrack/design/design-challenges.jade index 15a6bf01a..dc8b7260d 100644 --- a/app/profile/subtrack/design/design-challenges.jade +++ b/app/profile/subtrack/design/design-challenges.jade @@ -8,4 +8,4 @@ .no-challenges(ng-show="!vm.challenges || vm.challenges.length == 0") | Sorry, no successful challenges found. - tc-endless-paginator(state="vm.status.challenges", page-params="vm.pageParams") + tc-endless-paginator(state="vm.status.challenges", page-params="vm.pageParams") diff --git a/app/profile/subtrack/develop/develop-challenges.jade b/app/profile/subtrack/develop/develop-challenges.jade index 6214ffc5b..da7a5154b 100644 --- a/app/profile/subtrack/develop/develop-challenges.jade +++ b/app/profile/subtrack/develop/develop-challenges.jade @@ -10,4 +10,4 @@ .no-challenges(ng-show="!vm.challenges || vm.challenges.length == 0") | Sorry, no successful challenges found. - tc-endless-paginator(state="vm.status.challenges", page-params="vm.pageParams") + tc-endless-paginator(state="vm.status.challenges", page-params="vm.pageParams") diff --git a/app/submissions/submission-error/submission-error.jade b/app/submissions/submission-error/submission-error.jade new file mode 100644 index 000000000..d904ccd94 --- /dev/null +++ b/app/submissions/submission-error/submission-error.jade @@ -0,0 +1,3 @@ +.panel-body + p.tc-error-messages.submissions-access-error(ng-bind="submissions.errorMessage") + diff --git a/app/submissions/submissions.controller.js b/app/submissions/submissions.controller.js index ff9b79729..3f12e51cf 100644 --- a/app/submissions/submissions.controller.js +++ b/app/submissions/submissions.controller.js @@ -6,23 +6,30 @@ SubmissionsController.$inject = ['challengeToSubmitTo', '$state']; function SubmissionsController(challengeToSubmitTo, $state) { - var vm = this; - var challenge = challengeToSubmitTo.challenge; - vm.challengeTitle = challenge.name; - vm.challengeId = challenge.id; - vm.track = challenge.track.toLowerCase(); - - activate(); + vm.error = !!challengeToSubmitTo.error; - function activate() { - var track = challengeToSubmitTo.challenge.track; + if (vm.error) { + vm.errorType = challengeToSubmitTo.error.type; + vm.errorMessage = challengeToSubmitTo.error.message; + vm.challengeError = vm.errorType === 'challenge'; + } - if (track === 'DESIGN') { - $state.go('submissions.file.design'); - } else if (track === 'DEVELOP') { - $state.go('submissions.file.develop') + if (challengeToSubmitTo.challenge) { + var challenge = challengeToSubmitTo.challenge; + vm.challengeTitle = challenge.name; + vm.challengeId = challenge.id; + vm.track = challenge.track.toLowerCase(); + + if (challengeToSubmitTo.error) { + $state.go('submissions.file.error'); + } else { + if (challenge.track === 'DESIGN') { + $state.go('submissions.file.design'); + } else if (challenge.track === 'DEVELOP') { + $state.go('submissions.file.develop') + } } } } diff --git a/app/submissions/submissions.jade b/app/submissions/submissions.jade index 309269929..85ea902e0 100644 --- a/app/submissions/submissions.jade +++ b/app/submissions/submissions.jade @@ -1,5 +1,5 @@ .panel-page - .panel-header.flex.space-between + .panel-header.flex.space-between(ng-if="!submissions.challengeError") a.panel-header__back-button.flex.space-between.middle(ng-href="https://www.{{DOMAIN}}/challenge-details/{{submissions.challengeId}}/?type={{submissions.track}}") //- TODO: Replace below with svg tag diff --git a/app/submissions/submissions.routes.js b/app/submissions/submissions.routes.js index e0199c956..d6683bf91 100644 --- a/app/submissions/submissions.routes.js +++ b/app/submissions/submissions.routes.js @@ -29,13 +29,18 @@ var userHandle = UserService.getUserIdentity().handle; + var error = null; + return ChallengeService.getUserChallenges(userHandle, params) .then(function(challenge) { - challenge = challenge[0]; + challenge = challenge[0].plain(); if (!challenge) { - // TODO: There should be a challenge, redirect? - alert('User is not associated with this challenge.'); + setErrorMessage('challenge', 'This is not a valid challenge. Use your browser\'s back button to return.'); + return { + error: error, + challenge: null + }; } var phaseType; var phaseId; @@ -57,26 +62,43 @@ return false; }); + if (!isPhaseSubmission) { + setErrorMessage('phase', 'Submission phases are not currently open for this challenge.') + } + var isSubmitter = _.some(challenge.userDetails.roles, function(role) { return role === 'Submitter'; }); - if (!isPhaseSubmission || !isSubmitter) { - // TODO: Where do we redirect if you can't submit? - alert('You should not have access to this page'); + if (!isSubmitter) { + setErrorMessage('submitter', 'You do not have a submitter role for this challenge.') } return { + error: error, challenge: challenge, phaseType: phaseType, phaseId: phaseId }; }) .catch(function(err) { - console.log('ERROR GETTING CHALLENGE: ', err); - alert('There was an error accessing this page'); - // TODO: Where do we redirect if there is an error? + setErrorMessage('challenge', 'There was an error getting information for this challenge.'); + + return { + error: error, + challenge: null + }; }); + + function setErrorMessage(type, message) { + // Sets the error as the first error encountered + if (!error) { + error = { + type: type, + message: message + }; + } + } }] } }, @@ -85,6 +107,10 @@ abstract: true, template: '' }, + 'submissions.file.error': { + url: '', + templateUrl: 'submissions/submission-error/submission-error.html', + }, 'submissions.file.design': { url:'', templateUrl: 'submissions/submit-design-files/submit-design-files.html', @@ -96,7 +122,7 @@ templateUrl: 'submissions/submit-develop-files/submit-develop-files.html', controller: 'SubmitDevelopFilesController', controllerAs: 'vm', - }, + } }; for (var name in states) { diff --git a/app/submissions/submissions.spec.js b/app/submissions/submissions.spec.js index 4e2b16fa1..91ae5b4b9 100644 --- a/app/submissions/submissions.spec.js +++ b/app/submissions/submissions.spec.js @@ -33,12 +33,31 @@ describe('Submissions Controller', function() { expect(vm).to.exist; }); - it('has properties on vm from the routes resolve', function() { + it('sets error properties when there is an error passed down', function() { + controller = $controller('SubmissionsController', { + challengeToSubmitTo: { + challenge: null, + error: { + type: 'challenge', + message: 'error getting challenge information' + } + }, + $state: state + }); + vm = controller; + + expect(vm.errorType).to.equal('challenge'); + expect(vm.errorMessage).to.equal('error getting challenge information'); + expect(vm.challengeError).to.be.true; + }); + + it('sets challenge properties when there is a challenge from the routes resolve', function() { expect(vm.challengeTitle).to.equal(mockChallenge.challenge.name); expect(vm.challengeId).to.equal(30049240); expect(vm.track).to.equal(mockChallenge.challenge.track.toLowerCase()); }); + describe('routes to the correct child state for', function() { it('design challenges', function() { @@ -46,7 +65,6 @@ describe('Submissions Controller', function() { }); it('develop challenges', function() { - controller = $controller('SubmissionsController', { challengeToSubmitTo: { challenge: { @@ -62,5 +80,24 @@ describe('Submissions Controller', function() { expect(state.go).calledWith('submissions.file.develop'); }); + it('errors', function() { + controller = $controller('SubmissionsController', { + challengeToSubmitTo: { + challenge: { + name: 'Challenge Name', + track: 'DEVELOP', + id: 30049240 + }, + error: { + type: 'phase', + message: 'No open submissions phase' + } + }, + $state: state + }); + vm = controller; + + expect(state.go).calledWith('submissions.file.error'); + }); }); }); diff --git a/app/submissions/submit-design-files/submit-design-files.controller.js b/app/submissions/submit-design-files/submit-design-files.controller.js index 056795f25..6b8c9b70e 100644 --- a/app/submissions/submit-design-files/submit-design-files.controller.js +++ b/app/submissions/submit-design-files/submit-design-files.controller.js @@ -163,10 +163,8 @@ SubmissionsService.getPresignedURL(vm.submissionsBody, files, updateProgress); } - /* - * Callback for updating submission upload process. It looks for different phases e.g. PREPARE, UPLOAD, FINISH - * of the submission upload and updates the progress UI accordingly. - */ + // Callback for updating submission upload process. It looks for different phases e.g. PREPARE, UPLOAD, FINISH + // of the submission upload and updates the progress UI accordingly. function updateProgress(phase, args) { // for PREPARE phase if (phase === 'PREPARE') { diff --git a/app/submissions/submit-develop-files/submit-develop-files.controller.js b/app/submissions/submit-develop-files/submit-develop-files.controller.js new file mode 100644 index 000000000..9b40e7fc4 --- /dev/null +++ b/app/submissions/submit-develop-files/submit-develop-files.controller.js @@ -0,0 +1,171 @@ +(function () { + 'use strict'; + + angular.module('tc.submissions').controller('SubmitDevelopFilesController', SubmitDevelopFilesController); + + SubmitDevelopFilesController.$inject = ['$scope','$window', '$stateParams', '$log', 'UserService', 'SubmissionsService', 'challengeToSubmitTo']; + + function SubmitDevelopFilesController($scope, $window, $stateParams, $log, UserService, SubmissionsService, challengeToSubmitTo) { + var vm = this; + $log = $log.getInstance('SubmitDevelopFilesController'); + var files = {}; + var fileUploadProgress = {}; + vm.comments = ''; + vm.uploadProgress = 0; + vm.uploading = false; + vm.preparing = false; + vm.finishing = false; + vm.showProgress = false; + vm.errorInUpload = false; + vm.submissionForm = { + files: [], + + // use develop name + sourceZip: null, + + submitterComments: '', + hasAgreedToTerms: false + }; + + var userId = parseInt(UserService.getUserIdentity().userId); + + vm.submissionsBody = { + reference: { + type: 'CHALLENGE', + id: $stateParams.challengeId, + phaseType: challengeToSubmitTo.phaseType, + phaseId: challengeToSubmitTo.phaseId + }, + userId: userId, + data: { + method: challengeToSubmitTo.challenge.track.toUpperCase() + '_CHALLENGE_ZIP_FILE', + + // Can delete below since they are processed and added later? + files: [], + submitterComments: '', + } + }; + + vm.setFileReference = setFileReference; + vm.uploadSubmission = uploadSubmission; + vm.refreshPage = refreshPage; + vm.cancelRetry = cancelRetry; + + activate(); + + function activate() {} + + function setFileReference(file, fieldId) { + // Can clean up since fileValue on tcFileInput has file reference? + files[fieldId] = file; + + var fileObject = { + name: file.name, + type: fieldId, + status: 'PENDING' + }; + + // TODO: Refactor or develop + switch(fieldId) { + case 'SUBMISSION_ZIP': + fileObject.mediaType = 'application/octet-stream'; + break; + case 'SOURCE_ZIP': + fileObject.mediaType = 'application/octet-stream'; + break; + default: + fileObject.mediaType = file.type; + } + + // If user changes a file input's file, update the file details + var isFound = vm.submissionsBody.data.files.reduce(function(isFound, file, i, filesArray) { + if (isFound) { return true; } + + if (file.type === fileObject.type) { + filesArray[i] = fileObject; + return true; + } + + return false; + }, false); + + // Add new files to the list + if (!isFound) { + vm.submissionsBody.data.files.push(fileObject); + } + } + + function uploadSubmission() { + vm.errorInUpload = false; + vm.uploadProgress = 0; + vm.fileUploadProgress = {}; + vm.showProgress = true; + vm.preparing = true; + vm.uploading = false; + vm.finishing = false; + vm.submissionsBody.data.submitterComments = vm.comments; + + $log.debug('Body for request: ', vm.submissionsBody); + SubmissionsService.getPresignedURL(vm.submissionsBody, files, updateProgress); + } + + // Callback for updating submission upload process. It looks for different phases e.g. PREPARE, UPLOAD, FINISH + // of the submission upload and updates the progress UI accordingly. + function updateProgress(phase, args) { + // for PREPARE phase + if (phase === 'PREPARE') { + // we are concerned only for completion of the phase + if (args === 100) { + vm.preparing = false; + vm.uploading = true; + $log.debug('Prepared for upload.'); + } + } else if (phase === 'UPLOAD') { + // if args is object, this update is about XHRRequest's upload progress + if (typeof args === 'object') { + var requestId = args.file; + var progress = args.progress; + if (!fileUploadProgress[requestId] || fileUploadProgress[requestId] < progress) { + fileUploadProgress[requestId] = progress; + } + var total = 0, count = 0; + for(var requestId in fileUploadProgress) { + var prog = fileUploadProgress[requestId]; + total += prog; + count++; + } + vm.uploadProgress = total / count; + + // initiate digest cycle because this event (xhr event) is caused outside angular + $scope.$apply(); + } else { // typeof args === 'number', mainly used a s fallback to mark completion of the UPLOAD phase + vm.uploadProgress = args; + } + + // start next phase when UPLOAD is done + if (vm.uploadProgress == 100) { + $log.debug('Uploaded files.'); + vm.uploading = false; + vm.finishing = true; + } + } else if (phase === 'FINISH') { + // we are concerned only for completion of the phase + if (args === 100) { + $log.debug('Finished upload.'); + } + } else { + // assume it to be error condition + $log.debug("Error Condition: " + phase); + vm.errorInUpload = true; + } + } + + function refreshPage() { + $window.location.reload(true); + } + + function cancelRetry() { + vm.showProgress = false; + } + } +})(); diff --git a/app/submissions/submit-develop-files/submit-develop-files.jade b/app/submissions/submit-develop-files/submit-develop-files.jade new file mode 100644 index 000000000..9d2241fd5 --- /dev/null +++ b/app/submissions/submit-develop-files/submit-develop-files.jade @@ -0,0 +1,90 @@ +.mobile-redirect + .mobile-redirect__title File upload is not available for mobile + + .mobile-redirect__body + p Our team is working hard on new features, and file upload currently only works on the web. Please open this page on your desktop if you want to create a submission. + + a(ng-href="http://help.{{DOMAIN}}/design/submitting-to-a-design-challenge/formatting-your-submission-for-design-challenges/") TODO: same link as below for help on formatting submission + + a.tc-btn.tc-btn-s(ng-href="https://www.{{DOMAIN}}/challenge-details/{{submissions.challengeId}}/?type={{submissions.track}}") Back to Challenge Details + +.panel-body + form.form-blocks(name="submissionForm", role="form", ng-submit="submissionForm.$valid && vm.uploadSubmission()", novalidate) + .form-block.flex + .form-block__instructions + .form-block__title Files + + .form-block__text + p Need text from PMs. + + p Need text from PMs. + + p Need text from PMs. + + a(ng-href="http://help.{{DOMAIN}}/design/submitting-to-a-design-challenge/formatting-your-submission-for-design-challenges/") Need link from PMs + + .form-block__fields + .fieldset + tc-file-input.tc-file-field( + label-text="Preview Image", + field-id="DESIGN_COVER", + button-text="Add File", + file-type="jpg,jpeg,png" + placeholder="Image file as .jpg or .png", + mandatory="true", + set-file-reference="vm.setFileReference(file, fieldId)", + ng-model="vm.submissionForm.designCover" + ) + + .tc-error-messages( + ng-show="submissionForm['DESIGN_COVER'].$touched && submissionForm['DESIGN_COVER'].$invalid", + ng-messages="submissionForm['DESIGN_COVER'].$error" + ) + p(ng-message="filesize") File size may not exceed 500MB. + + p(ng-message="required") This is not the correct file format. Please select a .jpg or .png file. + + .panel-footer + p Submitting your files means you hereby agree to the #[a(ng-href="https://www.{{DOMAIN}}/community/how-it-works/terms/", target="_blank") Topcoder terms of use] and to the extent your uploaded file wins a Topcoder Competition, you hereby assign, grant, and transfer to Topcoder all rights in and title to the Winning Submission (as further described in the terms of use). + + p.tc-error-messages(ng-show="vm.submissionForm.hasAgreedToTerms && submissionForm.$invalid") There are outstanding problems with this page. You must fix them before you can upload your submission. + + .checkbox.flex.center + input(type="checkbox", ng-model="vm.submissionForm.hasAgreedToTerms", id="agree-to-terms", required) + + label(for="agree-to-terms") I understand and agree + + button.tc-btn.tc-btn-secondary(type="submit", ng-disabled="submissionForm.$invalid") Submit + +modal.transition(show="vm.showProgress", background-click-close="false", style="background-color:white;") + .upload-progress(ng-class="{'upload-progress--error': vm.errorInUpload}") + .upload-progress__title + p Uploading submission for + + p.upload-progress-title__challenge-name [Challenge name] + + img.upload-progress__image(src="/images/robot.svg", ng-hide="vm.errorInUpload") + img.upload-progress__image--error(src="/images/robot-embarresed.svg", ng-show="vm.errorInUpload") + + p.upload-progress__message(ng-hide="vm.errorInUpload") Hey, your work is AWESOME! Please don’t close the window while I’m working or you’ll loose all files! + + p.upload-progress__message--error(ng-show="vm.errorInUpload") Oh, that’s embarrassing! One of the files couldn’t be uploaded, I’m so sorry. + + progress-bar.upload-progress__progress-bar(completed="vm.uploadProgress", message="of 3 files uploaded") + + .upload-progress__preparing(ng-show="vm.preparing && !vm.errorInUpload") #[span Preparing...] + .upload-progress__finishing(ng-show="vm.finishing && !vm.errorInUpload") + p Finished! + + .upload-progess__links + a.tc-btn.tc-btn-s(ng-href="https://www.{{DOMAIN}}/challenge-details/{{submissions.challengeId}}/?type={{submissions.track}}") Back to the challenge + + a.tc-btn.tc-btn-s.tc-btn-ghost(ng-click="vm.refreshPage()") Submit another + + + .upload-progress__error(ng-show="vm.errorInUpload") #[span File upload failed] + + .upload-progress__error-action(ng-show="vm.errorInUpload") + button.tc-btn.tc-btn-s.tc-btn-ghost(type="button", ng-click="vm.cancelRetry()") Cancel + + button.tc-btn.tc-btn-s.tc-btn-secondary(type="button", ng-click="submissionForm.$valid && vm.uploadSubmission()") Try Again diff --git a/app/submissions/submit-develop-files/submit-develop-files.spec.js b/app/submissions/submit-develop-files/submit-develop-files.spec.js new file mode 100644 index 000000000..97ca01833 --- /dev/null +++ b/app/submissions/submit-develop-files/submit-develop-files.spec.js @@ -0,0 +1,191 @@ +/* jshint -W117, -W030 */ +describe('Submit Develop Files Controller', function() { + var controller, vm, scope; + + var mockChallenge = { + challenge: { + name: 'Challenge Name', + track: 'DEVELOP', + id: 30049240 + } + }; + + var userService = { + getUserIdentity: function() { + return { + userId: 123456 + }; + } + }; + + var submissionsService = { + getPresignedURL: function() {} + }; + + var mockWindow = { + location: { + reload: function(val) { return val; } + } + }; + + beforeEach(function() { + bard.appModule('tc.submissions'); + bard.inject(this, '$controller', '$rootScope'); + + scope = $rootScope.$new(); + }); + + bard.verifyNoOutstandingHttpRequests(); + + beforeEach(function() { + controller = $controller('SubmitDevelopFilesController', { + $scope: scope, + UserService: userService, + challengeToSubmitTo: mockChallenge, + SubmissionsService: submissionsService, + $window: mockWindow + }); + vm = controller; + }); + + it('exists', function() { + expect(vm).to.exist; + }); + + it('sets the right track for the method', function() { + controller = $controller('SubmitDevelopFilesController', { + $scope: scope, + UserService: userService, + challengeToSubmitTo: { + challenge: { + name: 'Challenge Name', + track: 'DESIGN', + id: 30049240 + } + }, + SubmissionsService: submissionsService, + $window: mockWindow + }); + vm = controller; + scope.$digest(); + + expect(vm.submissionsBody.data.method).to.equal('DESIGN_CHALLENGE_ZIP_FILE'); + }); + + describe('setFileReference', function() { + var file, fieldId; + + beforeEach(function() { + // TODO: change to be more relevant to develop + file = { + name: 'Dashboard 2.png', + size: 575548, + type: 'image/png' + }; + fieldId = 'DESIGN_COVER'; + + vm.setFileReference(file, fieldId); + scope.$digest(); + }); + + afterEach(function() { + file = undefined; + fieldId = undefined; + }); + + it('adds a file object to the submissions body', function() { + expect(vm.submissionsBody.data.files).to.have.length(1); + }); + + it('replaces a file object with a new one if it has the same fieldId', function() { + expect(vm.submissionsBody.data.files).to.have.length(1); + + // TODO: change to be more relevant to develop submissions + var newFile = { + name: 'different_image.png', + size: 4321, + type: 'image/png' + }; + + vm.setFileReference(newFile, fieldId); + scope.$digest(); + + expect(vm.submissionsBody.data.files).to.have.length(1); + expect(vm.submissionsBody.data.files[0].name).to.equal('different_image.png'); + }); + + it('sets the correct mediaTypes on the fileObject', function() { + // TODO: change to be more relevant to develop + expect(vm.submissionsBody.data.files[0].mediaType).to.equal('image/png'); + + var newFile = { + name: 'submission.zip', + size: 43121, + type: 'application/zip' + }; + var newFieldId = 'SUBMISSION_ZIP'; + + vm.setFileReference(newFile, newFieldId); + scope.$digest(); + + expect(vm.submissionsBody.data.files[1].mediaType).to.equal('application/octet-stream'); + + var newFile2 = { + name: 'source.zip', + size: 2314, + type: 'application/zip' + }; + var newFieldId2 = 'SOURCE_ZIP'; + + vm.setFileReference(newFile2, newFieldId2); + scope.$digest(); + + expect(vm.submissionsBody.data.files[2].mediaType).to.equal('application/octet-stream'); + }); + }); + + describe('uploadSubmission', function() { + it('adds comments to the submissions body', function() { + vm.comments = 'test comments'; + scope.$digest(); + + vm.uploadSubmission(); + scope.$digest(); + + expect(vm.submissionsBody.data.submitterComments).to.equal('test comments'); + }); + + it('calls the submission service', function() { + var mockAPICall = sinon.spy(submissionsService, 'getPresignedURL'); + + vm.uploadSubmission(); + scope.$digest(); + + expect(mockAPICall).calledOnce; + }); + }); + + describe('refreshPage', function() { + it('reloads the page', function() { + var mockRefresh = sinon.spy(mockWindow.location, 'reload'); + + vm.refreshPage(); + scope.$digest(); + + expect(mockRefresh).calledWith(true); + expect(mockRefresh).calledOnce; + }); + }); + + describe('cancelRetry', function() { + it('sets showProgress to false', function() { + vm.showProgress = true; + scope.$digest(); + expect(vm.showProgress).to.be.true; + + vm.cancelRetry(); + scope.$digest(); + expect(vm.showProgress).to.be.false; + }); + }); +}); diff --git a/assets/css/submissions/submissions.scss b/assets/css/submissions/submissions.scss index f423db0b3..e60c2898b 100644 --- a/assets/css/submissions/submissions.scss +++ b/assets/css/submissions/submissions.scss @@ -44,3 +44,9 @@ } } } + +.submissions-access-error { + text-align: center; + margin-left: auto; + margin-right: auto; +}