Browse Source

Merge branch 'master' of https://github.com/bcgov/eDivorce

pull/170/head
ariannedee 5 years ago
parent
commit
25a1e244ef
10 changed files with 200 additions and 78 deletions
  1. +25
    -0
      edivorce/apps/core/models.py
  2. +19
    -0
      edivorce/apps/core/tests/test_logic.py
  3. +4
    -2
      edivorce/apps/core/urls.py
  4. +15
    -27
      edivorce/apps/core/views/api.py
  5. +1
    -0
      edivorce/apps/poc/views.py
  6. +1
    -6
      vue/README.md
  7. +8
    -7
      vue/src/components/Uploader/Image.vue
  8. +51
    -13
      vue/src/components/Uploader/ItemTile.vue
  9. +6
    -3
      vue/src/components/Uploader/ProgressBar.vue
  10. +70
    -20
      vue/src/components/Uploader/Uploader.vue

+ 25
- 0
edivorce/apps/core/models.py View File

@ -1,3 +1,5 @@
import re
from django.contrib import admin
from django.db import models
from django.db.models import F
@ -173,10 +175,33 @@ class Document(models.Model):
def get_file_url(self):
return reverse('document', kwargs={'filename': self.filename, 'doc_type': self.doc_type, 'party_code': self.party_code, 'size': self.size})
def get_content_type(self):
return Document.content_type_from_filename(self.filename)
def update_sort_orders(self):
q = Document.objects.filter(bceid_user=self.bceid_user, doc_type=self.doc_type, party_code=self.party_code, sort_order__gt=self.sort_order)
q.update(sort_order=F('sort_order') - 1)
@staticmethod
def get_file(file_key):
return redis.RedisStorage().open(file_key)
@staticmethod
def content_type_from_filename(filename):
content_types = {
"pdf": "application/pdf",
"gif": "image/gif",
"png": "image/png",
"jpe": "image/jpeg",
"jpg": "image/jpeg",
"jpeg": "image/jpeg"
}
extension = re.split(r'[\._]', filename.lower())[-1]
content_type = content_types.get(extension)
if not content_type:
return "application/unknown"
return content_type
class DontLog:
def log_addition(self, *args):


+ 19
- 0
edivorce/apps/core/tests/test_logic.py View File

@ -5,6 +5,7 @@ from django.test import TestCase
from edivorce.apps.core.models import BceidUser, UserResponse
from edivorce.apps.core.utils.conditional_logic import get_cleaned_response_value, get_num_children_living_with
from edivorce.apps.core.utils.user_response import get_data_for_user
from edivorce.apps.core.models import Document
class ConditionalLogicTestCase(TestCase):
@ -51,3 +52,21 @@ class ConditionalLogicTestCase(TestCase):
self.assertEqual(get_num_children_living_with(self.questions_dict, 'Lives with you'), '1')
self.assertEqual(get_num_children_living_with(self.questions_dict, 'Lives with spouse'), '2')
self.assertEqual(get_num_children_living_with(self.questions_dict, 'Lives with both'), '3')
class ViewLogic(TestCase):
def test_content_type_from_filename(self):
self.assertEqual(Document.content_type_from_filename('test_file1.pdf'), 'application/pdf')
self.assertEqual(Document.content_type_from_filename('redis_key_test_file1_pdf'), 'application/pdf')
self.assertEqual(Document.content_type_from_filename('test_file2.png'), 'image/png')
self.assertEqual(Document.content_type_from_filename('redis_key_test_file2_png'), 'image/png')
self.assertEqual(Document.content_type_from_filename('Test File 3.GIF'), 'image/gif')
self.assertEqual(Document.content_type_from_filename('redis_key_test_file_3_GIF'), 'image/gif')
self.assertEqual(Document.content_type_from_filename('Test_File--4.JPEG'), 'image/jpeg')
self.assertEqual(Document.content_type_from_filename('redis_key_test_file_4_jpeg'), 'image/jpeg')
self.assertEqual(Document.content_type_from_filename('TestFile5.jpe'), 'image/jpeg')
self.assertEqual(Document.content_type_from_filename('redis_key_test_file_5_jpe'), 'image/jpeg')
self.assertEqual(Document.content_type_from_filename('testFile6.jpeg'), 'image/jpeg')
self.assertEqual(Document.content_type_from_filename('redis_key_testfile_6_jpeg'), 'image/jpeg')
self.assertEqual(Document.content_type_from_filename('test_file7.HEIC'), 'application/unknown')
self.assertEqual(Document.content_type_from_filename('redis_key_testfile_7_svgg'), 'application/unknown')

+ 4
- 2
edivorce/apps/core/urls.py View File

@ -7,8 +7,10 @@ urlpatterns = [
# url(r'^guide$', styleguide.guide),
url(r'^api/response$', api.UserResponseHandler.as_view()),
url(r'^api/documents/$', api.DocumentCreateView.as_view(), name='documents'),
path('api/documents/<doc_type>/<int:party_code>/', api.DocumentMetaDataView.as_view(), name='documents-meta'),
path('api/documents/<doc_type>/<int:party_code>/<int:size>/<filename>', api.DocumentView.as_view(), name='document'),
path('api/documents/<file_key>/', api.get_document_file_by_key, name='document_by_key'),
# we add an extra 'x' to the file extension so the siteminder proxy doesn't treat it as an image
path('api/documents/<doc_type>/<int:party_code>/<filename>x/<int:size>/', api.DocumentView.as_view(), name='document'),
# url(r'^login/headers$', system.headers),


+ 15
- 27
edivorce/apps/core/views/api.py View File

@ -1,12 +1,11 @@
import graphene
import graphene_django
from django.http import HttpResponse, HttpResponseGone
from django.http import Http404, HttpResponse, HttpResponseGone
from graphql import GraphQLError
from rest_framework import permissions, status
from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveUpdateDestroyAPIView
from rest_framework.views import APIView
from rest_framework.response import Response
from os.path import splitext
from ..models import Document, Question
from ..serializer import CreateDocumentSerializer, DocumentMetadataSerializer, UserResponseSerializer
@ -61,16 +60,6 @@ class DocumentCreateView(CreateAPIView):
queryset = Document.objects.all()
class DocumentMetaDataView(ListAPIView):
serializer_class = DocumentMetadataSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
doc_type = self.kwargs['doc_type']
party_code = self.kwargs['party_code']
return Document.objects.filter(doc_type=doc_type, party_code=party_code, bceid_user=self.request.user).order_by('sort_order')
class DocumentView(RetrieveUpdateDestroyAPIView):
serializer_class = DocumentMetadataSerializer
permission_classes = [permissions.IsAuthenticated]
@ -80,31 +69,30 @@ class DocumentView(RetrieveUpdateDestroyAPIView):
def retrieve(self, request, *args, **kwargs):
""" Return the file instead of meta data """
doc = self.get_object()
# Get the content-type based on the file extension
content_types = {
".pdf": "application/pdf",
".gif": "image/gif",
".png": "image/png",
".jpe": "image/jpeg",
".jpg": "image/jpeg",
".jpeg": "image/jpeg"
}
_, extension = splitext(doc.filename.lower())
content_type = content_types[extension]
document = self.get_object()
content_type = Document.content_type_from_filename(document.filename)
# If file doesn't exist anymore, delete it
try:
file_contents = doc.file.read()
file_contents = document.file.read()
except TypeError:
doc.delete()
document.delete()
return HttpResponseGone('File no longer exists')
return HttpResponse(file_contents, content_type=content_type)
def get_document_file_by_key(request, file_key):
file = Document.get_file(file_key)
content_type = Document.content_type_from_filename(file.name)
try:
return HttpResponse(file, content_type=content_type)
except TypeError:
raise Http404("File not found")
class DocumentType(graphene_django.DjangoObjectType):
file_url = graphene.String(source='get_file_url')
content_type = graphene.String(source='get_content_type')
class Meta:
model = Document


+ 1
- 0
edivorce/apps/poc/views.py View File

@ -60,6 +60,7 @@ class UploadStorage(CreateView):
class UploadStorageDelete(DeleteView):
model = Document
success_url = settings.FORCE_SCRIPT_NAME + 'poc/storage'
template_name = 'poc/document_confirm_delete.html'
def view_document_file(request, document_id):


+ 1
- 6
vue/README.md View File

@ -5,12 +5,7 @@
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
### Compile and minify
```
npm run build
```


+ 8
- 7
vue/src/components/Uploader/Image.vue View File

@ -92,11 +92,12 @@ export default {
i.fa-file-pdf-o, i.fa-frown-o {
display: block;
font-size: 105px;
margin-left: 32px;
margin-top: 15px;
position: absolute;
top: 0;
font-size: 48px;
margin-left: 60px;
}
i.fa-file-pdf-o {
color: #D5D5D5;
}
i.fa-frown-o {
@ -112,7 +113,7 @@ export default {
}
&.valid:hover {
background-color: #6484d3;
background-color: #365EBE;
cursor: pointer;
button.btn-remove {
@ -129,7 +130,7 @@ export default {
}
&:hover img {
opacity: 0.3;
opacity: 0.12;
}
}


+ 51
- 13
vue/src/components/Uploader/ItemTile.vue View File

@ -1,11 +1,16 @@
<template>
<div class="item-tile" v-if="file.progress === '100.00' || file.error">
<div :class="['item-tile', file.error ? 'error': '']" 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 class="filename-text">
{{file.name}}
</div>
<div class="size-text">
({{ Math.round(file.size/1024/1024 * 100) / 100 }} MB)
</div>
</div>
<div class="button-wrapper">
<div class="button-wrapper" v-if="file.error || file.type !== 'application/pdf'">
<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>
@ -68,35 +73,62 @@ export default {
.item-tile {
margin-bottom: 5px;
position: relative;
border: none !important;
.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;
padding: 7px 10px;
font-size: 16px;
line-height: 24px;
min-height: 87px;
.filename-text {
min-height: 25px;
max-height: 50px;
overflow: hidden;
overflow-wrap: anywhere;;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.size-text {
min-height: 25px;
max-height: 25px;
}
}
.button-wrapper {
margin-top: -4px;
text-align: center;
min-height: 32px;
}
.bottom-wrapper {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
border: 1px solid silver;
border-top: none;
background-color: #F2F2F2;
margin-bottom: 10px;
.alert-danger {
background-color: inherit;
border: none;
margin-bottom: 0;
padding: 0;
padding: 2px 0 0 0;
font-weight: 700;
font-size: 16px;
line-height: 24px;
}
}
&.error {
.bottom-wrapper {
background-color: #F7D4D5;
border: 1px solid #D8292F;
border-top: none;
}
}
}
@ -137,5 +169,11 @@ export default {
margin-right: 32px;
}
}
&.error {
button.btn-remove i.fa {
color: #D8292F;
}
}
}
</style>

+ 6
- 3
vue/src/components/Uploader/ProgressBar.vue View File

@ -3,7 +3,7 @@
<div class="item-tile" v-if="file.progress !== '0.00'">
<div class="status-wrap">
<div>
Uploading... {{ file.progress}}%
Uploading...<br>{{ file.progress}}%
</div>
<div class="progress">
<div :style="'width:' + file.progress + '%'">
@ -14,7 +14,7 @@
<div class="item-tile" v-else>
<div class="status-wrap">
<div>
Waiting...
Waiting...<br><br>
</div>
<div class="progress"></div>
</div>
@ -40,14 +40,17 @@ export default {
flex-direction: column;
justify-content: center;
text-align: center;
font-size: 16px;
.progress {
width: calc(100% - 1.5px);
background-color: #F2F2F3;
height: 22px;
position: relative;
bottom: -59.5px;
bottom: -54px;
left: 1px;
border-top-left-radius: 0;
border-top-right-radius: 0;
> div {
background-color: #365EBE;


+ 70
- 20
vue/src/components/Uploader/Uploader.vue View File

@ -57,7 +57,7 @@
@rotateleft="rotateLeft(index)"
@rotateright="rotateRight(index)"/>
</div>
<div class="card upload-button" v-if="!tooBig">
<div class="upload-button" v-if="!tooBig">
<div class="upload-button-wrapper">
<i class="fa fa-plus-circle"></i>
</div>
@ -163,8 +163,6 @@ export default {
// upload is complete
if (newFile && oldFile && !newFile.active && oldFile.active) {
this.saveMetaData();
if (newFile.xhr) {
// Error Handling
const statusCode = newFile.xhr.status;
@ -173,6 +171,8 @@ export default {
const message = JSON.parse(newFile.xhr.responseText)[0];
this.showError(message);
this.$refs.upload.remove(newFile);
} else if (statusCode === 403) {
this.showError('Error: Your user session has expired. Please log in again.');
} else if (statusCode !== 200 && statusCode !== 201 ) {
// 500 server error: show the status text and a generic message
this.showError('Error: ' + newFile.xhr.statusText + '. Please try the upload again. If this doesn\'t work, try again later.');
@ -241,15 +241,19 @@ export default {
}
// Add extra data to to the file object
newFile.objectURL = ''
newFile.objectURL = '';
newFile.width = 0;
newFile.height = 0;
newFile.rotation = 0;
let URL = window.URL || window.webkitURL
if (URL && URL.createObjectURL) {
newFile.objectURL = URL.createObjectURL(newFile.file)
newFile.rotation = 0;
const img = new Image();
const self = this;
img.onload = function() {
newFile.width = this.width;
newFile.height = this.height;
newFile.width = this.width || 0;
newFile.height = this.height || 0;
self.isDirty = true;
}
img.src = newFile.objectURL;
}
@ -258,10 +262,14 @@ export default {
remove(file) {
const urlbase = `${this.$parent.proxyRootPath}api/documents`;
const encFilename = encodeURIComponent(file.name);
const url = `${urlbase}/${this.docType}/${this.party}/${file.size}/${encFilename}`;
// we add an extra 'x' to the file extension so the siteminder proxy doesn't treat it as an image
const url = `${urlbase}/${this.docType}/${this.party}/${encFilename}x/${file.size}/`;
axios.delete(url)
.then(response => {
this.$refs.upload.remove(file)
var pos = this.files.findIndex(f => f.docType === file.docType && f.size === file.size);
if (pos > -1) {
this.files.splice(pos, 1);
}
})
.catch((error) => {
this.showError('Error deleting document from the server: ' + file.name);
@ -314,18 +322,25 @@ export default {
files: allFiles
};
const graphQLData = graphQLStringify(data,{singleQuotes: false, inlineCharacterLimit: 99999});
console.log('Call API', graphQLData);
const url = `${this.$parent.proxyRootPath}api/graphql/`;
axios.post(url, {
query: `
mutation updateMetadata {
updateMetadata(input:${graphQLData}){
documents{filename size width height rotation}
documents{filename size width height rotation contentType}
}
}
`})
.then(response => {
console.log('response', response);
// check for errors in the graphQL response
if (response.data.errors && response.data.errors.length) {
response.data.errors.forEach((error) => {
console.log('error', error.message || error);
// if there was an error it's probably because the upload isn't finished yet
// mark the metadata as dirty so it will save metadata again
this.isDirty = true;
})
}
})
.catch((error) => {
this.showError('Error saving metadata');
@ -334,6 +349,39 @@ export default {
}
},
created() {
// get saved state from the server
const url = `${this.$parent.proxyRootPath}api/graphql/`;
axios.post(url, {
query: `
query getMetadata {
documents(docType:"${this.docType}",partyCode:${this.party}) {
filename size width height rotation contentType
}
}
`,
variables: null})
.then(response => {
response.data.data.documents.forEach((doc) => {
this.files.push({
name: doc.filename,
size: doc.size,
width: doc.width,
height: doc.height,
rotation: doc.rotation,
type: doc.contentType,
error: false,
success: true,
progress: '100.00',
// we add an extra 'x' to the file extension so the siteminder proxy doesn't treat it as an image
objectURL: `${this.$parent.proxyRootPath}api/documents/${this.docType}/${this.party}/${doc.filename}x/${doc.size}/`
});
});
})
.catch((error) => {
this.showError('Error getting metadata');
console.log('error', error);
});
// 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 and possibly
@ -368,10 +416,11 @@ export default {
text-align: left;
border: 2px #365EBE dashed;
border-radius: 6px;
padding: 18px;
padding: 18px 32px 0 18px;
margin-bottom: 5px;
&.dragging {
background-color: #F2E3F2;
background-color: #D7DFF2;
}
.cards {
@ -385,14 +434,14 @@ export default {
margin-bottom: 10px;
width: 160px;
margin-right: 18px;
&.upload-button {
display: flex;
flex-direction: column;
justify-content: center;
}
}
.upload-button {
position: absolute;
right: 16px;
top: 17px;
}
.fa-plus-circle {
font-size: 3rem;
margin-bottom: 8px;
@ -401,6 +450,7 @@ export default {
.placeholder {
text-align: center;
margin-bottom: 18px;
}
.error-top {


Loading…
Cancel
Save