Skip to content

Commit 53b7513

Browse files
authored
Improved Field.file/Field.image, added Field.dropfile/Field.dropimage (iommirocks#704)
1 parent 82b10cd commit 53b7513

File tree

23 files changed

+1354
-131
lines changed

23 files changed

+1354
-131
lines changed

docs/test_doc_cookbook_edit_tables.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ def test_how_do_you_edit_one_to_one_in_a_table(black_sabbath):
3434
Include them in `auto__include`. Say you have a profile model for an artist:
3535
"""
3636

37+
# @test
3738
profile = Profile.objects.create(artist=black_sabbath)
39+
# @end
3840

3941
# language=rst
4042
"""
@@ -68,7 +70,7 @@ def test_how_do_you_edit_one_to_one_in_a_table(black_sabbath):
6870
# @end
6971

7072

71-
def test_how_do_I_change_delete_to_checkboxes():
73+
def test_how_do_i_change_delete_to_checkboxes(ozzy):
7274
# language=rst
7375
"""
7476
.. _edit-table-delete-as-checkbox:
@@ -79,6 +81,10 @@ def test_how_do_I_change_delete_to_checkboxes():
7981
Just add `data-iommi-edit-table-delete-with="checkbox"`:
8082
"""
8183

84+
# @test
85+
Profile.objects.create(artist=ozzy)
86+
# @end
87+
8288
edit_table = EditTable(
8389
auto__model=Profile,
8490
auto__include=['artist__name'],
@@ -92,3 +98,32 @@ def test_how_do_I_change_delete_to_checkboxes():
9298
# @test
9399
show_output(edit_table)
94100
# @end
101+
102+
103+
def test_how_do_i_include_labels_for_fields(ozzy):
104+
# language=rst
105+
"""
106+
.. _edit-table-include-field-labels:
107+
108+
How do I include labels for fields?
109+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
110+
111+
If you're using `EditTable` and not `EditTable.div`, then by default you get fields rendered without labels,
112+
because the label text is in the table header. But in case you still want to render labels (e.g. as floating labels),
113+
you can just set `extra_evaluated__input_labels_include = True`:
114+
"""
115+
116+
# @test
117+
Profile.objects.create(artist=ozzy)
118+
# @end
119+
120+
edit_table = EditTable(
121+
auto__model=Profile,
122+
auto__include=['artist__name'],
123+
columns__artist_name__field__include=True,
124+
extra_evaluated__input_labels_include=True,
125+
)
126+
127+
# @test
128+
show_output(edit_table)
129+
# @end

docs/test_doc_cookbook_forms.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from pathlib import Path
2+
3+
from django.core.files import File
4+
from django.db.models import FileField, ImageField
15
from django.contrib.auth.models import User
26
from django.core.exceptions import ValidationError
37
from django.utils.translation import gettext_lazy
@@ -1442,3 +1446,46 @@ class Meta:
14421446
14431447
The same way you can also use layouts for filter forms via `Table.query__form__layout`
14441448
"""
1449+
1450+
1451+
def test_dropfile_dropimage_field():
1452+
# language=rst
1453+
"""
1454+
.. _dropfile-dropimage:
1455+
1456+
How to make cool drag&drop file or image fields?
1457+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1458+
1459+
If you don't want a regular file-inputs, you can use `Field.dropfile` and `Field.dropimage`:
1460+
"""
1461+
1462+
# @test
1463+
existing_files = []
1464+
for fn in ("Black Sabbath Vol 4.jpg", "Heaven And Hell.jpg"):
1465+
with open(Path(__file__).resolve().parent.parent / "examples" / "examples" / "static" / "album_art" / "Black Sabbath" / fn, "rb") as f:
1466+
_file = File(f)
1467+
_file.url = f'https://github.com/iommirocks/iommi/blob/master/examples/examples/static/album_art/Black%20Sabbath/{fn}?raw=true'
1468+
existing_files.append(_file)
1469+
# @end
1470+
1471+
form = Form.create(
1472+
fields__my_document=Field.dropfile(display_name="My document"),
1473+
fields__my_image=Field.dropimage(display_name="My image"),
1474+
fields__existing_files=Field.dropfile(
1475+
display_name="Existing multiple files",
1476+
is_list=True,
1477+
initial=existing_files,
1478+
),
1479+
)
1480+
1481+
# @test
1482+
show_output(form)
1483+
# @end
1484+
1485+
# language=rst
1486+
"""
1487+
And of course you can also use `registrations </registrations.html#django-custom-fields>`__ to enable drag&drop for all file/image fields:
1488+
"""
1489+
1490+
register_field_factory(FileField, shortcut_name='dropfile')
1491+
register_field_factory(ImageField, shortcut_name='dropimage')

iommi/edit_table.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
)
1010

1111
from django.db.models import QuerySet
12+
from django.db.models.fields.files import FieldFile
1213
from django.http import HttpResponseRedirect
13-
from django.template import Context
1414
from django.utils.safestring import mark_safe
1515
from django.utils.translation import gettext_lazy
1616

@@ -23,6 +23,7 @@
2323
Actions,
2424
group_actions,
2525
)
26+
from iommi.error import Errors
2627
from iommi.sort_after import LAST
2728
from iommi.base import (
2829
MISSING,
@@ -115,24 +116,26 @@ def render_cell_contents(self):
115116

116117
bind_field_from_instance(field, self.row)
117118

118-
if self.table.extra_evaluated.render_inputs_only or 'hidden' in getattr(field, 'iommi_shortcut_stack', []):
119-
input_html = field.input.__html__()
120-
else:
121-
input_html = field.__html__()
122-
123-
field.attr = orig_attr
124-
125119
# Check both edit_errors and create_errors
126120
errors = None
127121
if self.table.edit_errors:
128122
errors = self.table.edit_errors.get(path)
129123
if not errors and self.table.create_errors:
130124
errors = self.table.create_errors.get(path)
131125

132-
if errors:
133-
return Template(
134-
'{{ input_html }}<br><span class="text-danger"><ul class="errors">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul></span>'
135-
).render(context=Context(dict(input_html=input_html, errors=errors)))
126+
if self.table.extra_evaluated.render_inputs_only or 'hidden' in getattr(field, 'iommi_shortcut_stack', []):
127+
input_html = field.input.__html__()
128+
else:
129+
if self.table.extra_evaluated.input_labels_include is False:
130+
field.label.include = False
131+
if errors:
132+
field._errors = errors
133+
134+
input_html = field.__html__()
135+
136+
field.errors = Errors(parent=field)
137+
138+
field.attr = orig_attr
136139

137140
return input_html
138141
else:
@@ -143,6 +146,7 @@ def on_refine_done(self):
143146
self.value = None
144147
super(EditCell, self).on_refine_done()
145148

149+
146150
def bind_field_from_instance(field, instance):
147151
field.input = field.iommi_namespace.input(_name='input')
148152
field.non_editable_input = field.iommi_namespace.non_editable_input(_name='non_editable_input')
@@ -337,6 +341,7 @@ def save(cells_iterator, form):
337341
instance = cells.row
338342
form.instance = instance
339343
attrs_to_save = []
344+
to_save_in_second_phase = []
340345
for cell in cells.iter_editable_cells():
341346
path = cell.get_path()
342347
if path not in parsed_data:
@@ -345,20 +350,25 @@ def save(cells_iterator, form):
345350
field = form.fields[cell.column.iommi_name()]
346351
field._iommi_path_override = path
347352
if cells.is_create_template or (
348-
field.invoke_callback(field.read_from_instance, instance=instance) != value
353+
(instance_value := field.invoke_callback(field.read_from_instance, instance=instance)) != value
349354
):
350-
field.invoke_callback(field.write_to_instance, instance=instance, value=value)
351-
if not field.extra.get('django_related_field', False):
355+
if field.extra.get('django_related_field', False):
356+
if not cells.is_create_template and isinstance(instance_value, FieldFile) and not instance_value and value is None:
357+
# instance value of FileField/ImageField is an empty FieldFile/ImageFieldFile, not None
358+
pass
359+
else:
360+
to_save_in_second_phase.append((field, value))
361+
else:
362+
field.invoke_callback(field.write_to_instance, instance=instance, value=value)
352363
attrs_to_save.append(field.attr)
353364

354-
to_save.append((instance, attrs_to_save))
365+
to_save.append((instance, attrs_to_save, to_save_in_second_phase))
355366

356367
to_save.sort(key=lambda x: abs(x[0].pk))
357-
for instance, attrs_to_save in to_save:
358-
if not to_save:
359-
pass
368+
for instance, attrs_to_save, to_save_in_second_phase in to_save:
360369
if instance.pk is not None and instance.pk < 0:
361370
instance.pk = None
371+
362372
if instance.pk is None:
363373
attrs_to_save = None
364374

@@ -371,6 +381,11 @@ def save(cells_iterator, form):
371381
model_object = getattr_path(model_object, prefix)
372382
model_object.save(update_fields=[strip_prefix(x, prefix=f'{prefix}__') for x in attrs_to_save if x.startswith(prefix)])
373383

384+
if to_save_in_second_phase:
385+
for field, value in to_save_in_second_phase:
386+
field.invoke_callback(field.write_to_instance, instance=instance, value=value)
387+
instance.save()
388+
374389
save(table.cells_for_rows(), table.edit_form)
375390
save(table.cells_for_rows_for_create(save=True), table.create_form)
376391

@@ -459,7 +474,8 @@ class Meta:
459474

460475
reorderable = False
461476

462-
extra_evaluated__render_inputs_only = lambda table, **_: table.row.layout is None
477+
extra_evaluated__render_inputs_only = False
478+
extra_evaluated__input_labels_include = lambda table, **_: table.row.layout is not None
463479

464480
bulk__include = True
465481

@@ -651,8 +667,9 @@ def parse_virtual_pk(k):
651667
except ValueError:
652668
return None
653669

654-
post_data = self.get_request().POST
655-
pks = {parse_virtual_pk(k) for k in keys(post_data)}
670+
request = self.get_request()
671+
post_data = request.POST
672+
pks = {parse_virtual_pk(k) for k in {*keys(post_data), *keys(request.FILES)}}
656673
virtual_pks = [
657674
k for k in pks
658675
if k is not None and k < 0 and f'{delete_prefix}{k}' not in post_data

0 commit comments

Comments
 (0)