Repository: liqd/a4-opin

Issue:
Upload widget doesn't allow delete and update
If the users deletes the image and uploads one at the same time, an unexpected error message shows up. We could either make the message more clear or prevent this kind of operation from the ui.



Discussion:

Resolving patch:
diff --git a/euth/contrib/services.py b/euth/contrib/services.py
--- a/euth/contrib/services.py
+++ b/euth/contrib/services.py
@@ -19,7 +19,8 @@ def delete_ratings(contenttype, pk):
 
 
 def delete_images(imagefields):
-    for image in imagefields:
-        thumbnailer = get_thumbnailer(image)
+    for imagefield in imagefields:
+        thumbnailer = get_thumbnailer(imagefield)
         thumbnailer.delete_thumbnails()
-        image.delete(False)
+        if imagefield.name:
+            imagefield.storage.delete(imagefield.name)
diff --git a/euth/contrib/widgets.py b/euth/contrib/widgets.py
--- a/euth/contrib/widgets.py
+++ b/euth/contrib/widgets.py
@@ -2,6 +2,7 @@
 
 from django.contrib.staticfiles.storage import staticfiles_storage
 from django.forms import widgets
+from django.template import loader
 from django.utils import formats
 from django.utils.html import conditional_escape
 from django.utils.safestring import mark_safe
@@ -16,94 +17,78 @@ class ImageInputWidget(widgets.ClearableFileInput):
     Allows to upload and delete uploaded files. It doesn't passing attributes
     using the positional `attrs` argument and hard codes css files.
     """
+    class Media:
+        js = (staticfiles_storage.url('js/imageUploader.js'),)
 
     def render(self, name, value, attrs=None):
 
-        substitutions = {
-            'name': name,
-            'filename': '',
-            'file_placeholder': ugettext(
-                'Select a picture from your local folder.'
-            ),
-            'post_note': ugettext(
-                'You’re image will be uploaded/removed '
-                'once you save your changes at the end of this page.'
-            ),
-            'upload_title': ugettext('Upload a picture'),
-            'clear_title': ugettext('Remove the picture'),
-        }
-        snippets = {
-            'name': '{name}',
-            'file_input': (
-                super(widgets.ClearableFileInput, self).render(name, value, {
-                    'id': name,
-                    'class': 'form-control form-control-file'
-                })
-            ),
-            'text_input': (
-                widgets.TextInput().render('__noname__', '{filename}', {
-                    'class': 'form-control form-control-file-dummy',
-                    'placeholder': '{file_placeholder}'
-                })
-            ),
-            'alert': (
-                '<div class="alert alert-info" role="alert">{post_note}</div>'
-            ),
-            'img': '',
-            'checkbox': '',
-        }
+        has_image_set = self.is_initial(value)
+        is_required = self.is_required
 
-        if self.is_initial(value):
-            substitutions['url'] = conditional_escape(value.url)
-            substitutions['filename'] = basename(value.name)
-            snippets['img'] = (
-                '<img src="{url}" class="img-responsive" alt="" />'
-            )
+        file_placeholder = ugettext('Select a picture from your local folder.')
+        file_input = super().render(name, None, {
+            'id': name,
+            'class': 'form-control form-control-file'
+        })
 
-            if not self.is_required:
-                substitutions['checkbox_id'] = self.clear_checkbox_id(name)
-                snippets['button'] = (
-                    '<label for="{checkbox_id}" class="btn btn-danger"'
-                    'title="{clear_title}"><i class="fa fa-trash"></i></label>'
-                )
-                snippets['checkbox'] = widgets.CheckboxInput()\
-                    .render(self.clear_checkbox_name(name), False,
-                            attrs={'id': '{checkbox_id}',
-                                   'class': 'clear-image'})
+        if has_image_set:
+            file_name = basename(value.name)
+            file_url = conditional_escape(value.url)
         else:
-            snippets['button'] = (
-                '<label for="{name}" class="btn btn-default"'
-                'title="{upload_title}">'
-                '<i class="fa fa-cloud-upload"></i></label>'
-            )
+            file_name = ""
+            file_url = ""
+
+        text_input = widgets.TextInput().render('__noname__', file_name, {
+            'class': 'form-control form-control-file-dummy',
+            'placeholder': file_placeholder
+        })
+
+        checkbox_id = self.clear_checkbox_id(name)
+        checkbox_name = self.clear_checkbox_name(name)
+        checkbox_input = widgets.CheckboxInput().render(checkbox_name, False, {
+            'id': checkbox_id,
+            'class': 'clear-image',
+            'data-upload-clear': name,
+        })
+
+        context = {
+            'name': name,
+            'has_image_set': has_image_set,
+            'is_required': is_required,
+            'file_url': file_url,
+            'file_input': file_input,
+            'file_id': name + '-file',
+            'text_input': text_input,
+            'checkbox_input': checkbox_input,
+            'checkbox_id': checkbox_id
+        }
 
-        markup = """
-        <div class="row">
-            <div class="upload-wrapper form-control-upload col-sm-9 col-md-8">
-                {text_input}
-                <span class="input-group-btn">
-                    {button}
-                </span>
-                {file_input}
-                {alert}
-            </div>
-            <div class="col-sm-3 col-md-4">
-                <div class="form-{name}">
-                    {checkbox}
-                    {img}
-                </div>
-            </div>
-        </div>
-        """.format(**snippets).format(**substitutions)
-
-        return mark_safe(markup)
+        return mark_safe(
+            loader.render_to_string(
+                'euth_contrib/image_upload_widget.html',
+                context
+            )
+        )
+
+    def value_from_datadict(self, data, files, name):
+        """
+        Modify value_from_datadict, so that delete takes precedence over
+        upload.
+        """
+        file_value = super(widgets.ClearableFileInput, self)\
+            .value_from_datadict(data, files, name)
+        checkbox_value = widgets.CheckboxInput()\
+            .value_from_datadict(data, files, self.clear_checkbox_name(name))
+        if not self.is_required and checkbox_value:
+            return False
+        return file_value
 
 
 class DateTimeInput(widgets.DateTimeInput):
 
     class Media:
         js = (staticfiles_storage.url('flatpickr.min.js'),
-              'js/dateTimeInput.js')
+              staticfiles_storage.url('js/dateTimeInput.js'))
         css = {'all': [staticfiles_storage.url('flatpickr.min.css')]}
 
     input_type = 'text'


Test patch:
diff --git a/tests/contrib/test_widgets.py b/tests/contrib/test_widgets.py
new file mode 100644
--- /dev/null
+++ b/tests/contrib/test_widgets.py
@@ -0,0 +1,17 @@
+from django.core.files.uploadedfile import SimpleUploadedFile
+
+from euth.contrib import widgets
+
+
+def test_image_input_delete_presedence():
+    input = widgets.ImageInputWidget()
+    jpeg_file = SimpleUploadedFile('test.jpg', b'file content',
+                                   content_type='image/jpeg')
+
+    data = {'test_image-clear': 'on'}
+    files = {'test_image': jpeg_file}
+    value = input.value_from_datadict(data, files, 'test_image')
+    assert value is False
+
+    value = input.value_from_datadict(data, {}, 'test_image')
+    assert value is False
diff --git a/tests/organisations/test_models.py b/tests/organisations/test_models.py
--- a/tests/organisations/test_models.py
+++ b/tests/organisations/test_models.py
@@ -85,10 +85,12 @@ def test_image_deleted_after_update(organisation_factory, ImagePNG):
     assert os.path.isfile(image_path)
     assert os.path.isfile(logo_path)
 
-    organisation.image = None
+    organisation.image = ImagePNG.generate(None, None, True, {})
     organisation.logo = None
     organisation.save()
 
+    new_image_path = os.path.join(settings.MEDIA_ROOT, organisation.image.path)
+    assert os.path.isfile(new_image_path)
     assert not os.path.isfile(thumbnail_image_path)
     assert not os.path.isfile(thumbnail_logo_path)
     assert not os.path.isfile(image_path)