# cython: binding=True
import orjson
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.files.images import ImageFile
from django.db.models import TextField, ForeignKey, FileField, ImageField, Field
from django.db.models.fields.files import FileDescriptor, FieldFile
from django.db.models.query_utils import DeferredAttribute
from vstutils.utils import raise_context_decorator_with_default
"""
These model fields used in :class:`vstutils.api.serializers.VstSerializer`
to form model_field_class: serializer_field_class mapping
"""
[docs]
class MultipleFieldFile(FieldFile):
"""
Subclasses :class:`django.db.models.fields.files.FieldFile`. Provides :meth:`MultipleFieldFile.save`
and :meth:`MultipleFieldFile.delete` to manipulate the underlying file, as well as update the
associated model instance.
"""
def __init__(self, instance, field, name):
super(MultipleFieldFile, self).__init__(instance, field, name)
self._old_name = self.name
def _set_attr_value(self):
"""
Set new value of file to object attr.
"""
setattr(
self.instance,
self.field.attname,
[
self.name if file.name == self._old_name else file
for file in getattr(self.instance, self.field.attname)
]
)
def _clear_attr_value(self):
"""
Pop None values from file list.
"""
setattr(
self.instance,
self.field.attname,
[
file
for file in getattr(self.instance, self.field.attname) if file.name != self.name
]
)
[docs]
def save(self, name, content, save=True):
"""
Save changes in file to storage and to object attr.
"""
self._old_name = self.name
name = self.field.generate_filename(self.instance, name)
self.name = self.storage.save(name, content, max_length=self.field.max_length)
self._set_attr_value()
self._committed = True
if save:
self.instance.save()
[docs]
def delete(self, save=True):
"""
Delete file from storage and from object attr.
"""
if not self:
return # nocv
if hasattr(self, '_file'):
self.close()
del self.file
self.storage.delete(self.name)
self._clear_attr_value()
self._committed = False
if save:
self.instance.save()
[docs]
class MultipleFileDescriptor(FileDescriptor):
"""
Subclasses :class:`django.db.models.fields.files.FileDescriptor` to handle list of files.
Return a list of :class:`MultipleFieldFile` when accessed so you can write code like:
.. sourcecode:: python
from myapp.models import MyModel
instance = MyModel.objects.get(pk=1)
instance.files[0].size
"""
[docs]
def get_file(self, file, instance):
"""
Always return valid attr_class object. For details on logic see
:meth:`django.db.models.fields.files.FileDescriptor.__get__`.
"""
if isinstance(file, str) or file is None:
attr = self.field.attr_class(instance, self.field, file)
file = attr
elif isinstance(file, SimpleUploadedFile):
file_copy = self.field.attr_class(instance, self.field, file.name)
file_copy.file = file
file_copy._committed = False # pylint: disable=W0212 protected-access
file = file_copy
elif isinstance(file, MultipleFieldFile) and instance != file.instance: # nocv
file.instance = instance
file.field = self.field
file.storage = self.field.storage
return file
def __get__(self, instance, cls=None):
"""
Return list of MultipleFieldFile at all times.
"""
if instance is None:
return self # nocv
instance.__dict__[self.field.attname] = [
self.get_file(file, instance)
for file in DeferredAttribute.__get__(self, instance, cls) or []
]
return instance.__dict__[self.field.attname]
[docs]
class MultipleFileMixin:
"""
Mixin suited to use with :class:`django.db.models.fields.files.FieldFile` to transform it to
a Field with list of files.
"""
def __init__(self, **kwargs):
kwargs['max_length'] = None
super().__init__(**kwargs)
[docs]
def pre_save(self, model_instance, add):
"""
Call .save() method on every file in list
"""
files = getattr(model_instance, self.attname)
for file in files:
if file and not file._committed: # pylint: disable=W0212 protected-access
file.save(file.name, file.file, save=False)
return files
[docs]
def get_prep_value(self, value):
"""
Prepare value for database insertion
"""
value = Field.get_prep_value(self, value)
if value is None:
return value
return orjson.dumps(list(map(str, value))).decode('utf-8')
@raise_context_decorator_with_default(default=[])
def from_db_value(self, value, expression, connection):
"""
Transform db value to an internal value
"""
if value:
return orjson.loads(value)
return value # nocv
def get_internal_type(self):
return "TextField"
[docs]
class MultipleFileField(MultipleFileMixin, FileField):
"""
Subclasses :class:`django.db.models.fields.files.FileField`.
Field for storing a list of Storage-kept files. All args passed to FileField.
"""
attr_class = MultipleFieldFile
descriptor_class = MultipleFileDescriptor
description = "List of Files"
[docs]
class MultipleImageFieldFile(ImageFile, MultipleFieldFile):
"""
Subclasses :class:`MultipleFieldFile` and :class:`ImageFile mixin`,
handles deleting _dimensions_cache when file is deleted.
"""
def delete(self, save=True):
if hasattr(self, '_dimensions_cache'):
del self._dimensions_cache
super().delete(save)
[docs]
class MultipleImageField(MultipleFileMixin, ImageField):
"""
Field for storing a list of storage-kept images. All args are passed to
:class:`django.db.models.fields.files.ImageField`, except height_field and width_field,
they are not currently implemented.
"""
attr_class = MultipleImageFieldFile
descriptor_class = MultipleFileDescriptor
description = "List of Images"
def update_dimension_fields(self, instance, force=False, *args, **kwargs):
pass # nocv
[docs]
class NamedBinaryFileInJSONField(TextField):
"""
Extends :class:`django.db.models.TextField`. Use this field in :class:`vstutils.models.BModel` to get
`vstutils.api.NamedBinaryFileInJSONField` in serializer.
"""
[docs]
class NamedBinaryImageInJSONField(NamedBinaryFileInJSONField):
"""
Extends :class:`django.db.models.TextField`. Use this field in :class:`vstutils.models.BModel` to get
`vstutils.api.NamedBinaryImageInJSONField` in serializer.
"""
[docs]
class MultipleNamedBinaryFileInJSONField(TextField):
"""
Extends :class:`django.db.models.TextField`. Use this field in :class:`vstutils.models.BModel` to get
`vstutils.api.MultipleNamedBinaryFileInJSONField` in serializer.
"""
[docs]
class MultipleNamedBinaryImageInJSONField(MultipleNamedBinaryFileInJSONField):
"""
Extends :class:`django.db.models.TextField`. Use this field in :class:`vstutils.models.BModel` to get
`vstutils.api.MultipleNamedBinaryImageInJSONField` in serializer.
"""
[docs]
class FkModelField(ForeignKey):
# pylint: disable=abstract-method
"""
Extends :class:`django.db.models.ForeignKey`. Use this field in :class:`vstutils.models.BModel` to get
`vstutils.api.FkModelField` in serializer. To set Foreign Key relation set `to` argument to string path to model
or to Model Class as in :class:`django.db.models.ForeignKey`
"""
[docs]
class HTMLField(TextField):
"""
Extends :class:`django.db.models.TextField`. A simple field for storing HTML markup.
The field is based on :class:`django.db.models.TextField`, therefore it does not support indexing
and is not recommended for use in filters.
"""
[docs]
class WYSIWYGField(TextField):
"""
Extends :class:`django.db.models.TextField`. A simple field for storing Markdown data.
The field is based on :class:`django.db.models.TextField`, therefore it does not support indexing
and is not recommended for use in filters.
"""