diff --git a/opentech/apply/stream_forms/fields.py b/opentech/apply/stream_forms/fields.py index 7517213d0dd9602724011d1f5a453ab82a96b2d8..6f542f221af663b98bd4ed7ecc69e32c0a8d307e 100644 --- a/opentech/apply/stream_forms/fields.py +++ b/opentech/apply/stream_forms/fields.py @@ -1,25 +1,67 @@ -from django.forms import FileInput, FileField +from django.forms import ClearableFileInput, FileField, CheckboxInput -class MultiFileInput(FileInput): +class MultiFileInput(ClearableFileInput): """ File Input only returns one file from its clean method. This passes all files through the clean method and means we have a list of files available for post processing """ - def __init__(self, *args, attrs={}, **kwargs): - attrs['multiple'] = True - super().__init__(*args, attrs=attrs, **kwargs) + template_name = 'stream_forms/fields/multi_file_field.html' + + input_text = '' + + def __init__(self, *args, **kwargs): + self.multiple = kwargs.pop('multiple', True) + super().__init__(*args, **kwargs) + + def is_initial(self, value): + is_initial = super().is_initial + return all( + is_initial(file) for file in value + ) + + def render(self, name, value, attrs=dict()): + if self.multiple: + attrs['multiple'] = 'multiple' + + return super().render(name, value, attrs) def value_from_datadict(self, data, files, name): - return files.getlist(name) + if hasattr(files, 'getlist'): + upload = files.getlist(name) + else: + upload = files.get(name) + if not isinstance(upload, list): + upload = [upload] + + checkbox_name = self.clear_checkbox_name(name) + '-' + checkboxes = {k for k in data if checkbox_name in k} + cleared = { + int(checkbox.replace(checkbox_name, '')) for checkbox in checkboxes + if CheckboxInput().value_from_datadict(data, files, checkbox) + } + + return { + 'files': upload, + 'cleared': cleared, + } class MultiFileField(FileField): widget = MultiFileInput def clean(self, value, initial): - if not value: + files = value['files'] + cleared = value['cleared'] + if not files and not cleared: return initial - return [FileField().clean(file, initial) for file in value] + new = [FileField().clean(file, initial) for file in files] + + if initial: + old = [file for i, file in enumerate(initial) if i not in cleared] + else: + old = [] + + return old + new diff --git a/opentech/apply/stream_forms/files.py b/opentech/apply/stream_forms/files.py index e31b07cf76dc86a772b750c97145ef4493fbe8fb..1d5b8ce5d7df2b780b2e70269fa8bffc227dfe73 100644 --- a/opentech/apply/stream_forms/files.py +++ b/opentech/apply/stream_forms/files.py @@ -22,8 +22,15 @@ class StreamFieldFile(File): self.filename = filename or os.path.basename(self.name) self._committed = False + def __str__(self): + return self.filename + def __eq__(self, other): - return self.filename == other.filename and self.size == other.size + if isinstance(other, File): + return self.filename == other.filename and self.size == other.size + # Rely on the other object to know how to check equality + # Could cause infinite loop if the other object is unsure how to compare + return other.__eq__(self) def _get_file(self): if getattr(self, '_file', None) is None: diff --git a/opentech/apply/stream_forms/templates/stream_forms/fields/multi_file_field.html b/opentech/apply/stream_forms/templates/stream_forms/fields/multi_file_field.html new file mode 100644 index 0000000000000000000000000000000000000000..e48370c3e80349b72ab6c42389e0379262f721c8 --- /dev/null +++ b/opentech/apply/stream_forms/templates/stream_forms/fields/multi_file_field.html @@ -0,0 +1,17 @@ +{% if widget.is_initial %}{{ widget.initial_text }}: +<p> +{{ widget.clear_checkbox_label }} +</p> +{% for file in widget.value %} +<p> + {% with y=forloop.counter0|stringformat:"s" %} + {% with file_id=widget.checkbox_name|add:'-'|add:y %} + <input type="checkbox" name="{{ file_id }}" id="{{ file_id }}"> + <label for="{{ file_id }}"></label> + {% endwith %} + {% endwith %} + <a href="{{ file.url }}">{{ file }}</a> +</p> +{% endfor %} +{{ widget.input_text }}{% endif %} +<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}> diff --git a/opentech/apply/stream_forms/tests.py b/opentech/apply/stream_forms/tests.py index a79ca8be565f44aacce95bad20c1ee34d175ed20..5bdf4b189f2951344bc06e496c9bf9a4f9a2283a 100644 --- a/opentech/apply/stream_forms/tests.py +++ b/opentech/apply/stream_forms/tests.py @@ -1,3 +1,89 @@ -# from django.test import TestCase +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase -# Create your tests here. +from faker import Faker + +from .files import StreamFieldFile +from .fields import MultiFileField, MultiFileInput + +fake = Faker() + + +def make_files(number): + file_names = [f'{fake.word()}_{i}' for i in range(3)] + files = [ + StreamFieldFile(SimpleUploadedFile(name, b'Some File Content'), filename=name) + for name in file_names + ] + return files + + +class TestMultiFileInput(TestCase): + widget = MultiFileInput() + + def test_renders_multiple_attr(self): + html = self.widget.render('', []) + self.assertIn('multiple', html) + + def test_renders_multiple_files(self): + files = make_files(3) + html = self.widget.render('', files) + for file in files: + self.assertIn(file.filename, html) + + def test_handles_files(self): + field_name = 'testing' + files = make_files(3) + files_data = {field_name: files} + data = self.widget.value_from_datadict({}, files_data, field_name) + self.assertEqual(data['files'], files) + + def test_no_delete(self): + data = self.widget.value_from_datadict({}, {}, '') + self.assertFalse(data['cleared']) + + def test_delete(self): + field_name = 'testing' + field_id = self.widget.clear_checkbox_name(field_name) + '-' + form_data = { + field_id + '0': 'on', + field_id + '4': 'on', + } + data = self.widget.value_from_datadict(form_data, {}, field_name) + self.assertEqual(data['cleared'], {0, 4}) + + +class TestMultiFileField(TestCase): + field = MultiFileField() + + def multi_file_value(self, files=list(), cleared=set()): + return { + 'files': files, + 'cleared': cleared, + } + + def test_returns_files_if_no_change(self): + files = make_files(3) + cleaned = self.field.clean(self.multi_file_value(), files) + self.assertEqual(files, cleaned) + + def test_returns_new_files(self): + files = make_files(3) + cleaned = self.field.clean(self.multi_file_value(files=files), None) + self.assertEqual(files, cleaned) + + def test_returns_inital_and_files(self): + initial_files = make_files(3) + new_files = make_files(3) + cleaned = self.field.clean(self.multi_file_value(files=new_files), initial_files) + self.assertEqual(initial_files + new_files, cleaned) + + def test_returns_nothing_all_cleared(self): + initial_files = make_files(3) + cleaned = self.field.clean(self.multi_file_value(cleared=range(3)), initial_files) + self.assertEqual([], cleaned) + + def test_returns_something_some_cleared(self): + initial_files = make_files(3) + cleaned = self.field.clean(self.multi_file_value(cleared=[0, 2]), initial_files) + self.assertEqual([initial_files[1]], cleaned)