Further work on multi-file-upload

This commit is contained in:
Carl van Tonder 2018-04-23 01:15:33 -04:00
parent bb326bfed8
commit 935af1355b
13 changed files with 235 additions and 58 deletions

View File

@ -34,13 +34,13 @@ $ export DJANGO_SETTINGS_MODULE=ojusomap.settings
## Install the Python Dependencies ## Install the Python Dependencies
```bash ```bash
$ pip3 install -r requirements.txt $ pip3 install -r requirements-devel.txt
``` ```
If you run into issues with `psycopg2` you may need to run the following: If you run into issues with `psycopg2` you may need to run the following:
```bash ```bash
$ pip uninstall psycopg2 && pip install --no-binary :all: psycopg2 $ pip3 uninstall psycopg2 && pip3 install --no-binary :all: psycopg2
``` ```
## Run The Migrations ## Run The Migrations

View File

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-04-19 19:19 # Generated by Django 1.11.6 on 2018-04-23 02:20
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -11,7 +10,6 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('map', '0056_delete_shapefile'),
] ]
operations = [ operations = [
@ -20,8 +18,6 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to='.')), ('file', models.FileField(upload_to='.')),
('case_study', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='files', to='map.CaseStudy')),
('case_study_draft', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='files', to='map.CaseStudyDraft')),
], ],
options={ options={
'abstract': False, 'abstract': False,

View File

@ -7,16 +7,13 @@ class BaseFile(models.Model):
file = models.FileField( file = models.FileField(
upload_to='.', upload_to='.',
) )
case_study = models.ForeignKey(
CaseStudy, related_name='files', blank=True, null=True,
)
case_study_draft = models.ForeignKey(
CaseStudyDraft, related_name='files', blank=True, null=True
)
class Meta: class Meta:
abstract = True abstract = True
def __str__(self):
return self.file.name
class File(BaseFile): class File(BaseFile):
pass pass

View File

@ -1,9 +1,40 @@
$(function () { $(function () {
/* 2. INITIALIZE THE FILE UPLOAD COMPONENT */ // un-set "name" attributes to avoid submitting to server
$("input[type=file]").fileupload({ $(".fileupload").removeAttr('name');
// set up all file inputs for jQuery-fileUpload
$(".fileupload").fileupload({
dataType: 'json', dataType: 'json',
done: function (e, data) { /* 3. PROCESS THE RESPONSE FROM THE SERVER */ paramName: 'file',
console.log(data); done: function (e, data) {
// process server response
if (data.result.is_valid) {
var $field = $('#id_' + $(this).attr('data-field')),
$ul = $(this).siblings('ul'),
$li = $("<li>"),
$remove = $('<a title="remove"></a>'),
ids = $field.val().split(",").filter(function (v) {
return v != '';
});
ids.push(data.result.id);
if (!$ul.length) {
$ul = $("<ul>").insertAfter(this);
}
$li.text(data.result.name);
// FIXME AJAXify
//$remove.attr('href', '/files/' + data.result.id + '/delete/');
//$remove.append($('<i class="glyphicon glyphicon-remove"/>'));
//$li.append($remove);
$ul.append($li);
$field.val(ids.toString());
document.getElementById('case-study-form').dispatchEvent(new Event('dirty'))
}
} }
}); });
}); });

View File

@ -1,9 +1,10 @@
from django.conf.urls import url from django.conf.urls import url
from .views import FileUploadView from .views import FileUploadView, FileDeleteView
app_name = 'files' app_name = 'files'
urlpatterns = [ urlpatterns = [
url(r'^upload/?$', FileUploadView.as_view(), name='upload'), url(r'^upload/$', FileUploadView.as_view(), name='upload'),
url(r'^delete/(?P<pk>\d+)/$', FileDeleteView.as_view(), name='delete'),
] ]

View File

@ -1,19 +1,45 @@
from django.shortcuts import render from django.shortcuts import render
from django.http import JsonResponse from django.http import JsonResponse
from django.views.generic import CreateView from django.views.generic import FormView, DetailView
from .forms import FileForm from .forms import FileForm
from .models import File from .models import File
class FileUploadView(CreateView): class FileUploadView(FormView):
# FIXME require login
model = File model = File
form_class = FileForm form_class = FileForm
def form_valid(self, form): def form_valid(self, form):
# save the File to the database self.object = form.save()
super().form_valid(form)
return JsonResponse({'is_valid': True, 'url': self.object.file.url}) # FIXME set File owner
return JsonResponse({
'is_valid': True, 'url': self.object.file.url,
'name': self.object.file.name,
'id': self.object.pk
})
def form_invalid(self, form): def form_invalid(self, form):
return JsonResponse({'is_valid': False, 'errors': form.errors}) return JsonResponse({'is_valid': False, 'errors': form.errors})
class FileDeleteView(DetailView):
# FIXME require login
model = File
def get(self, request, *args, **kwargs):
return self.post(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# FIXME check file ownership
self.object = self.get_object()
self.object.delete()
return JsonResponse({
'success': True
})

View File

@ -9,7 +9,10 @@ from crispy_forms.bootstrap import Tab, TabHolder, PrependedText, FormActions
from dal import autocomplete from dal import autocomplete
from leaflet.forms.widgets import LeafletWidget from leaflet.forms.widgets import LeafletWidget
from apps.files.models import File
from .models import CaseStudy, SpatialRefSys from .models import CaseStudy, SpatialRefSys
from .widgets import CommaSeparatedTextInput
class MinimumZoomWidget(LeafletWidget): class MinimumZoomWidget(LeafletWidget):
@ -19,13 +22,6 @@ class MinimumZoomWidget(LeafletWidget):
class BaseCaseStudyForm(forms.models.ModelForm): class BaseCaseStudyForm(forms.models.ModelForm):
"""Base form class for the CaseStudy model.""" """Base form class for the CaseStudy model."""
official_project_documents = forms.FileField(
widget=forms.ClearableFileInput(attrs={
'multiple': True,
'data-url': reverse_lazy('files:upload'),
})
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BaseCaseStudyForm, self).__init__(*args, **kwargs) super(BaseCaseStudyForm, self).__init__(*args, **kwargs)
self.helper = FormHelper(self) self.helper = FormHelper(self)
@ -48,13 +44,6 @@ class BaseCaseStudyForm(forms.models.ModelForm):
}), }),
} }
class Media:
js = (
'files/jquery.ui.widget.js',
'files/jquery.iframe-transport.js',
'files/jquery.fileupload.js'
)
class ShortCaseStudyForm(BaseCaseStudyForm): class ShortCaseStudyForm(BaseCaseStudyForm):
"""Short version of the CaseStudy form.""" """Short version of the CaseStudy form."""
@ -95,6 +84,51 @@ class ShortCaseStudyForm(BaseCaseStudyForm):
class LongCaseStudyForm(BaseCaseStudyForm): class LongCaseStudyForm(BaseCaseStudyForm):
"""Long version of the CaseStudy form.""" """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',
}), required=False
)
official_project_documents_files = forms.ModelMultipleChoiceField(
queryset=File.objects.all(),
widget=CommaSeparatedTextInput(),
required=False
)
other_documents = forms.FileField(
widget=forms.ClearableFileInput(attrs={
'multiple': True,
'data-url': reverse_lazy('files:upload'),
'data-field': 'other_documents_files',
'class': 'fileupload',
}), required=False
)
other_documents_files = forms.ModelMultipleChoiceField(
queryset=File.objects.all(),
widget=CommaSeparatedTextInput(),
required=False
)
shapefiles = forms.FileField(
widget=forms.ClearableFileInput(attrs={
'multiple': True,
'data-url': reverse_lazy('files:upload'),
'data-field': 'shapefiles_files',
'class': 'fileupload',
}), required=False
)
shapefiles_files = forms.ModelMultipleChoiceField(
queryset=File.objects.all(),
widget=CommaSeparatedTextInput(),
required=False
)
coordinate_reference_system = forms.ModelChoiceField( coordinate_reference_system = forms.ModelChoiceField(
queryset=SpatialRefSys.objects.all(), queryset=SpatialRefSys.objects.all(),
widget=autocomplete.ModelSelect2(url='srs-autocomplete'), widget=autocomplete.ModelSelect2(url='srs-autocomplete'),
@ -288,8 +322,11 @@ class LongCaseStudyForm(BaseCaseStudyForm):
Tab( Tab(
_("Uploads"), _("Uploads"),
'official_project_documents', 'official_project_documents',
'official_project_documents_files',
'other_documents', 'other_documents',
'other_documents_files',
'shapefiles', 'shapefiles',
'shapefiles_files',
'coordinate_reference_system', 'coordinate_reference_system',
'name_of_territory_or_area', 'name_of_territory_or_area',
'shown_on_other_platforms', 'shown_on_other_platforms',
@ -302,3 +339,11 @@ class LongCaseStudyForm(BaseCaseStudyForm):
class Meta(BaseCaseStudyForm.Meta): class Meta(BaseCaseStudyForm.Meta):
exclude = ('approved',) exclude = ('approved',)
class Media:
js = (
'files/jquery.ui.widget.js',
'files/jquery.iframe-transport.js',
'files/jquery.fileupload.js',
'files/upload.js',
)

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-04-23 02:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0001_initial'),
('map', '0056_delete_shapefile'),
]
operations = [
migrations.RemoveField(
model_name='casestudy',
name='official_project_documents',
),
migrations.AddField(
model_name='casestudy',
name='official_project_documents',
field=models.ManyToManyField(blank=True, help_text='Attach any legal or official documents that relate to the project.', null=True, related_name='official_project_document_for', to='files.File', verbose_name='Official project documents'),
),
migrations.RemoveField(
model_name='casestudy',
name='other_documents',
),
migrations.AddField(
model_name='casestudy',
name='other_documents',
field=models.ManyToManyField(blank=True, help_text='Attach any other documents that relate to the project.', null=True, related_name='other_document_for', to='files.File', verbose_name='Other documents'),
),
migrations.RemoveField(
model_name='casestudy',
name='shapefiles',
),
migrations.AddField(
model_name='casestudy',
name='shapefiles',
field=models.ManyToManyField(blank=True, help_text='If you have territory that you would like to show in relation to this project - e.g. Bienes Comunales de Ixtepec etc. This is a set of 3 or more (often 5-6) files with file extensions like .cpg, .dbf, .prj, .qpj, .shp, .shx', null=True, related_name='shapefile_for', to='files.File', verbose_name='Shapefiles'),
),
]

View File

@ -17,8 +17,7 @@ from . import validators
class CaseStudyDraft(models.Model): class CaseStudyDraft(models.Model):
author = models.ForeignKey( author = models.ForeignKey(
User, User, on_delete=models.CASCADE
on_delete=models.CASCADE
) )
data = models.TextField() data = models.TextField()
@ -1001,31 +1000,31 @@ class CaseStudy(models.Model):
## ##
# 4.1 # 4.1
official_project_documents = models.FileField( official_project_documents = models.ManyToManyField(
'files.File',
related_name='official_project_document_for',
verbose_name=_("Official project documents"), verbose_name=_("Official project documents"),
help_text=_("Attach any legal or official documents that relate to the project."), help_text=_("Attach any legal or official documents that relate to the project."),
default=None,
null=True,
blank=True, blank=True,
) )
# 4.2 # 4.2
other_documents = models.FileField( other_documents = models.ManyToManyField(
'files.File',
related_name='other_document_for',
verbose_name=_("Other documents"), verbose_name=_("Other documents"),
help_text=_("Attach any other documents that relate to the project."), help_text=_("Attach any other documents that relate to the project."),
default=None,
null=True,
blank=True, blank=True,
) )
# 4.3.1 # 4.3.1
shapefiles = models.FileField( shapefiles = models.ManyToManyField(
'files.File',
related_name='shapefile_for',
verbose_name=_("Shapefiles"), verbose_name=_("Shapefiles"),
help_text=_("If you have territory that you would like to show in relation to this project - e.g. Bienes \ help_text=_("If you have territory that you would like to show in relation to this project - e.g. Bienes \
Comunales de Ixtepec etc. This is a set of 3 or more (often 5-6) files with file extensions like \ Comunales de Ixtepec etc. This is a set of 3 or more (often 5-6) files with file extensions like \
.cpg, .dbf, .prj, .qpj, .shp, .shx"), .cpg, .dbf, .prj, .qpj, .shp, .shx"),
default=None,
null=True,
blank=True blank=True
) )
@ -1078,7 +1077,6 @@ class CaseStudy(models.Model):
# Continue normal save method by calling original save method. # Continue normal save method by calling original save method.
super(CaseStudy, self).save(*args, **kwargs) super(CaseStudy, self).save(*args, **kwargs)
def is_video_youtube(self): def is_video_youtube(self):
return self.video.count("youtube.com") > 0 return self.video.count("youtube.com") > 0
@ -1086,7 +1084,6 @@ class CaseStudy(models.Model):
"""Gets the 11 character YouTube video ID from the video field.""" """Gets the 11 character YouTube video ID from the video field."""
return parse.parse_qs(parse.urlparse(self.video).query)["v"][0] return parse.parse_qs(parse.urlparse(self.video).query)["v"][0]
def is_video_vimeo(self): def is_video_vimeo(self):
return self.video.count("vimeo.com") > 0 return self.video.count("vimeo.com") > 0
@ -1094,7 +1091,6 @@ class CaseStudy(models.Model):
"""Gets the 11 number video ID from the video field.""" """Gets the 11 number video ID from the video field."""
return parse.urlparse(self.video).path return parse.urlparse(self.video).path
def get_negative_case_reasons_no_other(self): def get_negative_case_reasons_no_other(self):
"""Return a list of negative case reasons, minus the 'other' choice (if selected)""" """Return a list of negative case reasons, minus the 'other' choice (if selected)"""
choices = self.get_negative_case_reasons_list() choices = self.get_negative_case_reasons_list()

View File

@ -509,10 +509,10 @@ function initDrafts() {
var button = new SaveButton(document.querySelector('.savebutton')) var button = new SaveButton(document.querySelector('.savebutton'))
// Dirty forms set up // Dirty forms set up
$('#case-study-form').dirtyForms() $('#case-study-form').dirtyForms();
$('#case-study-form').on('dirty.dirtyforms', ev => { $('#case-study-form').on('dirty.dirtyforms', ev => {
button.switchStateUnsaved() button.switchStateUnsaved()
}) });
// Save button // Save button
button.element.button.addEventListener('click', evt => { button.element.button.addEventListener('click', evt => {

View File

@ -55,8 +55,25 @@ class BaseForm(LoginRequiredMixin, CreateView):
fail_silently=False, fail_silently=False,
) )
def form_invalid(self, form):
print(form.errors)
return super().form_invalid(form)
def form_valid(self, form): def form_valid(self, form):
from pdb import set_trace; set_trace()
self.object = form.save() self.object = form.save()
self.object.official_project_documents = form.cleaned_data.get(
'official_project_document_files', []
)
self.object.other_documents = form.cleaned_data.get(
'other_documents_files', []
)
self.object.shapefiles = form.cleaned_data.get(
'shapefiles_files', []
)
self.send_email() self.send_email()
# Delete the corresponding draft # Delete the corresponding draft
@ -140,6 +157,10 @@ class Drafts(View):
return HttpResponse(status=403) # Forbidden return HttpResponse(status=403) # Forbidden
draft = self.get_object(request) draft = self.get_object(request)
if draft != None: if draft != None:
draft.delete() draft.delete()
from pdb import set_trace; set_trace()
return HttpResponse(status=204) return HttpResponse(status=204)

21
apps/map/widgets.py Normal file
View File

@ -0,0 +1,21 @@
from django.forms import widgets
class CommaSeparatedTextInput(widgets.HiddenInput):
def format_value(self, value):
try:
value = ','.join(value)
except TypeError:
value = ''
return super().format_value(value)
def value_from_datadict(self, data, files, name):
value = super().value_from_datadict(data, files, name)
if value == '':
return None
try:
return value.split(',')
except AttributeError:
return None

View File

@ -68,12 +68,12 @@ urlpatterns = [
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^avatar/', include('avatar.urls')), url(r'^avatar/', include('avatar.urls')),
url(r'^cas/', include('cas_server.urls', namespace='cas_server')), url(r'^cas/', include('cas_server.urls', namespace='cas_server')),
url(r'^files/', include('apps.files.urls')),
# url(r'^contact/', include('apps.contact.urls'), name="contact"), # url(r'^contact/', include('apps.contact.urls'), name="contact"),
] ]
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
url(r'^accounts/profile/', include('apps.profiles.urls', namespace="profile")), url(r'^accounts/profile/', include('apps.profiles.urls', namespace="profile")),
url(r'^accounts/', include('registration.backends.default.urls')), url(r'^accounts/', include('registration.backends.default.urls')),
url(r'^files/', include('apps.files.urls')),
url(r'', include('apps.map.urls'), name="map"), url(r'', include('apps.map.urls'), name="map"),
) )