Skip to content

Commit

Permalink
Merge pull request #999 from CruGlobal/2314-require-image
Browse files Browse the repository at this point in the history
[EP-2314] Require uploaded files to be images
  • Loading branch information
canac authored Feb 13, 2023
2 parents 55f6391 + cab992b commit f4ee531
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 21 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"angular-translate": "^2.19.0",
"angular-ui-bootstrap": "^2.5.6",
"angular-ui-router": "^1.0.30",
"angular-upload": "^1.0.13",
"bootstrap-sass": "^3.4.1",
"change-case-object": "^2.0.0",
"cru-payments": "^1.2.2",
Expand Down
10 changes: 6 additions & 4 deletions src/app/designationEditor/carouselModal/carouselModal.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ <h5 translate>Click + to add photos to the carousel.</h5>
<div class="upload-drag-target p mb">
<div class="form-group text-center">
<div ng-if="!$ctrl.uploading">
<label translate>Select a Photo to Upload</label>
<upload-button
<div><label translate>Select a Photo to Upload</label></div>
<div><label ng-if="$ctrl.invalidFileType" translate>Uploaded image must be a JPEG or a PNG file</label></div>
<image-upload
class="form-control"
url="/bin/crugive/image?designationNumber={{$ctrl.designationNumber}}"
accept="image/*"
on-invalid-file-type="$ctrl.invalidFileType = true"
on-upload="$ctrl.uploading = true"
on-success="$ctrl.uploadComplete(response)"
on-complete="$ctrl.invalidFileType = false"
on-success="$ctrl.uploadComplete()"
on-error="$ctrl.uploading = false"/>
</div>

Expand Down
4 changes: 2 additions & 2 deletions src/app/designationEditor/photoModal/photo.modal.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import angular from 'angular'
import 'angular-upload'

import imageUploadDirective from 'common/directives/imageUpload.directive'
import designationEditorService from 'common/services/api/designationEditor.service'

const controllerName = 'photoCtrl'
Expand Down Expand Up @@ -49,7 +49,7 @@ class ModalInstanceCtrl {

export default angular
.module(controllerName, [
'lr.upload',
imageUploadDirective.name,
designationEditorService.name
])
.controller(controllerName, ModalInstanceCtrl)
13 changes: 7 additions & 6 deletions src/app/designationEditor/photoModal/photoModal.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ <h3 class="modal-title text-center" ng-if="$ctrl.photoLocation === 'signatureIma
<div class="upload-drag-target p mb">
<div class="form-group text-center">
<div ng-if="!$ctrl.uploading">
<label translate>Select a Photo to Upload</label>

<upload-button
<div><label translate>Select a Photo to Upload</label></div>
<div><label ng-if="$ctrl.invalidFileType" translate>Uploaded image must be a JPEG or a PNG file</label></div>
<image-upload
class="form-control"
url="/bin/crugive/image?designationNumber={{$ctrl.designationNumber}}"
accept="image/*"
on-invalid-file-type="$ctrl.invalidFileType = true"
on-complete="$ctrl.invalidFileType = false"
on-upload="$ctrl.uploading = true"
on-success="$ctrl.uploadComplete(response)"
on-success="$ctrl.uploadComplete()"
on-error="$ctrl.uploading = false"
></upload-button>
>
</div>

<label ng-if="$ctrl.uploading" translate>Uploading...</label>
Expand Down
68 changes: 68 additions & 0 deletions src/common/directives/imageUpload.directive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import angular from 'angular'

const directiveName = 'imageUpload'

// Based on https://github.com/leon/angular-upload
// Fixes bug where even if accept is "image/*", you could still drag-and-drop non-image files onto the upload button
const imageUpload = /* @ngInject */ ($http) => ({
restrict: 'EA',
scope: {
url: '@',
onInvalidFileType: '&',
onUpload: '&',
onSuccess: '&',
onError: '&',
onComplete: '&'
},
link: (scope, element) => {
const validMimeTypes = ['image/jpeg', 'image/png']

const el = angular.element(element)
const fileInput = angular.element(`<input type="file" accept="${validMimeTypes.join(',')}" />`)
el.append(fileInput)

fileInput.on('change', (event) => {
const files = event.target.files
const file = files[0]
if (!file) {
return
}

if (!validMimeTypes.includes(file.type)) {
// Clear the selected file because it's not a valid image
event.target.value = null
scope.$apply(() => {
scope.onInvalidFileType()
})
return
}

scope.$apply(() => {
scope.onUpload({ files })
})

const formData = new FormData()
formData.append('file', file)
$http({
url: scope.url,
method: 'POST',
headers: {
// Allow the browser to automatically determine the content type
'Content-Type': undefined
},
data: formData
}).then((response) => {
scope.onSuccess({ response })
scope.onComplete({ response })
}).catch((response) => {
scope.onError({ response })
scope.onComplete({ response })
}
)
})
}
})

export default angular
.module(directiveName, [])
.directive(directiveName, imageUpload)
112 changes: 112 additions & 0 deletions src/common/directives/imageUpload.directive.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import angular from 'angular'
import 'angular-mocks'
import module from './imageUpload.directive'

const uploadFile = (input, file) => {
// jsdom doesn't yet support changing an inputs' files because it doesn't let you instantiate a
// FileList and it doesn't yet support DataTransfer. The easiest way around this to send a change
// event with `target.files` manually mocked.
// Reference: https://github.com/jsdom/jsdom/issues/1272
const event = new Event('change')
// Use defineProperty because `target` is a getter and can't be set normally
Object.defineProperty(event, 'target', {
get: () => ({ files: file ? [file] : [] })
})
input.dispatchEvent(event)
}

describe('imageUpload', () => {
beforeEach(angular.mock.module(module.name))

let $httpBackend, $scope, element
beforeEach(() => {
inject(($compile, $injector, $rootScope) => {
$httpBackend = $injector.get('$httpBackend')
$httpBackend.whenPOST(/^\/upload$/).respond(() => [200, 'Success', {}])

$scope = $rootScope.$new()
$scope.onInvalidFileType = jest.fn()
$scope.onUpload = jest.fn()
$scope.onSuccess = jest.fn()
$scope.onError = jest.fn()
$scope.onComplete = jest.fn()

element = $compile('<div class="btn btn-primary btn-upload" image-upload url="/upload" on-invalid-file-type="onInvalidFileType()" on-upload="onUpload()" on-success="onSuccess()" on-error="onError()" on-complete="onComplete()"><button>Upload</button></div>')($scope)
})
})

it('should display input', () => {
$scope.$digest()
expect(element.html()).toContain('type="file"')
})

it('should set accept', () => {
$scope.$digest()
expect(element.find('input').attr('accept')).toEqual('image/jpeg,image/png')
})

it('should accept file uploads for JPEG', () => {
const file = new File(['contents'], 'file.jpg', { type: 'image/jpeg' })
uploadFile(element.find('input')[0], file)
$scope.$digest()

expect($scope.onUpload).toHaveBeenCalled()
})

it('should accept file uploads for PNG', () => {
const file = new File(['contents'], 'file.png', { type: 'image/png' })
uploadFile(element.find('input')[0], file)
$scope.$digest()

expect($scope.onUpload).toHaveBeenCalled()
})

it('should ignore null file', () => {
uploadFile(element.find('input')[0], null)
$scope.$digest()

expect($scope.onUpload).not.toHaveBeenCalled()
})

it('should reject non-image files', () => {
const file = new File(['contents'], 'file.txt', { type: 'text/plain' })
uploadFile(element.find('input')[0], file)
$scope.$digest()

expect($scope.onInvalidFileType).toHaveBeenCalled()
expect($scope.onUpload).not.toHaveBeenCalled()
})

it('should reject HEIC files', () => {
const file = new File(['contents'], 'file.heic', { type: 'image/heic' })
uploadFile(element.find('input')[0], file)
$scope.$digest()

expect($scope.onInvalidFileType).toHaveBeenCalled()
expect($scope.onUpload).not.toHaveBeenCalled()
})

it('should call onSuccess and onComplete', () => {
const file = new File(['contents'], 'file.png', { type: 'image/png' })
uploadFile(element.find('input')[0], file)
$scope.$digest()
$httpBackend.flush()

expect($scope.onSuccess).toHaveBeenCalled()
expect($scope.onComplete).toHaveBeenCalled()
})

it('should call onError and onComplete', () => {
$httpBackend.matchLatestDefinitionEnabled(true)
$httpBackend.whenPOST(/^\/upload$/).respond(() => [500, 'Error', {}])

const file = new File(['contents'], 'file.png', { type: 'image/png' })
uploadFile(element.find('input')[0], file)
$scope.$digest()
$httpBackend.flush()

expect($scope.onSuccess).not.toHaveBeenCalled()
expect($scope.onError).toHaveBeenCalled()
expect($scope.onComplete).toHaveBeenCalled()
})
})
9 changes: 1 addition & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2134,14 +2134,7 @@ angular-ui-router@^1.0.30:
dependencies:
"@uirouter/core" "6.0.8"

angular-upload@^1.0.13:
version "1.0.13"
resolved "https://registry.yarnpkg.com/angular-upload/-/angular-upload-1.0.13.tgz#f622bde164e5ed96ba062409e659a3021921c407"
integrity sha1-9iK94WTl7Za6BiQJ5lmjAhkhxAc=
dependencies:
angular ">=1.2.0"

angular@*, angular@>=1.2.0:
angular@*:
version "1.7.9"
resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.9.tgz#e52616e8701c17724c3c238cfe4f9446fd570bc4"
integrity sha512-5se7ZpcOtu0MBFlzGv5dsM1quQDoDeUTwZrWjGtTNA7O88cD8TEk5IEKCTDa3uECV9XnvKREVUr7du1ACiWGFQ==
Expand Down

0 comments on commit f4ee531

Please sign in to comment.