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: %s', [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 %} +

{{ widget.upload_text }}: {{ widget.value }}

+{% else %} +

+{% 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 + )