From 7d07348023b35a0916de9a501d2e9b3fc8bba71b Mon Sep 17 00:00:00 2001
From: seroy <seroy@bk.ru>
Date: Sun, 11 Feb 2018 15:08:12 +0300
Subject: [PATCH] introduce AdminCloudinaryJSFileWidget for direct upload and
 CloudinaryFieldsAdminMixin

---
 README.rst                                    |  26 +++++
 cloudinary/admin.py                           |  34 ++++++
 .../static/js/cloudinary-file-widget.js       |  24 ++++
 cloudinary/static/js/jquery.django.init.js    |   1 +
 .../widgets/admin_cloudinary_js_file.html     |   7 ++
 cloudinary/widgets.py                         | 107 ++++++++++++++++++
 6 files changed, 199 insertions(+)
 create mode 100644 cloudinary/admin.py
 create mode 100644 cloudinary/static/js/cloudinary-file-widget.js
 create mode 100644 cloudinary/static/js/jquery.django.init.js
 create mode 100644 cloudinary/templates/widgets/admin_cloudinary_js_file.html
 create mode 100644 cloudinary/widgets.py

diff --git a/README.rst b/README.rst
index 642ee6bd..d498f681 100644
--- a/README.rst
+++ b/README.rst
@@ -364,6 +364,32 @@ Optional parameters:
 
 -  ``public_id`` - The name of the uploaded file in Cloudinary
 
+Django Admin Integration
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+CloudinaryFieldsAdminMixin
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``cloudinary.admin.CloudinaryFieldsAdminMixin`` sets ``django.contrib.admin.widgets.AdminFileWidget``
+for ``cloudinary.models.CloudinaryField`` fields in model and ``cloudinary.widgets.AdminCloudinaryJSFileWidget`` for
+fields which has ``CloudinaryJsFileField`` or ``CloudinaryUnsignedJsFileField`` in ``default_form_class``.
+
+To enable widgets in the admin, you need to inherit from ``CloudinaryFieldsAdminMixin``:
+
+.. code:: python
+
+   from django.contrib import admin
+   from myapp.models import MyCloudinaryModel
+
+   from cloudinary.admin import CloudinaryFieldsAdminMixin
+
+   class MyCloudinaryModelAdmin(CloudinaryFieldsAdminMixin, admin.ModelAdmin):
+       """Any admin options you need go here"""
+
+
+   admin.site.register(MyCloudinaryModelAdmin, MyCloudinaryModelAdmin)
+
+
 Code samples
 ------------
 
diff --git a/cloudinary/admin.py b/cloudinary/admin.py
new file mode 100644
index 00000000..b168d027
--- /dev/null
+++ b/cloudinary/admin.py
@@ -0,0 +1,34 @@
+import copy
+
+from django.contrib.admin.widgets import AdminFileWidget
+
+from cloudinary.models import CloudinaryField
+from cloudinary.forms import CloudinaryJsFileField, CloudinaryUnsignedJsFileField
+from cloudinary.widgets import AdminCloudinaryJSFileWidget
+
+
+FORMFIELD_FOR_CLOUDINARY_FIELDS_DEFAULTS = {
+    CloudinaryField: {'widget': AdminFileWidget},
+}
+
+class CloudinaryFieldsAdminMixin:
+    """Mixin for making the fancy widgets work in Django Admin."""
+
+    def __init__(self, *args, **kwargs):
+        super(CloudinaryFieldsAdminMixin, self).__init__(*args, **kwargs)
+        overrides = FORMFIELD_FOR_CLOUDINARY_FIELDS_DEFAULTS.copy()
+        overrides.update(self.formfield_overrides)
+        self.formfield_overrides = overrides
+
+    def formfield_for_dbfield(self, db_field, request, **kwargs):
+        if isinstance(db_field, CloudinaryField) and \
+                db_field.default_form_class in (CloudinaryJsFileField,
+                                                CloudinaryUnsignedJsFileField):
+            for klass in db_field.__class__.mro():
+                if klass in self.formfield_overrides:
+                    kwargs = dict(
+                        copy.deepcopy(self.formfield_overrides[klass]),
+                        widget=AdminCloudinaryJSFileWidget, **kwargs)
+                    return db_field.formfield(**kwargs)
+        return super(CloudinaryFieldsAdminMixin, self).formfield_for_dbfield(
+            db_field, request, **kwargs)
diff --git a/cloudinary/static/js/cloudinary-file-widget.js b/cloudinary/static/js/cloudinary-file-widget.js
new file mode 100644
index 00000000..598299d7
--- /dev/null
+++ b/cloudinary/static/js/cloudinary-file-widget.js
@@ -0,0 +1,24 @@
+/*global gettext, interpolate */
+(function ($) {
+  'use strict';
+  $(function () {
+    $('.cloudinary-fileupload').each(function () {
+      var status_element = $('#' + $(this).data('status-element-id'));
+      var uploaded_text = $(this).data('uploaded-text');
+      $(this).cloudinary_fileupload({
+        start: function () {
+          status_element.text(gettext('Starting direct upload...'));
+        },
+        progress: function (e, data) {
+          var progress = Math.round((data.loaded * 100.0) / data.total);
+          status_element.text(interpolate('Uploading...%s%', [progress]));
+        }
+      }).on('cloudinaryfail', function (e, data) {
+        status_element.text(interpolate('Upload failed. %s: %s', [data.textStatus, data.errorThrown]));
+      }).on('cloudinarydone', function (e, data) {
+        status_element.text(gettext('Uploaded'));
+        status_element.html(interpolate('%s: <a href="%s">%s</a>', [uploaded_text, data.result.url, data.result.public_id]));
+      });
+    });
+  });
+})(django.jQuery);
diff --git a/cloudinary/static/js/jquery.django.init.js b/cloudinary/static/js/jquery.django.init.js
new file mode 100644
index 00000000..35a47c3d
--- /dev/null
+++ b/cloudinary/static/js/jquery.django.init.js
@@ -0,0 +1 @@
+var jQuery = django.jQuery, $ = jQuery;
diff --git a/cloudinary/templates/widgets/admin_cloudinary_js_file.html b/cloudinary/templates/widgets/admin_cloudinary_js_file.html
new file mode 100644
index 00000000..27411e0c
--- /dev/null
+++ b/cloudinary/templates/widgets/admin_cloudinary_js_file.html
@@ -0,0 +1,7 @@
+{% include 'admin/widgets/clearable_file_input.html' with widget=widget.file_input %}
+{% include 'django/forms/widgets/hidden.html' with widget=widget.hidden_input %}
+{% if not widget.is_initial and widget.value %}
+    <p id="{{ widget.status_element_id }}" class="file-upload">{{ widget.upload_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a></p>
+{% else %}
+    <p id="{{ widget.status_element_id }}" class="file-upload"></p>
+{% endif %}
diff --git a/cloudinary/widgets.py b/cloudinary/widgets.py
new file mode 100644
index 00000000..5bf1a8a2
--- /dev/null
+++ b/cloudinary/widgets.py
@@ -0,0 +1,107 @@
+import json
+
+from django import forms
+from django.conf import settings
+from django.contrib.admin.widgets import AdminFileWidget
+from django.forms import HiddenInput, Widget, CheckboxInput
+from django.utils.translation import gettext as _
+
+from cloudinary import CloudinaryResource
+from cloudinary.models import CloudinaryField
+import cloudinary.utils
+
+
+class AdminCloudinaryJSFileWidget(Widget):
+    initial_text = _('Currently')
+    uploaded_text = _('New')
+    input_text = _('Change')
+    template_name = 'cloudinary/widgets/admin_cloudinary_js_file.html'
+
+    @property
+    def media(self):
+        extra = '' if settings.DEBUG else '.min'
+        js = [
+            'admin/js/vendor/jquery/jquery%s.js' % extra,
+            'admin/js/jquery.init.js',
+            'js/jquery.django.init.js',
+            'js/jquery.ui.widget.js',
+            'js/jquery.iframe-transport.js',
+            'js/jquery.fileupload.js',
+            'js/jquery.cloudinary.js',
+            'js/cloudinary-file-widget.js'
+        ]
+        return forms.Media(js=js)
+
+    def status_element_id(self, name):
+        """
+        Given the name of the status element, return the HTML id for it.
+        """
+        return name + '-status_id'
+
+    def get_context(self, name, value, attrs):
+        options = attrs.pop('options', {})
+        params = cloudinary.utils.build_upload_params(**options)
+        if options.get("unsigned"):
+            params = cloudinary.utils.cleanup_params(params)
+        else:
+            params = cloudinary.utils.sign_request(params, options)
+
+        if 'resource_type' not in options: options['resource_type'] = 'auto'
+        cloudinary_upload_url = cloudinary.utils.cloudinary_api_url("upload", **options)
+
+        attrs["data-url"] = cloudinary_upload_url
+        attrs["data-form-data"] = json.dumps(params)
+        attrs["data-cloudinary-field"] = name
+        attrs["data-status-element-id"] = self.status_element_id(name)
+        attrs["data-uploaded-text"] = self.uploaded_text
+        chunk_size = options.get("chunk_size", None)
+        if chunk_size: attrs["data-max-chunk-size"] = chunk_size
+        attrs["class"] = " ".join(["cloudinary-fileupload", attrs.get("class", "")])
+
+        admin_file_widget = AdminFileWidget()
+        admin_file_widget.initial_text = self.initial_text
+        admin_file_widget.input_text = self.input_text
+        admin_file_widget.is_required = self.is_required
+        file_widget_context = admin_file_widget.get_context(name, value, attrs)
+        # override input name attribute because real value are store in the hidden input
+        file_widget_context['widget']['name'] = 'file'
+        context = super(AdminCloudinaryJSFileWidget, self).get_context(name, value, attrs)
+        context['widget'].update({
+            'file_input': file_widget_context['widget'],
+            'hidden_input': HiddenInput().get_context(name, self.format_value(value), {})['widget'],
+            'status_element_id': self.status_element_id(name),
+            'is_initial': self.is_initial(value),
+            'upload_text': self.uploaded_text
+        })
+        if value and not self.is_initial(value) and not isinstance(value, CloudinaryResource):
+            context['widget']['value'] = CloudinaryField().parse_cloudinary_resource(value)
+        return context
+
+    def is_initial(self, value):
+        """
+        Return whether value is considered to be initial value.
+        """
+        return bool(value and getattr(value, 'url', False))
+
+    def format_value(self, value):
+        if isinstance(value, CloudinaryResource):
+            if value:
+                return value.get_presigned()
+            else:
+                return None
+        return super(AdminCloudinaryJSFileWidget, self).format_value(value)
+
+    def value_from_datadict(self, data, files, name):
+        if not self.is_required and CheckboxInput().value_from_datadict(
+                data, files, AdminFileWidget().clear_checkbox_name(name)):
+            return None
+        return super(AdminCloudinaryJSFileWidget, self).value_from_datadict(data, files, name)
+
+    def use_required_attribute(self, initial):
+        return super(AdminCloudinaryJSFileWidget, self).use_required_attribute(initial) and not initial
+
+    def value_omitted_from_data(self, data, files, name):
+        return (
+                super(AdminCloudinaryJSFileWidget, self).value_omitted_from_data(data, files, name) and
+                AdminFileWidget().clear_checkbox_name(name) not in data
+        )