diff --git a/apps/files/static/files/upload.js b/apps/files/static/files/upload.js
index dbd858c..952724d 100644
--- a/apps/files/static/files/upload.js
+++ b/apps/files/static/files/upload.js
@@ -1,40 +1,193 @@
-$(function () {
- // un-set "name" attributes to avoid submitting to server
- $(".fileupload").removeAttr('name');
- // set up all file inputs for jQuery-fileUpload
- $(".fileupload").fileupload({
- dataType: 'json',
- paramName: 'file',
- done: function (e, data) {
- // process server response
- if (data.result.is_valid) {
- var $field = $('#id_' + $(this).attr('data-field')),
- $ul = $(this).siblings('ul'),
- $li = $("
"),
- $remove = $(''),
- ids = $field.val().split(",").filter(function (v) {
- return v != '';
- });
+class MultipleFilesWidget {
+ constructor(div) {
+ this.root = div
+ this.fieldName = this.root.getAttribute('data-field')
+ this.fileList = []
+ this.element = {
+ list: this.root.querySelector('.filewidget--list'),
+ uploadButton: this.root.querySelector('.filewidget--input'),
+ field: document.querySelector(`[name="${this.fieldName}"]`)
+ }
- ids.push(data.result.id);
-
- if (!$ul.length) {
- $ul = $("").insertAfter(this);
+ const self = this
+
+ // Set up jquery-fileupload
+ $(this.element.uploadButton).fileupload({
+ dataType: 'json',
+ paramName: 'file',
+ done: function (e, data) {
+ // process server response
+ if (!data.result.is_valid) {
+ throw new Error('Server sent us invalid data!')
}
- $li.text(data.result.name);
+ self.fileFinished.bind(self)(data.result)
+ },
+ add: function (e, data) {
+ $.each(data.files, (index, file) => {
+ self.addFile.bind(self)(null, file.name, self.setFileInProgress.bind(self))
+ });
- // FIXME AJAXify
- //$remove.attr('href', '/files/' + data.result.id + '/delete/');
- //$remove.append($(''));
- //$li.append($remove);
+ data.process().done(function () {
+ data.submit()
+ })
+ }
+ })
- $ul.append($li);
+ // Set up listening for restore
+ this.element.field.addEventListener('change', evt => {
+ let idList = evt.srcElement.value.split(",")
+ for (let id of idList) {
+ this.addFile(id, id, this.setFileDone.bind(this))
+ }
+ this.viewFileList()
+ })
+ }
- $field.val(ids.toString());
+ addFile(id, name, stateFunc) {
+ console.log('addfile here!')
- document.getElementById('case-study-form').dispatchEvent(new Event('dirty'))
+ let li = document.createElement('li')
+ li.className = 'filewidget--file'
+ li.innerHTML =
+ `
+ `
+
+ let file = {
+ root: li,
+ element: {
+ icon: li.querySelector('.filewidget--file--icon'),
+ name: li.querySelector('.filewidget--file--name'),
+ actions: li.querySelector('.filewidget--file--actions'),
+ },
+ name: name,
+ id: id || null,
+ }
+
+ stateFunc(file)
+
+ this.fileList.push(file)
+ this.viewFileList()
+ }
+
+ removeFile(file) {
+ // Remove file from display
+ this.element.list.removeChild(file.root)
+
+ // Remove file from fileList
+ this.fileList = this.fileList.filter(cmpFile => cmpFile === file)
+ }
+
+ fileFinished(serverResponse) {
+ // Using data, find the file
+ let file = this.fileList.filter(file => file.name === serverResponse.name)[0]
+
+ // Set the ID
+ file.id = serverResponse.id
+
+ // Set the file state now it's finished
+ this.setFileDone(file)
+ this.viewFileList()
+
+ // Do this last tho
+ this.updateFormField()
+ }
+
+ //
+ // Update the field that keeps track of file IDs
+ //
+
+ updateFormField() {
+ console.log("updateFormField", this)
+
+ let oldVal = this.element.field.value
+ this.element.field.value = this.fileList.filter(f => f.id != null)
+ .map(f => f.id)
+ .toString()
+
+ // Mark form as dirty
+ if (this.element.field.value !== oldVal) {
+ document.getElementById('case-study-form').dispatchEvent(new Event('dirty'))
+ }
+ }
+
+ //
+ // Manage the state of individual files in the widget
+ //
+
+ setFileState(file, state) {
+ file.element.icon.classList = `filewidget--file--icon ${state.iconClassList}`
+ file.element.name.innerText = state.fileName
+ file.element.actions.innerHTML = state.actions || ''
+ }
+
+ setFileInProgress(file) {
+ this.setFileState(file, {
+ iconClassList: 'fa fa-spinner fa-spin',
+ fileName: `${file.name} (uploading...)`
+ })
+ }
+
+ setFileDeletingInProgress(file) {
+ this.setFileState(file, {
+ iconClassList: 'fa fa-spinner fa-spin',
+ fileName: `Deleting...`
+ })
+ }
+
+ setFileDeletingFailed(file) {
+ this.setFileState(file, {
+ iconClassList: 'fa fa-exclamation-triangle',
+ fileName: `${file.name} (delete failed!)`
+ })
+ }
+
+ setFileDone(file) {
+ console.log("setFileDone", file)
+
+ this.setFileState(file, {
+ iconClassList: 'fa fa-file-o',
+ fileName: `${file.name}`,
+ actions: ''
+ })
+
+ file.element.actions.querySelector('a').addEventListener('click', () => {
+ this.setFileDeletingInProgress(file)
+
+ return fetch(`/files/delete/${file.id}/`, {
+ method: 'POST',
+ credentials: "same-origin",
+ headers: {
+ "X-CSRFToken": jQuery("[name=csrfmiddlewaretoken]").val(),
+ }
+ }).then(response => {
+ if (response.ok) {
+ this.removeFile(file)
+ } else {
+ this.setFileDeletingFailed(file)
+ }
+ })
+ })
+ }
+
+ //
+ // Redraw the file list
+ //
+
+ viewFileList() {
+ console.log("viewFileList", this)
+
+ for (let file of this.fileList) {
+ // Check if it's appended to the list
+ if (!this.element.list.contains(file.root)) {
+ this.element.list.appendChild(file.root)
}
}
- });
-});
+ }
+}
+
+$(function() {
+ window.official_project_documents = new MultipleFilesWidget(
+ document.querySelector('[data-field=official_project_documents_files]')
+ )
+})
diff --git a/apps/map/forms.py b/apps/map/forms.py
index cf74c80..7e441c6 100644
--- a/apps/map/forms.py
+++ b/apps/map/forms.py
@@ -81,15 +81,17 @@ class ShortCaseStudyForm(BaseCaseStudyForm):
]
+class BootstrapClearableFileInput(forms.ClearableFileInput):
+ template_name = 'map/forms/widgets/file.html'
+
+
class LongCaseStudyForm(BaseCaseStudyForm):
"""Long version of the CaseStudy form."""
official_project_documents = forms.FileField(
- widget=forms.ClearableFileInput(attrs={
- 'multiple': True,
- 'data-url': reverse_lazy('files:upload'),
- 'data-field': 'official_project_documents_files',
- 'class': 'fileupload',
+ widget=BootstrapClearableFileInput(attrs={
+ 'url': reverse_lazy('files:upload'),
+ 'field': 'official_project_documents_files',
}), required=False
)
@@ -100,11 +102,9 @@ class LongCaseStudyForm(BaseCaseStudyForm):
)
other_documents = forms.FileField(
- widget=forms.ClearableFileInput(attrs={
- 'multiple': True,
+ widget=BootstrapClearableFileInput(attrs={
'data-url': reverse_lazy('files:upload'),
'data-field': 'other_documents_files',
- 'class': 'fileupload',
}), required=False
)
@@ -115,11 +115,9 @@ class LongCaseStudyForm(BaseCaseStudyForm):
)
shapefiles = forms.FileField(
- widget=forms.ClearableFileInput(attrs={
- 'multiple': True,
+ widget=BootstrapClearableFileInput(attrs={
'data-url': reverse_lazy('files:upload'),
'data-field': 'shapefiles_files',
- 'class': 'fileupload',
}), required=False
)
diff --git a/apps/map/templates/map/form.html b/apps/map/templates/map/form.html
index 54a2705..f2d1ddd 100644
--- a/apps/map/templates/map/form.html
+++ b/apps/map/templates/map/form.html
@@ -33,6 +33,41 @@
border: 1px solid #ccc;
font-weight: bold;
}
+
+ .filewidget--input { position: absolute; left: -1000px; }
+ .filewidget--list {
+ list-style-type: none;
+ padding-left: 0;
+ margin-left: 0;
+ margin-top: 12px;
+ }
+
+ .filewidget--file {
+ border: 1px solid #aaa;
+ display: inline-block;
+ padding: 4px 8px;
+ width: 34em;
+ background-color: #eee;
+ border-radius: 4px;
+ cursor: default;
+ }
+
+ .filewidget--file--icon {
+ margin-right: 8px;
+ }
+
+ .filewidget--file--actions {
+ float: right;
+ }
+
+ .filewidget--file--actions a {
+ color: black;
+ cursor: pointer;
+ }
+
+ .filewidget--file--actions a:hover {
+ color: red;
+ }
{% endblock %}
diff --git a/apps/map/templates/map/forms/widgets/file.html b/apps/map/templates/map/forms/widgets/file.html
new file mode 100644
index 0000000..d17a136
--- /dev/null
+++ b/apps/map/templates/map/forms/widgets/file.html
@@ -0,0 +1,7 @@
+