1import re 2 3from cloudinary import CloudinaryResource, forms, uploader 4from django.core.files.uploadedfile import UploadedFile 5from django.db import models 6from cloudinary.uploader import upload_options 7from cloudinary.utils import upload_params 8 9# Add introspection rules for South, if it's installed. 10try: 11 from south.modelsinspector import add_introspection_rules 12 add_introspection_rules([], ["^cloudinary.models.CloudinaryField"]) 13except ImportError: 14 pass 15 16CLOUDINARY_FIELD_DB_RE = r'(?:(?P<resource_type>image|raw|video)/' \ 17 r'(?P<type>upload|private|authenticated)/)?' \ 18 r'(?:v(?P<version>\d+)/)?' \ 19 r'(?P<public_id>.*?)' \ 20 r'(\.(?P<format>[^.]+))?$' 21 22 23def with_metaclass(meta, *bases): 24 """ 25 Create a base class with a metaclass. 26 27 This requires a bit of explanation: the basic idea is to make a dummy 28 metaclass for one level of class instantiation that replaces itself with 29 the actual metaclass. 30 31 Taken from six - https://pythonhosted.org/six/ 32 """ 33 class metaclass(meta): 34 def __new__(cls, name, this_bases, d): 35 return meta(name, bases, d) 36 return type.__new__(metaclass, 'temporary_class', (), {}) 37 38 39class CloudinaryField(models.Field): 40 description = "A resource stored in Cloudinary" 41 42 def __init__(self, *args, **kwargs): 43 self.default_form_class = kwargs.pop("default_form_class", forms.CloudinaryFileField) 44 self.type = kwargs.pop("type", "upload") 45 self.resource_type = kwargs.pop("resource_type", "image") 46 self.width_field = kwargs.pop("width_field", None) 47 self.height_field = kwargs.pop("height_field", None) 48 # Collect all options related to Cloudinary upload 49 self.options = {key: kwargs.pop(key) for key in set(kwargs.keys()) if key in upload_params + upload_options} 50 51 field_options = kwargs 52 field_options['max_length'] = 255 53 super(CloudinaryField, self).__init__(*args, **field_options) 54 55 def get_internal_type(self): 56 return 'CharField' 57 58 def value_to_string(self, obj): 59 """ 60 We need to support both legacy `_get_val_from_obj` and new `value_from_object` models.Field methods. 61 It would be better to wrap it with try -> except AttributeError -> fallback to legacy. 62 Unfortunately, we can catch AttributeError exception from `value_from_object` function itself. 63 Parsing exception string is an overkill here, that's why we check for attribute existence 64 65 :param obj: Value to serialize 66 67 :return: Serialized value 68 """ 69 70 if hasattr(self, 'value_from_object'): 71 value = self.value_from_object(obj) 72 else: # fallback for legacy django versions 73 value = self._get_val_from_obj(obj) 74 75 return self.get_prep_value(value) 76 77 def parse_cloudinary_resource(self, value): 78 m = re.match(CLOUDINARY_FIELD_DB_RE, value) 79 resource_type = m.group('resource_type') or self.resource_type 80 upload_type = m.group('type') or self.type 81 return CloudinaryResource( 82 type=upload_type, 83 resource_type=resource_type, 84 version=m.group('version'), 85 public_id=m.group('public_id'), 86 format=m.group('format') 87 ) 88 89 def from_db_value(self, value, expression, connection, *args, **kwargs): 90 # TODO: when dropping support for versions prior to 2.0, you may return 91 # the signature to from_db_value(value, expression, connection) 92 if value is not None: 93 return self.parse_cloudinary_resource(value) 94 95 def to_python(self, value): 96 if isinstance(value, CloudinaryResource): 97 return value 98 elif isinstance(value, UploadedFile): 99 return value 100 elif value is None or value is False: 101 return value 102 else: 103 return self.parse_cloudinary_resource(value) 104 105 def pre_save(self, model_instance, add): 106 value = super(CloudinaryField, self).pre_save(model_instance, add) 107 if isinstance(value, UploadedFile): 108 options = {"type": self.type, "resource_type": self.resource_type} 109 options.update(self.options) 110 if hasattr(value, 'seekable') and value.seekable(): 111 value.seek(0) 112 instance_value = uploader.upload_resource(value, **options) 113 setattr(model_instance, self.attname, instance_value) 114 if self.width_field: 115 setattr(model_instance, self.width_field, instance_value.metadata.get('width')) 116 if self.height_field: 117 setattr(model_instance, self.height_field, instance_value.metadata.get('height')) 118 return self.get_prep_value(instance_value) 119 else: 120 return value 121 122 def get_prep_value(self, value): 123 if not value: 124 return self.get_default() 125 if isinstance(value, CloudinaryResource): 126 return value.get_prep_value() 127 else: 128 return value 129 130 def formfield(self, **kwargs): 131 options = {"type": self.type, "resource_type": self.resource_type} 132 options.update(kwargs.pop('options', {})) 133 defaults = {'form_class': self.default_form_class, 'options': options, 'autosave': False} 134 defaults.update(kwargs) 135 return super(CloudinaryField, self).formfield(**defaults) 136