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