Browse Source

Merge branch 'master' into DIV-1142

pull/170/head
ariannedee 5 years ago
parent
commit
f4d0773434
20 changed files with 946 additions and 516 deletions
  1. +2
    -1
      edivorce/apps/core/templates/prequalification/step_04.html
  2. +7
    -0
      edivorce/apps/core/tests/test_storage.py
  3. +42
    -0
      edivorce/apps/poc/migrations/0005_auto_20200921_1255.py
  4. +20
    -0
      edivorce/apps/poc/migrations/0006_auto_20200921_1301.py
  5. +23
    -0
      edivorce/apps/poc/migrations/0007_auto_20200921_1523.py
  6. +39
    -0
      edivorce/apps/poc/migrations/0008_auto_20200922_1027.py
  7. +18
    -0
      edivorce/apps/poc/migrations/0009_auto_20200922_1033.py
  8. +29
    -2
      edivorce/apps/poc/models.py
  9. +7
    -5
      edivorce/apps/poc/views.py
  10. +1
    -1
      edivorce/fixtures/Question.json
  11. +0
    -249
      vue/src/components/ItemTile.vue
  12. +0
    -256
      vue/src/components/Uploader.vue
  13. +155
    -0
      vue/src/components/Uploader/Image.vue
  14. +141
    -0
      vue/src/components/Uploader/ItemTile.vue
  15. +381
    -0
      vue/src/components/Uploader/Main.vue
  16. +63
    -0
      vue/src/components/Uploader/ModalPreview.vue
  17. +0
    -0
      vue/src/components/Uploader/ProgressBar.vue
  18. +1
    -1
      vue/src/pages/final-filing/FinalFiling.vue
  19. +1
    -1
      vue/src/pages/initial-filing/InitialFiling.vue
  20. +16
    -0
      vue/src/utils/rotation.js

+ 2
- 1
edivorce/apps/core/templates/prequalification/step_04.html View File

@ -169,6 +169,7 @@
<div class="reveal question-well" id="financial_support" hidden> <div class="reveal question-well" id="financial_support" hidden>
<h3>Are you financially supporting any of the children that are 19 <h3>Are you financially supporting any of the children that are 19
years or older?</h3> years or older?</h3>
<p>Financially supporting means that you are paying for the basic expenses (food, clothing, housing, etc.) for the child(ren).</p>
<p>Please check all that apply.</p> <p>Please check all that apply.</p>
<div class="checkbox-group"> <div class="checkbox-group">
<div class="checkbox"><label>{% input_field type="checkbox" name="children_financial_support" value="NO" id="no_children_financial_support" data_checkbox_radio="true" data_checkbox_radio_state_set="false" %}No</label></div> <div class="checkbox"><label>{% input_field type="checkbox" name="children_financial_support" value="NO" id="no_children_financial_support" data_checkbox_radio="true" data_checkbox_radio_state_set="false" %}No</label></div>
@ -237,7 +238,7 @@
</div> </div>
<div class="reveal question-well" id="children_live_with_others"> <div class="reveal question-well" id="children_live_with_others">
<h3>Do any of the children live with someone who is not you or your spouse?</h3>
<h3>Are any of the children that you are financially supporting living with someone who is not you or your spouse?</h3>
<div class="btn-radio-group" data-toggle="buttons"> <div class="btn-radio-group" data-toggle="buttons">
<label class="btn btn-radio"> <label class="btn btn-radio">


+ 7
- 0
edivorce/apps/core/tests/test_storage.py View File

@ -4,11 +4,14 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TransactionTestCase from django.test import TransactionTestCase
from redis.exceptions import ConnectionError from redis.exceptions import ConnectionError
from edivorce.apps.core.models import BceidUser
from edivorce.apps.core.redis import generate_unique_filename from edivorce.apps.core.redis import generate_unique_filename
from edivorce.apps.poc.models import Document from edivorce.apps.poc.models import Document
class UploadStorageTests(TransactionTestCase): class UploadStorageTests(TransactionTestCase):
def setUp(self):
self.user = BceidUser.objects.create(user_guid='1234')
@mock.patch('redis.connection.ConnectionPool.get_connection') @mock.patch('redis.connection.ConnectionPool.get_connection')
def test_storage_connection_error(self, mock_redis): def test_storage_connection_error(self, mock_redis):
@ -21,6 +24,7 @@ class UploadStorageTests(TransactionTestCase):
file = SimpleUploadedFile('file.txt', b'this is some content') file = SimpleUploadedFile('file.txt', b'this is some content')
test = Document() test = Document()
test.file = file test.file = file
test.bceid_user = self.user
test.save() test.save()
except ConnectionError: except ConnectionError:
connection_error = True connection_error = True
@ -37,6 +41,7 @@ class UploadStorageTests(TransactionTestCase):
file = SimpleUploadedFile('file.txt', b'this is some content') file = SimpleUploadedFile('file.txt', b'this is some content')
test = Document() test = Document()
test.file = file test.file = file
test.bceid_user = self.user
test.save() test.save()
self.assertTrue(mock_redis_save.called) self.assertTrue(mock_redis_save.called)
@ -51,6 +56,7 @@ class UploadStorageTests(TransactionTestCase):
file = SimpleUploadedFile('file.txt', b'this is some content') file = SimpleUploadedFile('file.txt', b'this is some content')
test = Document() test = Document()
test.file = file test.file = file
test.bceid_user = self.user
test.save() test.save()
self.assertTrue(mock_redis_save.called) self.assertTrue(mock_redis_save.called)
@ -85,6 +91,7 @@ class UploadStorageTests(TransactionTestCase):
file = SimpleUploadedFile('file.txt', b'this is some content') file = SimpleUploadedFile('file.txt', b'this is some content')
test = Document() test = Document()
test.bceid_user = self.user
test.file = file test.file = file
test.save() test.save()


+ 42
- 0
edivorce/apps/poc/migrations/0005_auto_20200921_1255.py View File

@ -0,0 +1,42 @@
# Generated by Django 2.2.15 on 2020-09-21 19:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0020_auto_20200903_2328'),
('poc', '0004_auto_20200917_1008'),
]
operations = [
migrations.RenameField(
model_name='document',
old_name='docType',
new_name='doc_type',
),
migrations.RenameField(
model_name='document',
old_name='partyId',
new_name='party_id',
),
migrations.AddField(
model_name='document',
name='bceid_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='uploads', to='core.BceidUser'),
),
migrations.AddField(
model_name='document',
name='length',
field=models.IntegerField(default=0),
),
migrations.AlterUniqueTogether(
name='document',
unique_together={('bceid_user', 'doc_type', 'party_id', 'filename', 'length')},
),
migrations.RunSQL(
sql='delete from poc_document'
)
]

+ 20
- 0
edivorce/apps/poc/migrations/0006_auto_20200921_1301.py View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.15 on 2020-09-21 20:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('poc', '0005_auto_20200921_1255'),
]
operations = [
migrations.AlterField(
model_name='document',
name='bceid_user',
field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='uploads', to='core.BceidUser'),
preserve_default=False,
),
]

+ 23
- 0
edivorce/apps/poc/migrations/0007_auto_20200921_1523.py View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.15 on 2020-09-21 22:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('poc', '0006_auto_20200921_1301'),
]
operations = [
migrations.AddField(
model_name='document',
name='order',
field=models.IntegerField(default=1),
),
migrations.AddField(
model_name='document',
name='rotation',
field=models.IntegerField(default=0),
),
]

+ 39
- 0
edivorce/apps/poc/migrations/0008_auto_20200922_1027.py View File

@ -0,0 +1,39 @@
# Generated by Django 2.2.15 on 2020-09-22 17:27
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0020_auto_20200903_2328'),
('poc', '0007_auto_20200921_1523'),
]
operations = [
migrations.RenameField(
model_name='document',
old_name='party_id',
new_name='party_code',
),
migrations.RenameField(
model_name='document',
old_name='length',
new_name='size',
),
migrations.RenameField(
model_name='document',
old_name='order',
new_name='sort_order',
),
migrations.AddField(
model_name='document',
name='date_uploaded',
field=models.DateTimeField(default=datetime.datetime.now),
),
migrations.AlterUniqueTogether(
name='document',
unique_together={('bceid_user', 'doc_type', 'party_code', 'filename', 'size')},
),
]

+ 18
- 0
edivorce/apps/poc/migrations/0009_auto_20200922_1033.py View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.15 on 2020-09-22 17:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('poc', '0008_auto_20200922_1027'),
]
operations = [
migrations.AlterField(
model_name='document',
name='date_uploaded',
field=models.DateTimeField(auto_now_add=True),
),
]

+ 29
- 2
edivorce/apps/poc/models.py View File

@ -1,5 +1,6 @@
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from edivorce.apps.core.models import BceidUser
from edivorce.apps.core import redis from edivorce.apps.core import redis
@ -9,12 +10,38 @@ class Document(models.Model):
This is only a POC model and should not be loaded on a production system. This is only a POC model and should not be loaded on a production system.
""" """
filename = models.CharField(max_length=128, null=True) # saving the original filename separately filename = models.CharField(max_length=128, null=True) # saving the original filename separately
""" File name and extension """
size = models.IntegerField(default=0)
""" Size of the file (size and name uniquely identify each file on the input) """
file = models.FileField(upload_to=redis.generate_unique_filename, storage=redis.RedisStorage()) file = models.FileField(upload_to=redis.generate_unique_filename, storage=redis.RedisStorage())
docType = models.CharField(max_length=4, null=True, blank=True)
partyId = models.IntegerField(default=0)
""" File temporarily stored in Redis """
doc_type = models.CharField(max_length=4, null=True, blank=True)
""" CEIS Document Type Code (2-4 letters) """
party_code = models.IntegerField(default=0)
""" 1 = You, 2 = Your Spouse, 0 = Shared """
sort_order = models.IntegerField(default=1)
""" file order (page number in the PDF) """
rotation = models.IntegerField(default=0)
""" 0, 90, 180 or 270 """
bceid_user = models.ForeignKey(BceidUser, related_name='uploads', on_delete=models.CASCADE)
""" User who uploaded the attachment """
date_uploaded = models.DateTimeField(auto_now_add=True)
""" Date the record was last updated """
class Meta:
unique_together = ("bceid_user", "doc_type", "party_code", "filename", "size")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.filename = self.file.name self.filename = self.file.name
self.size = self.file.size
super(Document, self).save(*args, **kwargs) super(Document, self).save(*args, **kwargs)


+ 7
- 5
edivorce/apps/poc/views.py View File

@ -39,21 +39,23 @@ class UploadScan(FormView):
class UploadStorage(CreateView): class UploadStorage(CreateView):
model = Document model = Document
fields = ['file', 'docType', 'partyId']
fields = ['file', 'doc_type', 'party_code']
template_name = "storage.html" template_name = "storage.html"
success_url = settings.FORCE_SCRIPT_NAME + 'poc/storage' success_url = settings.FORCE_SCRIPT_NAME + 'poc/storage'
@method_decorator(csrf_exempt) @method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
response = super(UploadStorage, self).dispatch(request, *args, **kwargs)
if response.status_code == 200:
return HttpResponse(status=204)
return response
return super(UploadStorage, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['documents'] = Document.objects.all() kwargs['documents'] = Document.objects.all()
return super(UploadStorage, self).get_context_data(**kwargs) return super(UploadStorage, self).get_context_data(**kwargs)
def form_valid(self, form):
obj = form.save(commit=False)
obj.bceid_user = self.request.user
return super(UploadStorage, self).form_valid(form)
class UploadStorageDelete(DeleteView): class UploadStorageDelete(DeleteView):
model = Document model = Document


+ 1
- 1
edivorce/fixtures/Question.json View File

@ -109,7 +109,7 @@
}, },
{ {
"fields": { "fields": {
"name": "Do any of the your children live with someone who is not you or your spouse?",
"name": "Are any of the children that you are financially supporting living with someone who is not you or your spouse?",
"description": "For pre-qualification step 4, determines eligibility of using the app", "description": "For pre-qualification step 4, determines eligibility of using the app",
"summary_order": 11, "summary_order": 11,
"required": "Conditional", "required": "Conditional",


+ 0
- 249
vue/src/components/ItemTile.vue View File

@ -1,249 +0,0 @@
<template>
<div class="item-tile" v-if="file.progress === '100.00' || file.error">
<div class="image-wrap" @click.prevent="showImage($event)">
<img v-if="file.objectURL && !file.error && file.type !== 'application/pdf'" :src="file.objectURL" :style="imageStyle"/>
<i class="fa fa-file-pdf-o" v-if="file.type === 'application/pdf'"></i>
<button type="button" class="btn-remove" @click.prevent="$emit('remove')" aria-label="Delete">
<i class="fa fa-times-circle"></i>
</button>
</div>
<div class="bottom-wrapper">
<div class="item-text">
{{file.name}} ({{ Math.round(file.size/1024 * 100) / 100 }}KB)
</div>
<div class="button-wrapper">
<div v-if="!file.active && file.success">
<button type="button" @click.prevent="$emit('moveup')" :disabled="index === 0" aria-label="Move down one position">
<i class="fa fa-chevron-circle-left"></i>
</button>
<button type="button" @click.prevent="$emit('movedown')" :disabled="index >= (fileCount - 1)" aria-label="Move up one position">
<i class="fa fa-chevron-circle-right"></i>
</button>
<button type="button" aria-label="Rotate counter-clockwise" @click.prevent="$emit('rotateleft')">
<i class="fa fa-undo"></i>
</button>
<button type="button" aria-label="Rotate clockwise" @click.prevent="$emit('rotateright')">
<i class="fa fa-undo fa-flip-horizontal"></i>
</button>
</div>
<div class="alert alert-danger" style="padding: 4px; margin-bottom: 0" v-if="file.error">Upload Error</div>
</div>
</div>
<modal v-model="showModal" ref="modal" :footer="false">
<img v-if="file.objectURL && !file.error && file.type !== 'application/pdf'" :src="file.objectURL" :style="imageStyle">
</modal>
</div>
<div v-else>
<ProgressBar :file="file"/>
</div>
</template>
<script>
import ProgressBar from './ProgressBar'
import { Modal } from 'uiv';
export default {
props: {
file: Object,
index: Number,
fileCount: Number
},
data: function () {
return {
showModal: false,
}
},
components: {
ProgressBar,
Modal
},
methods: {
showImage($event) {
if ($event.target.tagName !== 'I' && $event.target.tagName !== 'BUTTON') {
this.showModal = true;
}
}
},
computed: {
rotateVal() {
let rotation = this.file.rotation;
while (rotation < 0) {
rotation += 360;
}
while (rotation > 360) {
rotation -= 360;
}
if (rotation === 90) {
return 90;
}
if (rotation === 180) {
return 180;
}
if (rotation === 270) {
return 270;
}
return 0;
},
imageStyle() {
if (this.rotateVal === 90) {
let scale = this.file.width / this.file.height;
let yshift = -100 * scale;
return "transform:rotate(90deg) translateY("+yshift+"%) scale("+scale+"); transform-origin: top left;";
}
if (this.rotateVal === 270) {
let scale = this.file.width / this.file.height;
let xshift = -100 * scale;
return "transform:rotate(270deg) translateX("+xshift+"%) scale("+scale+"); transform-origin: top left;";
}
if (this.rotateVal === 180) {
return "transform:rotate(180deg);";
}
return '';
}
},
}
</script>
<style scoped lang="scss">
.item-tile {
margin-bottom: 5px;
position: relative;
.image-wrap {
height: 160px;
border: 1px solid black;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
background-color: white;
overflow: hidden;
position: relative;
z-index: 2;
i.fa-file-pdf-o {
color: silver;
display: block;
font-size: 50px;
margin-left: 15px;
margin-top: 15px;
opacity: 0.75;
}
&::after {
font-family: FontAwesome;
content: "\f06e";
position: absolute;
left: 58px;
top: 52px;
font-size: 43px;
color: transparent;
}
&:hover {
background-color: #6484d3;
cursor: pointer;
button.btn-remove {
background-color: transparent;
i.fa {
color: white;
}
}
}
&:hover::after {
color: white;
}
&:hover img {
opacity: 0.3;
}
}
.item-text {
text-align: center;
min-height: 75px;
max-height: 75px;
overflow: hidden;
padding: 5px;
line-height: 1.05;
font-size: 0.95em;
}
.button-wrapper {
text-align: center;
}
.bottom-wrapper {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
border: 1px solid silver;
background-color: #F2F2F2;
margin-bottom: 10px;
}
button {
position: relative;
z-index: 2;
background-color: transparent;
border: none;
outline: none;
font-size: 22px;
padding: 0;
margin-right: 16px;
&:disabled {
i.fa {
opacity: 0.15;
}
}
&:hover {
cursor: pointer !important;
}
i.fa {
color: #003366;
}
&:last-of-type {
margin-right: 0;
}
&:nth-of-type(2) {
margin-right: 32px;
}
&.btn-remove {
position: absolute;
top: 130px;
left: 130px;
background-color: white;
border-radius: 10px;
height: 22px;
line-height: 1;
z-index: 4;
i.fa {
color: #365EBE;
font-size: 23px;
}
}
}
}
</style>
<style lang="css">
.modal-content {
background-color: inherit;
box-shadow: none;
-webket-box-shadow: none;
border: none;
}
.modal-content button.close {
font-size: 80px;
margin-right: 75px;
margin-bottom: -100px;
}
</style>

+ 0
- 256
vue/src/components/Uploader.vue View File

@ -1,256 +0,0 @@
<template>
<div>
<h5 class="uploader-label">
{{ formInfo.preText }}
<a href="javascript:void(0)" :id="'Tooltip-' + uniqueId">
{{ formInfo.name }} <i class="fa fa-question-circle"></i>
</a>
<strong v-if="party === 1"> - For You</strong>
<strong v-if="party === 2"> - For Your Spouse</strong>
</h5>
<tooltip :text="formInfo.help" :target="'#Tooltip-' + uniqueId"></tooltip>
<label :for="inputId" class="sr-only">
{{ formInfo.preText }} {{ formInfo.name }}
<span v-if="party === 1"> - For You</span>
<span v-if="party === 2"> - For Your Spouse</span>
</label>
<div @dragover="draggingOn" @dragenter="draggingOn" @dragleave="draggingOff" @dragend="draggingOff" @drop="draggingOff">
<file-upload
ref="upload"
v-model="files"
:multiple="true"
:drop="true"
:drop-directory="false"
:post-action="postAction"
:input-id="inputId"
name="file"
:class="['drop-zone', dragging ? 'dragging' : '']"
:data="data"
@input-file="inputFile"
@input-filter="inputFilter">
<div v-if="files.length === 0" class="placeholder">
<i class="fa fa-plus-circle"></i><br>
<em>Drag and Drop the PDF document or JPG pages here,<br>or click here to Browse for files.</em>
</div>
<div v-else class="cards">
<div v-for="(file, index) in files" v-bind:key="index" class="card">
<item-tile
:file="file"
:index="index"
:file-count="files.length"
@remove="remove(file)"
@moveup="moveUp(index)"
@movedown="moveDown(index)"
@rotateleft="rotateLeft(index)"
@rotateright="rotateRight(index)"/>
</div>
<div class="card upload-button">
<div class="upload-button-wrapper">
<i class="fa fa-plus-circle"></i>
</div>
</div>
</div>
</file-upload>
</div>
</div>
</template>
<script>
import VueUploadComponent from 'vue-upload-component'
import { Tooltip } from 'uiv';
import ItemTile from './ItemTile'
import Forms from "../utils/forms";
export default {
props: {
docType: String,
party: { type: Number, default: 0 }
},
data: function () {
return {
files: [],
dragging: false
}
},
components: {
FileUpload: VueUploadComponent,
ItemTile,
Tooltip
},
computed: {
uniqueId() {
if (this.party === 0) {
return this.docType;
}
return this.docType + this.party;
},
inputId() {
return "Uploader-" + this.uniqueId;
},
formInfo() {
return Forms[this.docType];
},
postAction() {
return this.$parent.proxyRootPath + "poc/storage"
},
data() {
return {
docType: this.docType,
partyId: this.party
};
}
},
methods: {
/**
* Has changed
* @param Object|undefined newFile Read only
* @param Object|undefined oldFile Read only
* @return undefined
*/
inputFile(newFile, oldFile) {
if (newFile && oldFile && !newFile.active && oldFile.active) {
// Get response data
console.log('response', newFile.response)
if (newFile.xhr) {
// Get the response status code
console.log('status', newFile.xhr.status)
}
}
this.$refs.upload.active = true;
if (newFile) {
console.log('inputFile newFile=' + newFile.name);
}
if (oldFile) {
console.log('inputFile oldFile=' + oldFile.name);
}
},
/**
* Pretreatment
* @param Object|undefined newFile Read and write
* @param Object|undefined oldFile Read only
* @param Function prevent Prevent changing
* @return undefined
*/
inputFilter(newFile, oldFile, prevent) {
if (newFile && !oldFile) {
// Filter non-image file
if (!/\.(jpeg|jpg|png|pdf)$/i.test(newFile.name)) {
return prevent()
}
this.files.forEach(function(f) {
// prevent duplicates (based on filename and length)
if (f.name === newFile.name && f.length === newFile.length) {
return prevent();
}
});
}
// Add extra data to to the file object
if (newFile) {
newFile.objectURL = ''
let URL = window.URL || window.webkitURL
if (URL && URL.createObjectURL) {
newFile.objectURL = URL.createObjectURL(newFile.file)
newFile.rotation = 0;
const img = new Image();
img.onload = function() {
newFile.width = this.width;
newFile.height = this.height;
}
img.src = newFile.objectURL;
}
}
},
remove(file) {
this.$refs.upload.remove(file)
},
moveUp(old_index) {
if (old_index >= 1 && this.files.length > 1) {
this.files.splice(old_index - 1, 0, this.files.splice(old_index, 1)[0]);
}
},
moveDown(old_index) {
if (old_index <= this.files.length && this.files.length > 1) {
this.files.splice(old_index + 1, 0, this.files.splice(old_index, 1)[0]);
}
},
rotateLeft(index) {
this.files[index].rotation -= 90;
},
rotateRight(index) {
this.files[index].rotation += 90;
},
draggingOn() {
this.dragging = true;
},
draggingOff() {
this.dragging = false;
}
}
}
</script>
<style scoped lang="scss">
.drop-zone {
background-color: white;
width: 100%;
display: block;
text-align: left;
border: 2px #365EBE dashed;
border-radius: 6px;
padding: 18px;
&.dragging {
background-color: #F2E3F2;
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: left;
}
.card {
flex: 0 1 160px;
margin-bottom: 10px;
width: 160px;
margin-right: 18px;
&.upload-button {
display: flex;
flex-direction: column;
justify-content: center;
}
}
.fa-plus-circle {
font-size: 3rem;
margin-bottom: 8px;
color: #365EBE;
&:hover {
cursor: pointer;
}
}
.placeholder {
text-align: center;
}
}
h5.uploader-label {
display: block;
margin-top: 30px;
margin-bottom: 10px;
font-weight: normal;
font-size: 1em;
a {
font-weight: bold;
text-decoration: underline;
}
}
</style>

+ 155
- 0
vue/src/components/Uploader/Image.vue View File

@ -0,0 +1,155 @@
<template>
<div>
<div :class="['image-wrap', isValidImage ? 'valid' : '']" @click.prevent="showPreview($event)">
<img v-if="isValidImage" :src="file.objectURL" :style="imageStyle"/>
<i class="fa fa-file-pdf-o" v-if="file.type === 'application/pdf'"></i>
<button type="button" class="btn-remove" @click.prevent="$emit('removeclick')" aria-label="Delete">
<i class="fa fa-times-circle"></i>
</button>
</div>
<modal-preview :file="file"
:imageStyle="imageStyle"
:rotate-val="rotateVal"
:show-modal="showModal"
@close="closePreview()" />
</div>
</template>
<script>
import ModalPreview from './ModalPreview';
import rotateFix from '../../utils/rotation';
export default {
props: {
file: Object
},
components: {
ModalPreview
},
data: function () {
return {
showModal: false,
}
},
methods: {
showPreview($event) {
if (this.isValidImage) {
if ($event.target.tagName !== 'I' && $event.target.tagName !== 'BUTTON') {
this.showModal = true;
}
}
},
closePreview() {
this.showModal = false;
}
},
computed: {
isValidImage() {
return this.file.objectURL && !this.file.error && this.file.type !== 'application/pdf';
},
rotateVal() {
return rotateFix(this.file.rotation);
},
imageStyle() {
if (this.rotateVal === 90) {
let scale = this.file.width / this.file.height;
let yshift = -100 * scale;
return "transform:rotate(90deg) translateY("+yshift+"%) scale("+scale+"); transform-origin: top left;";
}
if (this.rotateVal === 270) {
let scale = this.file.width / this.file.height;
let xshift = -100 * scale;
return "transform:rotate(270deg) translateX("+xshift+"%) scale("+scale+"); transform-origin: top left;";
}
if (this.rotateVal === 180) {
return "transform:rotate(180deg);";
}
return '';
}
}
}
</script>
<style scoped lang="scss">
.image-wrap {
height: 160px;
border: 1px solid black;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
background-color: white;
overflow: hidden;
position: relative;
z-index: 2;
display: flex;
align-items: center;
img {
position: absolute;
top: 0;
left: 0;
}
i.fa-file-pdf-o {
color: #F40F02;
display: block;
font-size: 50px;
margin-left: 15px;
margin-top: 15px;
opacity: 0.75;
}
&::after {
font-family: FontAwesome;
content: "\f06e";
position: absolute;
left: 58px;
font-size: 43px;
color: transparent;
}
&.valid:hover {
background-color: #6484d3;
cursor: pointer;
button.btn-remove {
background-color: transparent;
i.fa {
color: white;
}
}
}
&:hover::after {
color: white;
}
&:hover img {
opacity: 0.3;
}
}
button {
&.btn-remove {
position: absolute;
top: 130px;
left: 130px;
background-color: white;
border-radius: 10px;
height: 18px;
line-height: 1;
z-index: 4;
i.fa {
color: #365EBE;
font-size: 23px;
&::before {
display: block;
margin-top: -2px;
margin-left: -1px;
}
}
}
}
</style>

+ 141
- 0
vue/src/components/Uploader/ItemTile.vue View File

@ -0,0 +1,141 @@
<template>
<div class="item-tile" v-if="file.progress === '100.00' || file.error">
<uploaded-image :file="file" :image-style="imageStyle" @imageclick="showPreview" @removeclick="$emit('remove')" />
<div class="bottom-wrapper">
<div class="item-text">
{{file.name}} <span class="no-wrap">({{ Math.round(file.size/1024/1024 * 100) / 100 }} MB)</span>
</div>
<div class="button-wrapper">
<div v-if="!file.active && file.success && !isPdf">
<button type="button" @click.prevent="$emit('moveup')" :disabled="index === 0" aria-label="Move down one position">
<i class="fa fa-chevron-circle-left"></i>
</button>
<button type="button" @click.prevent="$emit('movedown')" :disabled="index >= (fileCount - 1)" aria-label="Move up one position">
<i class="fa fa-chevron-circle-right"></i>
</button>
<button type="button" aria-label="Rotate counter-clockwise" @click.prevent="$emit('rotateleft')">
<i class="fa fa-undo"></i>
</button>
<button type="button" aria-label="Rotate clockwise" @click.prevent="$emit('rotateright')">
<i class="fa fa-undo fa-flip-horizontal"></i>
</button>
</div>
<div class="alert alert-danger" v-if="file.error">Upload Error</div>
</div>
</div>
</div>
<div v-else>
<progress-bar :file="file"/>
</div>
</template>
<script>
import UploadedImage from './Image'
import ProgressBar from './ProgressBar'
export default {
props: {
file: Object,
index: Number,
fileCount: Number
},
data: function () {
return {
showModal: false,
}
},
components: {
ProgressBar,
UploadedImage
},
methods: {
showPreview() {
this.showModal = true;
},
closePreview() {
this.showModal = false;
}
},
computed: {
isPdf() {
return this.file.type === 'application/pdf';
}
}
}
</script>
<style lang="scss">
.item-tile {
margin-bottom: 5px;
position: relative;
.item-text {
text-align: center;
min-height: 75px;
max-height: 75px;
overflow: hidden;
padding: 5px;
line-height: 1.05;
font-size: 0.95em;
.no-wrap {
white-space: nowrap;
}
}
.button-wrapper {
text-align: center;
}
.bottom-wrapper {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
border: 1px solid silver;
background-color: #F2F2F2;
margin-bottom: 10px;
.alert-danger {
margin-bottom: 0;
padding: 0;
}
}
}
</style>
<style lang="scss">
.item-tile {
button {
position: relative;
z-index: 2;
background-color: transparent;
border: none;
outline: none;
font-size: 22px;
padding: 0;
margin-right: 16px;
&:disabled {
i.fa {
opacity: 0.15;
}
}
&:hover {
cursor: pointer !important;
opacity: 0.8;
}
i.fa {
color: #003366;
}
&:last-of-type {
margin-right: 0;
}
&:nth-of-type(2) {
margin-right: 32px;
}
}
}
</style>

+ 381
- 0
vue/src/components/Uploader/Main.vue View File

@ -0,0 +1,381 @@
<template>
<div>
<h5 class="uploader-label">
{{ formDef.preText }}
<a href="javascript:void(0)" :id="'Tooltip-' + uniqueId">
{{ formDef.name }} <i class="fa fa-question-circle"></i>
</a>
<strong v-if="party === 1"> - For You</strong>
<strong v-if="party === 2"> - For Your Spouse</strong>
</h5>
<tooltip :text="formDef.help" :target="'#Tooltip-' + uniqueId"></tooltip>
<label :for="inputId" class="sr-only">
{{ formDef.preText }} {{ formDef.name }}
<span v-if="party === 1"> - For You</span>
<span v-if="party === 2"> - For Your Spouse</span>
</label>
<div @dragover="dragOn" @dragenter="dragOn" @dragleave="dragOff" @dragend="dragOff" @drop="dragOff">
<file-upload
ref="upload"
v-model="files"
:multiple="true"
:maximum="maxFiles"
:size="maxMegabytes * 1024 * 1024"
:drop="true"
:drop-directory="false"
:post-action="postAction"
:input-id="inputId"
name="file"
:class="['drop-zone', dragging ? 'dragging' : '']"
:data="inputKeys"
@input-file="inputFile"
@input-filter="inputFilter">
<div v-if="files.length === 0" class="placeholder">
<i class="fa fa-plus-circle"></i><br>
<em>Drag and Drop the PDF document or JPG pages here,<br>or click here to Browse for files.</em>
</div>
<template v-else>
<div class="text-danger error-top" v-if="fileErrors === 1">
<strong>One file has failed to upload to the server. Please remove the file marked 'Upload Error' and try uploading it again.</strong>
</div>
<div class="text-danger error-top" v-if="fileErrors > 1">
<strong>Some files have failed to upload to the server. Please remove the files marked 'Upload Error' and try uploading them again.</strong>
</div>
<div class="text-danger error-top" v-if="tooBig">
<strong>The total of all uploaded files for this form cannot exceed {{ maxMegabytes }} MB.
Please reduce the size of your files so the total is below this limit.</strong>
</div>
<div class="cards">
<div v-for="(file, index) in files" v-bind:key="index" class="card">
<item-tile
:file="file"
:index="index"
:file-count="files.length"
@remove="remove(file)"
@moveup="moveUp(index)"
@movedown="moveDown(index)"
@rotateleft="rotateLeft(index)"
@rotateright="rotateRight(index)"/>
</div>
<div class="card upload-button" v-if="!tooBig">
<div class="upload-button-wrapper">
<i class="fa fa-plus-circle"></i>
</div>
</div>
</div>
<div class="text-danger pull-right error-bottom" v-if="tooBig">
<em>
<strong>
(Total {{ Math.round(totalSize/1024/1024 * 100) / 100 }}
MB of {{ maxMegabytes }} MB)
</strong>
</em>
</div>
</template>
</file-upload>
</div>
<div class="pull-right" v-if="!tooBig">
<em>(Maximum {{ maxMegabytes }} MB)</em>
</div>
<modal ref="warningModal" v-model="showWarning">
{{ warningText }}
</modal>
</div>
</template>
<script>
import VueUploadComponent from 'vue-upload-component'
import { Tooltip, Modal } from 'uiv';
import ItemTile from './ItemTile'
import FormDefinitions from "../../utils/forms";
import rotateFix from '../../utils/rotation';
export default {
props: {
docType: String,
party: { type: Number, default: 0 }
},
data: function () {
return {
maxFiles: 30,
maxMegabytes: 10,
files: [],
dragging: false,
showWarning: false,
warningText: "",
isDirty: false
}
},
components: {
FileUpload: VueUploadComponent,
ItemTile,
Tooltip,
Modal
},
computed: {
inputId() {
return "Uploader-" + this.uniqueId;
},
inputKeys() {
return {
doc_type: this.docType,
party_code: this.party
};
},
formDef() {
return FormDefinitions[this.docType];
},
postAction() {
return this.$parent.proxyRootPath + "poc/storage"
},
uniqueId() {
if (this.party === 0) {
return this.docType;
}
return this.docType + this.party;
},
totalSize() {
let size = 0;
this.files.forEach((file) => {
if (!file.error) {
size += file.size;
}
});
return size;
},
fileErrors() {
let count = 0;
this.files.forEach((file) => {
if (file.error) {
count++;
}
});
return count;
},
tooBig() {
return this.totalSize > this.maxMegabytes * 1024 * 1024;
}
},
methods: {
inputFile(newFile, oldFile) {
// upload is complete
if (newFile && oldFile && !newFile.active && oldFile.active) {
// todo: send metadata to the server
console.log('Upload Complete; file=' + newFile.name)
this.saveMetaData();
if (newFile.xhr) {
// Get the response status code (we can use this for error handling)
if (newFile.xhr.status !== 200) {
// todo: handler errors
this.showError('Error: ' + newFile.xhr.statusText);
console.log('status', newFile.xhr.status)
}
}
}
this.$refs.upload.active = true;
},
inputFilter(newFile, oldFile, prevent) {
if (newFile && !oldFile) {
// Filter non-image file
if (!/\.(jpeg|jpg|gif|png|pdf)$/i.test(newFile.name)) {
this.showError('Unsupported file type. Allowed extensions are jpeg, jpg, gif,png and pdf.');
return prevent()
}
this.files.forEach((file) => {
// prevent duplicates (based on filename and length)
if (file.name === newFile.name && file.length === newFile.length) {
this.showError('Duplicate file: ' + newFile.name);
return prevent();
}
});
}
if (newFile) {
// make sure the user isn't uploading more MB of files than allowed
if (this.totalSize > this.maxMegabytes * 1024 * 1024) {
this.showError('The total of all uploaded files for this form cannot exceed ' + this.maxMegabytes + ' MB. Please reduce the size of your files so the total is below this limit.');
// only allow one file over the limit (so we can show the red messaging on the screen)
let previousTotalSize = 0;
this.files.forEach((file) => {
if ((file.name !== newFile.name || file.size !== newFile.size) && !file.error) {
previousTotalSize += file.size;
}
});
// if the user is more than one file over the limit, then block the upload
if (previousTotalSize > this.maxMegabytes * 1024 * 1024) {
this.$refs.upload.remove(newFile);
return prevent();
}
}
// if it's a PDF, make sure it's the only item being uploaded
if (newFile.type === 'application/pdf') {
if (this.files.length > 0) {
if (this.files[0].name != newFile.name || this.files[0].length != newFile.length) {
this.showError('Only one PDF is allowed per form, and PDF documents cannot be combined with images.');
this.$refs.upload.remove(newFile);
return prevent();
}
}
} else {
// if it's not a PDF, make sure there are no PDFs already uplaoded
this.files.forEach((file) => {
if (file.type === 'application/pdf') {
this.showError('PDF documents cannot be combined with images.');
this.$refs.upload.remove(newFile);
return prevent();
}
});
}
// Add extra data to to the file object
newFile.objectURL = ''
let URL = window.URL || window.webkitURL
if (URL && URL.createObjectURL) {
newFile.objectURL = URL.createObjectURL(newFile.file)
newFile.rotation = 0;
const img = new Image();
img.onload = function() {
newFile.width = this.width;
newFile.height = this.height;
}
img.src = newFile.objectURL;
}
}
},
remove(file) {
// todo: call the API to remove the file
this.$refs.upload.remove(file)
},
moveUp(old_index) {
if (old_index >= 1 && this.files.length > 1) {
this.files.splice(old_index - 1, 0, this.files.splice(old_index, 1)[0]);
}
this.isDirty = true;
},
moveDown(old_index) {
if (old_index <= this.files.length && this.files.length > 1) {
this.files.splice(old_index + 1, 0, this.files.splice(old_index, 1)[0]);
}
this.isDirty = true;
},
rotateLeft(index) {
this.files[index].rotation -= 90;
this.isDirty = true;
},
rotateRight(index) {
this.files[index].rotation += 90;
this.isDirty = true;
},
dragOn() {
this.dragging = true;
},
dragOff() {
this.dragging = false;
},
showError(message) {
this.warningText = message;
this.showWarning = true;
},
saveMetaData() {
let allFiles = [];
this.files.forEach((file) => {
allFiles.push({
filename: file.name,
size: file.size,
rotation: rotateFix(file.rotation)
});
});
const data = {
docType: this.docType,
partyCode: this.party,
files: allFiles
};
console.log('Call API', data);
}
},
created() {
// call the API to update the metadata every second, but only if the data has changed
// (throttling requests because rotating and re-ordering images can cause a lot of traffic)
setInterval(() => {
if (this.isDirty) {
this.saveMetaData();
this.isDirty = false;
}
}, 1000);
}
}
</script>
<style scoped lang="scss">
.drop-zone {
background-color: white;
width: 100%;
display: block;
text-align: left;
border: 2px #365EBE dashed;
border-radius: 6px;
padding: 18px;
&.dragging {
background-color: #F2E3F2;
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: left;
}
.card {
flex: 0 1 160px;
margin-bottom: 10px;
width: 160px;
margin-right: 18px;
&.upload-button {
display: flex;
flex-direction: column;
justify-content: center;
}
}
.fa-plus-circle {
font-size: 3rem;
margin-bottom: 8px;
color: #365EBE;
}
.placeholder {
text-align: center;
}
.error-top {
padding-bottom: 16px;
}
.error-bottom {
margin-bottom: -10px;
}
}
h5.uploader-label {
display: block;
margin-top: 30px;
margin-bottom: 10px;
font-weight: normal;
font-size: 1em;
a {
font-weight: bold;
text-decoration: underline;
}
}
</style>

+ 63
- 0
vue/src/components/Uploader/ModalPreview.vue View File

@ -0,0 +1,63 @@
<template>
<modal v-model="showModal" class="image-preview-modal" ref="modal" :footer="false" @hide="$emit('close')">
<img v-if="file.objectURL && !file.error && file.type !== 'application/pdf'" :src="file.objectURL" :style="modalImageStyle" :data-rotate="rotateVal">
</modal>
</template>
<script>
import { Modal } from 'uiv';
export default {
props: {
file: Object,
imageStyle: String,
rotateVal: Number,
showModal: Boolean
},
components: {
Modal
},
computed: {
modalImageStyle() {
let extraCss = '';
if (this.rotateVal === 90 || this.rotateVal === 270) {
extraCss = ' width: 100%; height: inherit !important;';
}
return this.imageStyle + extraCss;
}
},
}
</script>
<style lang="scss">
.image-preview-modal {
.modal-dialog {
max-width: 780px;
width: inherit;
text-align: center;
}
.modal-content {
background-color: transparent;
box-shadow: none;
-webket-box-shadow: none;
border: none;
.modal-body, .modal-header {
padding: 0 !important;
}
.modal-body {
img {
max-width: 100%;
}
}
button.close {
font-size: 40px;
color: white;
opacity: 1;
}
}
}
</style>

vue/src/components/ProgressBar.vue → vue/src/components/Uploader/ProgressBar.vue View File


+ 1
- 1
vue/src/pages/final-filing/FinalFiling.vue View File

@ -155,7 +155,7 @@
</template> </template>
<script> <script>
import Uploader from '../../components/Uploader.vue';
import Uploader from '../../components/Uploader/Main.vue';
export default { export default {
name: 'App', name: 'App',


+ 1
- 1
vue/src/pages/initial-filing/InitialFiling.vue View File

@ -117,7 +117,7 @@
</template> </template>
<script> <script>
import Uploader from '../../components/Uploader.vue';
import Uploader from '../../components/Uploader/Main.vue';
export default { export default {
name: 'App', name: 'App',


+ 16
- 0
vue/src/utils/rotation.js View File

@ -0,0 +1,16 @@
export default function(rotation) {
while (rotation < 0) {
rotation += 360;
}
while (rotation > 360) {
rotation -= 360;
}
switch (rotation) {
case 90:
case 180:
case 270:
return rotation;
default:
return 0;
}
};

Loading…
Cancel
Save