diff --git a/app/directives/tc-fp-file-input/tc-file-input.spec.js b/app/directives/tc-fp-file-input/tc-file-input.spec.js new file mode 100644 index 000000000..f6c0ce231 --- /dev/null +++ b/app/directives/tc-fp-file-input/tc-file-input.spec.js @@ -0,0 +1,208 @@ +/*eslint no-undef:0*/ +import angular from 'angular' + +describe('Topcoder File Input Directive', function() { + var scope, element, isolateScope, fileInput + + beforeEach(function() { + bard.appModule('topcoder') + bard.inject(this, '$compile', '$rootScope', '$timeout') + scope = $rootScope.$new() + + var html = '' + + '
' + var form = angular.element(html) + element = form.find('tc-file-input') + $compile(form)(scope) + scope.$digest() + + isolateScope = element.isolateScope() + }) + + beforeEach(function() { + fileInput = $(element).find('.none')[0] + }) + + afterEach(function() { + scope.$destroy() + fileInput = undefined + }) + + bard.verifyNoOutstandingHttpRequests() + + describe('selectFile', function() { + it('triggers a click on the file input', function() { + var mockClick = sinon.spy(fileInput, 'click') + + isolateScope.selectFile() + scope.$digest() + + expect(mockClick).calledOnce + }) + }) + + describe('a change event on the file input', function() { + var fileNameInput, fileList, mockSetFileReference + + beforeEach(function() { + fileNameInput = $(element).find('input[type=text]')[0] + fileList = { + 0: { + name: 'test.zip', + size: 50, + type: 'application/zip' + }, + length: 1, + item: function (index) { return index } + } + + mockSetFileReference = sinon.spy(isolateScope, 'setFileReference') + }) + + afterEach(function() { + fileNameInput = undefined + fileList = undefined + mockSetFileReference = undefined + }) + + it('sets the value of the fileNameInput with the name of the file', function() { + $(fileInput).triggerHandler({ + type: 'change', + target: { files: fileList } + }) + + $timeout.flush() + + expect(fileNameInput.value).to.equal('test.zip') + }) + + describe('with a valid file', function() { + beforeEach(function() { + $(fileInput).triggerHandler({ + type: 'change', + target: { files: fileList } + }) + $timeout.flush() + }) + + it('calls setFileReference', function() { + expect(mockSetFileReference).calledOnce + }) + + it('has ng-valid-filesize class', function() { + expect($(fileInput).hasClass('ng-valid-filesize')).to.be.true + }) + + it('has ng-valid-required class', function() { + expect($(fileInput).hasClass('ng-valid-required')).to.be.true + }) + + it('works with Windows file type application/x-zip', function(){ + fileList[0].type = 'application/x-zip' + + $(fileInput).triggerHandler({ + type: 'change', + target: { files: fileList } + }) + + $timeout.flush() + + expect(mockSetFileReference).called + expect($(fileInput).hasClass('ng-valid-filesize')).to.be.true + expect($(fileInput).hasClass('ng-valid-required')).to.be.true + }) + + it('works with Windows file type application/x-zip-compressed', function(){ + fileList[0].type = 'application/x-zip-compressed' + + $(fileInput).triggerHandler({ + type: 'change', + target: { files: fileList } + }) + + $timeout.flush() + + expect(mockSetFileReference).called + expect($(fileInput).hasClass('ng-valid-filesize')).to.be.true + expect($(fileInput).hasClass('ng-valid-required')).to.be.true + }) + }) + + describe('with a file type that\'s not in the list of fileTypes given to the directive', function() { + beforeEach(function() { + fileList[0].type = 'image/png' + + $(fileInput).triggerHandler({ + type: 'change', + target: { files: fileList } + }) + + $timeout.flush() + }) + + it('does not call setFileReference', function() { + expect(mockSetFileReference).not.calledOnce + }) + + it('has ng-touched and ng-invalid-required classes', function() { + expect($(fileInput).hasClass('ng-invalid-required')).to.be.true + expect($(fileInput).hasClass('ng-touched')).to.be.true + }) + }) + + describe('with a file extension that is not in the list of fileTypes given to the directive', function() { + beforeEach(function() { + fileList[0].name = 'submission.zip.jpg' + + $(fileInput).triggerHandler({ + type: 'change', + target: { files: fileList } + }) + + $timeout.flush() + }) + + it('does not call setFileReference', function() { + expect(mockSetFileReference).not.calledOnce + }) + + it('has ng-touched and ng-invalid-required classes', function() { + expect($(fileInput).hasClass('ng-invalid-required')).to.be.true + expect($(fileInput).hasClass('ng-touched')).to.be.true + }) + }) + + describe('with a file that\'s greater than 500MB', function() { + beforeEach(function() { + fileList[0].size = 524288001 + + $(fileInput).triggerHandler({ + type: 'change', + target: { files: fileList } + }) + + $timeout.flush() + }) + + it('does not call setFileReference', function() { + expect(mockSetFileReference).not.calledOnce + }) + + it('has ng-touched and ng-invalid-filesize classes', function() { + expect($(fileInput).hasClass('ng-invalid-filesize')).to.be.true + expect($(fileInput).hasClass('ng-touched')).to.be.true + }) + }) + }) +}) diff --git a/app/directives/tc-fp-file-input/tc-fp-file-input.directive.js b/app/directives/tc-fp-file-input/tc-fp-file-input.directive.js new file mode 100644 index 000000000..4b111fcd0 --- /dev/null +++ b/app/directives/tc-fp-file-input/tc-fp-file-input.directive.js @@ -0,0 +1,80 @@ +import angular from 'angular' +import _ from 'lodash' + +(function() { + 'use strict' + + angular.module('tcUIComponents').directive('tcFpFileInput', ['$rootScope', 'CONSTANTS', 'logger', 'UserService', 'filepickerService', tcFPFileInput]) + + function tcFPFileInput($rootScope, CONSTANTS, logger, UserService, filepickerService) { + return { + restrict: 'E', + require: '^form', + template: require('./tc-fp-file-input')(), + scope: { + labelText: '@', + fieldId: '@', + placeholder: '@', + fileType: '@', + showFileType: '=', + mandatory: '=', + maxFileSize: '@', + fpServices: '@', + buttonText: '@', + setFileReference: '&', + ngModel: '=' + }, + link: function(scope, element, attrs, formController) { + // set filePath + var userId = parseInt(UserService.getUserIdentity().userId) + scope.filePath = scope.fieldId + '/' + if (scope.fieldId.indexOf('ZIP') > -1) { + scope.filePath += _.join([userId, scope.fieldId, (new Date()).valueOf()], '-') + '.zip' + } + // set extensions + if (scope.fieldId.indexOf('ZIP') > -1) { + scope.extensions = ".zip" + } else if (scope.fieldId.indexOf('DESIGN_COVER') > -1) { + scope.extensions = ".png,.jpeg,.jpg,.bmp" + } + + // set default services + scope.fpServices = scope.fpServices || "COMPUTER,GOOGLE_DRIVE,BOX,DROPBOX" + scope.fpContainer = CONSTANTS.FILE_PICKER_SUBMISSION_CONTAINER_NAME || 'submission-staging-dev' + + // set max size + scope.maxSize = 500*1024*1024 + + var key, value; + /* + *pass original event + */ + element.bind('change', function(event) { + event.preventDefault() + scope.onSuccess(event.originalEvent || event); + $rootScope.$apply() + }); + element = element.length ? element[0] : element; + for (key in attrs.$attr){ + value = attrs.$attr[key] + element.setAttribute(value, attrs[key]) + } + filepickerService.constructWidget(element) + + scope.onSuccess = function (event) { + debugger + var fpFile = event.fpfile + var _file = { + name: scope.filename || fpFile.filename, + container: fpFile.container || scope.fpContainer, + path: fpFile.key, + size: fpFile.size, + mimetype: fpFile.mimetype + } + scope.ngModel = _file + scope.setFileReference({file: _file, fieldId: scope.fieldId}) + } + } + } + } +})() diff --git a/app/directives/tc-fp-file-input/tc-fp-file-input.jade b/app/directives/tc-fp-file-input/tc-fp-file-input.jade new file mode 100644 index 000000000..780dbce18 --- /dev/null +++ b/app/directives/tc-fp-file-input/tc-fp-file-input.jade @@ -0,0 +1,19 @@ +.tc-file-field__label + label.tc-label {{labelText}} + span.lowercase(ng-if="showFileType") {{ ' *(.' + fileType + ')'}} + +.tc-file-field__inputs + .tc-label__wrapper + span.tc-label__asterisk.lowercase(ng-if="mandatory") #[span *]mandatory + input.tc-file-field__input( + type="filepicker-dragdrop", + data-fp-maxSize="{{maxSize}}", + data-fp-button-class="tc-btn", + data-fp-services="{{fpServices}}", + data-fp-multiple="false", + data-fp-extensions="{{extensions}}", + data-fp-store-location="s3", + data-fp-store-container="{{fpContainer}}", + data-fp-store-path="{{filePath}}", + on-success="onFileSeleted(event.fpfile)" + ) diff --git a/app/services/submissions.service.js b/app/services/submissions.service.js index d30c4256e..c942f1149 100644 --- a/app/services/submissions.service.js +++ b/app/services/submissions.service.js @@ -11,20 +11,25 @@ import angular from 'angular' var api = ApiService.getApiServiceProvider('SUBMISSIONS') var service = { - getPresignedURL: getPresignedURL, - uploadSubmissionFileToS3: uploadSubmissionFileToS3, - updateSubmissionStatus: updateSubmissionStatus, - recordCompletedSubmission: recordCompletedSubmission + startSubmission: startSubmission, + processSubmission: processSubmission + // uploadSubmissionFileToS3: uploadSubmissionFileToS3, + // updateSubmissionStatus: updateSubmissionStatus, + // recordCompletedSubmission: recordCompletedSubmission } return service - function getPresignedURL(body, files, progressCallback) { + function startSubmission(body, progressCallback) { return api.all('submissions').customPOST(body) .then(function(response) { - progressCallback.call(progressCallback, 'PREPARE', 100) + //progressCallback.call(progressCallback, 'PREPARE', 100) - uploadSubmissionFileToS3(response, response.data.files, files, progressCallback) + // uploadSubmissionFileToS3(response, response.data.files, files, progressCallback) + + console.log(response); + + processSubmission(response, progressCallback); }) .catch(function(err) { logger.error('Could not get presigned url', err) @@ -35,6 +40,7 @@ import angular from 'angular' }) } + /** function uploadSubmissionFileToS3(presignedURLResponse, filesWithPresignedURL, files, progressCallback) { var promises = filesWithPresignedURL.map(function(fileWithPresignedURL) { @@ -120,14 +126,17 @@ import angular from 'angular' progressCallback.call(progressCallback, 'ERROR', err) }) } + */ - function recordCompletedSubmission(body, progressCallback) { + function processSubmission(body, progressCallback) { // Once all uploaded, make record and begin processing return api.one('submissions', body.id).customPOST(body, 'process') .then(function(response) { logger.info('Successfully made file record. Beginning processing') progressCallback.call(progressCallback, 'FINISH', 100) + + return response; }) .catch(function(err) { logger.error('Could not start processing', err) diff --git a/app/submissions/submissions.module.js b/app/submissions/submissions.module.js index 387625919..6e1b461fd 100644 --- a/app/submissions/submissions.module.js +++ b/app/submissions/submissions.module.js @@ -8,9 +8,15 @@ import angular from 'angular' 'tc.services', 'tcUIComponents', 'toaster', - 'appirio-tech-ng-ui-components' + 'appirio-tech-ng-ui-components', + 'angular-filepicker' ] angular.module('tc.submissions', dependencies) + .config(['filepickerProvider', 'CONSTANTS', + function (filepickerProvider, CONSTANTS) { + filepickerProvider.setKey(CONSTANTS.FILE_PICKER_API_KEY || 'AzFINuQoqTmqw0QEoaw9az') + } + ]) })() 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 31c85a2c1..ef047d149 100644 --- a/app/submissions/submit-design-files/submit-design-files.controller.js +++ b/app/submissions/submit-design-files/submit-design-files.controller.js @@ -50,9 +50,7 @@ import _ from 'lodash' }, userId: userId, data: { - method: challengeToSubmitTo.challenge.track.toUpperCase() + '_CHALLENGE_ZIP_FILE', - - // Can delete below since they are processed and added later? + method: 'DESIGN_CHALLENGE_FILE_PICKER_ZIP_FILE', files: [], submitterRank: 1, submitterComments: '', @@ -80,24 +78,14 @@ import _ from 'lodash' } 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' - } - - 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 + status: 'STAGED', + stagedFileContainer: file.container, + stagedFilePath: file.path, + size: file.size, + mediaType: file.mimetype } // If user changes a file input's file, update the file details @@ -159,8 +147,18 @@ import _ from 'lodash' }, []) vm.submissionsBody.data.fonts = processedFonts - - SubmissionsService.getPresignedURL(vm.submissionsBody, files, updateProgress) + SubmissionsService.startSubmission(vm.submissionsBody, updateProgress) + .then(function(newSubmission) { + logger.debug("New Submission: ", newSubmission) + //SubmissionsService.processSubmission(newSubmission, updateProgress) + }) + .then(function(processedSubmission) { + logger.debug("Processed Submission: ", processedSubmission) + + }) + .catch(function(err) { + logger.error("Submission processing failed ", err) + }) } // Callback for updating submission upload process. It looks for different phases e.g. PREPARE, UPLOAD, FINISH diff --git a/app/submissions/submit-design-files/submit-design-files.jade b/app/submissions/submit-design-files/submit-design-files.jade index ce0be9d04..0da471c4e 100644 --- a/app/submissions/submit-design-files/submit-design-files.jade +++ b/app/submissions/submit-design-files/submit-design-files.jade @@ -10,7 +10,7 @@ .panel-body p.tc-error-messages.submissions-access-error(ng-if="submissions.error", ng-bind="submissions.errorMessage") - + //-{{vm.submissionForm}}form.form-blocks(ng-if="!submissions.error", name="submissionForm", role="form", ng-submit="submissionForm.$valid && vm.uploadSubmission()", novalidate) .form-block.flex .form-block__instructions @@ -27,18 +27,19 @@ .form-block__fields .fieldset - tc-file-input.tc-file-field( + tc-fp-file-input( label-text="Submission", field-id="SUBMISSION_ZIP", - button-text="Add File", file-type="zip", + button-text="Add File", + services="COMPUTER,GOOGLE_DRIVE,BOX,DROPBOX", show-file-type="true", placeholder="Attach all visible files as a single .zip file", mandatory="true", - set-file-reference="vm.setFileReference(file, fieldId)", - ng-model="vm.submissionForm.submissionZip" + ng-model="vm.submissionForm.submissionZip", + set-file-reference="vm.setFileReference(file, fieldId)" ) - + .tc-error-messages( ng-show="submissionForm['SUBMISSION_ZIP'].$touched && submissionForm['SUBMISSION_ZIP'].$invalid", ng-messages="submissionForm['SUBMISSION_ZIP'].$error" @@ -47,19 +48,19 @@ p(ng-message="required") This is not the correct file format. Please select a .zip file. - - tc-file-input.tc-file-field( + tc-fp-file-input.tc-file-field( label-text="Source", field-id="SOURCE_ZIP", - button-text="Add File", file-type="zip", + button-text="Add File", + services="COMPUTER,GOOGLE_DRIVE,BOX,DROPBOX", show-file-type="true", - placeholder="Attach all source files as a single .zip file", + placeholder="Attach all visible files as a single .zip file", mandatory="true", set-file-reference="vm.setFileReference(file, fieldId)", ng-model="vm.submissionForm.sourceZip" ) - + .tc-error-messages( ng-show="submissionForm['SOURCE_ZIP'].$touched && submissionForm['SOURCE_ZIP'].$invalid", ng-messages="submissionForm['SOURCE_ZIP'].$error" @@ -68,11 +69,12 @@ p(ng-message="required") This is not the correct file format. Please select a .zip file. - tc-file-input.tc-file-field( + tc-fp-file-input.tc-file-field( label-text="Preview Image", field-id="DESIGN_COVER", + file-type=".jpg,.jpeg,.png", button-text="Add File", - file-type="jpg,jpeg,png" + services="COMPUTER,GOOGLE_DRIVE,BOX,DROPBOX", placeholder="Image file as .jpg or .png", mandatory="true", set-file-reference="vm.setFileReference(file, fieldId)", diff --git a/app/submissions/submit-design-files/submit-design-files.spec.js b/app/submissions/submit-design-files/submit-design-files.spec.js index 42766baee..5244c44b9 100644 --- a/app/submissions/submit-design-files/submit-design-files.spec.js +++ b/app/submissions/submit-design-files/submit-design-files.spec.js @@ -69,7 +69,7 @@ describe('Submit Design Files Controller', function() { vm = controller scope.$digest() - expect(vm.submissionsBody.data.method).to.equal('DEVELOP_CHALLENGE_ZIP_FILE') + expect(vm.submissionsBody.data.method).to.equal('DESIGN_CHALLENGE_FILE_PICKER_ZIP_FILE') }) describe('setRankTo1', function() { @@ -93,7 +93,7 @@ describe('Submit Design Files Controller', function() { file = { name: 'Dashboard 2.png', size: 575548, - type: 'image/png' + mimetype: 'image/png' } fieldId = 'DESIGN_COVER' @@ -116,7 +116,7 @@ describe('Submit Design Files Controller', function() { var newFile = { name: 'different_image.png', size: 4321, - type: 'image/png' + mimetype: 'image/png' } vm.setFileReference(newFile, fieldId) @@ -125,37 +125,9 @@ describe('Submit Design Files Controller', function() { 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() { - 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() { + describe.only('uploadSubmission', function() { it('adds comments to the submissions body', function() { vm.comments = 'test comments' scope.$digest() diff --git a/app/submissions/submit-develop-files/submit-develop-files.spec.js b/app/submissions/submit-develop-files/submit-develop-files.spec.js index ffe43fd96..5386e617a 100644 --- a/app/submissions/submit-develop-files/submit-develop-files.spec.js +++ b/app/submissions/submit-develop-files/submit-develop-files.spec.js @@ -69,7 +69,7 @@ describe('Submit Develop Files Controller', function() { vm = controller scope.$digest() - expect(vm.submissionsBody.data.method).to.equal('DESIGN_CHALLENGE_ZIP_FILE') + expect(vm.submissionsBody.data.method).to.equal('DEVELOP_CHALLENGE_ZIP_FILE') }) describe('setFileReference', function() { diff --git a/assets/scripts/angular-filepicker.js b/assets/scripts/angular-filepicker.js new file mode 100644 index 000000000..b54020d71 --- /dev/null +++ b/assets/scripts/angular-filepicker.js @@ -0,0 +1 @@ +"use strict";function filepickerDirective(e,r,i){return{restrict:"A",scope:{onSuccess:"&"},link:function(i,n,t){var c,l;n.bind("change",function(r){r.preventDefault(),i.onSuccess({event:r.originalEvent||r}),e.$apply()}),n=n.length?n[0]:n;for(c in t.$attr)l=t.$attr[c],n.setAttribute(l,t[c]);r.constructWidget(n)}}}function filepickerService(e){return e.filepicker}function filepickerPreviewDirective(e,r){return{restrict:"A",scope:{url:"="},link:function(e,r,i){function n(e){e&&(e=e.replace("api/file/","api/preview/"),c.src=e)}var t=e.url,c=document.createElement("iframe");c.src=t,c.width="100%",c.height="100%",angular.element(r).append(c),e.$watch("url",n)}}}function fpUtilService(){function e(e){var i=[];for(var n in e)e.hasOwnProperty(n)&&("[object Object]"!==Object.prototype.toString.call(e[n])?i.push(n+"="+e[n]):i.push(r(e[n])));return i.join("&")}function r(e){var r=[];for(var i in e)e.hasOwnProperty(i)&&r.push(encodeURIComponent(i)+"="+encodeURIComponent(e[i]));return r.join("&")}return{toParams:e,serialize:r}}function fpConvert(e,r){return function(i,n){var t=e("fpUrlFilter")(i);if(t&&n)return t+"/convert?"+r.toParams(n)}}angular.module("angular-filepicker",[]),window.filepicker=window.filepicker||{},window.filepicker.plugin="angular_js_lib",angular.module("angular-filepicker").directive("filepicker",filepickerDirective),filepickerDirective.$inject=["$rootScope","filepickerService","$parse"],angular.module("angular-filepicker").provider("filepicker",function(){this.$get=function(){return window.filepicker},this.setKey=function(e){try{window.filepicker.setKey(e)}catch(r){console.error("Include filepicker.js script")}}}),angular.module("angular-filepicker").service("filepickerService",filepickerService),filepickerService.$inject=["$window"],angular.module("angular-filepicker").directive("filepickerPreview",filepickerPreviewDirective),filepickerPreviewDirective.$inject=["$rootScope","filepickerService"],angular.module("angular-filepicker").service("fpUtilService",fpUtilService),angular.module("angular-filepicker").filter("fpConvert",fpConvert),fpConvert.$inject=["$filter","fpUtilService"],angular.module("angular-filepicker").filter("fpUrlFilter",function(){return function(e){if(!e)return"";var r=["/convert","/metadata","?"];for(var i in r){var n=e.indexOf(r[i]);if(n>-1)return e.substr(0,n)}return e}}); diff --git a/assets/scripts/filepicker.js b/assets/scripts/filepicker.js new file mode 100644 index 000000000..3b2fd72c0 --- /dev/null +++ b/assets/scripts/filepicker.js @@ -0,0 +1,3 @@ +"use strict";!function(){var e=function(){var e={},t=function(t,r,n){for(var o=t.split("."),i=0;i